diff options
Diffstat (limited to 'activerecord')
332 files changed, 16026 insertions, 17247 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 69cf1193b6..c0c8096084 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,6861 +1,748 @@ ## Rails 4.0.0 (unreleased) ## -* Implemented ActiveRecord::Relation#none method - - The `none` method returns a chainable relation with zero records - (an instance of the NullRelation class). - - Any subsequent condition chained to the returned relation will continue - generating an empty relation and will not fire any query to the database. - - *Juanjo Bazán* +* Rails now raise an exception when you're trying to run a migration that has an invalid + file name. Only lower case letters, numbers, and '_' are allowed in migration's file name. + Please see #7419 for more details. -* Added the `ActiveRecord::NullRelation` class implementing the null - object pattern for the Relation class. *Juanjo Bazán* + *Jan Bernacki* -* Added deprecation for the `:dependent => :restrict` association option. +* Fix bug when call `store_accessor` multiple times. + Fixes #7532. - 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. - - * New rails application would be generated with the - `config.active_record.dependent_restrict_raises = false` in the - application config. - - *Manoj Kumar* - -* Added `create_join_table` migration helper to create HABTM join tables - - create_join_table :products, :categories - # => - # create_table :categories_products, :id => false do |td| - # td.integer :product_id, :null => false - # td.integer :category_id, :null => false - # end + *Matt Jones* - *Rafael Mendonça França* +* Fix store attributes that show the changes incorrectly. + Fixes #7532. -* The primary key is always initialized in the @attributes hash to nil (unless - another value has been specified). + *Matt Jones* -* In previous releases, the following would generate a single query with - an `OUTER JOIN comments`, rather than two separate queries: +* Fix `ActiveRecord::Relation#pluck` when columns or tables are reserved words. - Post.includes(:comments) - .where("comments.name = 'foo'") + *Ian Lesperance* - This behaviour relies on matching SQL string, which is an inherently - flawed idea unless we write an SQL parser, which we do not wish to - do. +* Allow JSON columns to be created in PostgreSQL and properly encoded/decoded + to/from database. - Therefore, it is now deprecated. + *Dickson S. Guedes* - To avoid deprecation warnings and for future compatibility, you must - explicitly state which tables you reference, when using SQL snippets: +* Fix time column type casting for invalid time string values to correctly return nil. - Post.includes(:comments) - .where("comments.name = 'foo'") - .references(:comments) + *Adam Meehan* - Note that you do not need to explicitly specify references in the - following cases, as they can be automatically inferred: +* Allow to pass Symbol or Proc into :limit option of #accepts_nested_attributes_for - Post.where(comments: { name: 'foo' }) - Post.where('comments.name' => 'foo') - Post.order('comments.name') + *Mikhail Dieterle* - You also do not need to worry about this unless you are doing eager - loading. Basically, don't worry unless you see a deprecation warning - or (in future releases) an SQL error due to a missing JOIN. +* 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* - [Jon Leighton] +* 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. -* Support for the `schema_info` table has been dropped. Please - switch to `schema_migrations`. + *Dave Desrochers* -* Connections *must* be closed at the end of a thread. If not, your - connection pool can fill and an exception will be raised. +* `serialized_attributes` and `_attr_readonly` become class method only. Instance reader methods are deprecated. -* Added the `ActiveRecord::Model` module which can be included in a - class as an alternative to inheriting from `ActiveRecord::Base`: + *kennyj* - class Post - include ActiveRecord::Model - end +* Round usec when comparing timestamp attributes in the dirty tracking. + Fixes #6975. - Please note: + *kennyj* - * Up until now it has been safe to assume that all AR models are - descendants of `ActiveRecord::Base`. This is no longer a safe - assumption, but it may transpire that there are areas of the - code which still make this assumption. So there may be - 'teething difficulties' with this feature. (But please do try it - and report bugs.) +* Use inversed parent for first and last child of has_many association. - * Plugins & libraries etc that add methods to `ActiveRecord::Base` - will not be compatible with `ActiveRecord::Model`. Those libraries - should add to `ActiveRecord::Model` instead (which is included in - `Base`), or better still, avoid monkey-patching AR and instead - provide a module that users can include where they need it. + *Ravil Bayramgalin* - * To minimise the risk of conflicts with other code, it is - advisable to include `ActiveRecord::Model` early in your class - definition. +* 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. - *Jon Leighton* + *Ari Pollak* -* PostgreSQL hstore records can be created. +* Raise `ArgumentError` if list of attributes to change is empty in `update_all`. -* PostgreSQL hstore types are automatically deserialized from the database. + *Roman Shatsov* +* Fix AR#create to return an unsaved record when AR::RecordInvalid is + raised. Fixes #3217. -## Rails 3.2.1 (unreleased) ## + *Dave Yeu* -* The threshold for auto EXPLAIN is ignored if there's no logger. *fxn* +* Fixed table name prefix that is generated in engines for namespaced models + *Wojciech Wnętrzak* -* Fix possible race condition when two threads try to define attribute - methods for the same class. +* Make sure `:environment` task is executed before `db:schema:load` or `db:structure:load` + Fixes #4772. + *Seamus Abshere* -## Rails 3.2.0 (January 20, 2012) ## +* Allow Relation#merge to take a proc. -* 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!`. + This was requested by DHH to allow creating of one's own custom + association macros. - Before: + For example: - class Order < ActiveRecord::Base - def cancel! - transaction do - lock! - # ... cancelling logic - end + module Commentable + def has_many_comments(extra) + has_many :comments, -> { where(:foo).merge(extra) } end end - After: - - class Order < ActiveRecord::Base - def cancel! - with_lock do - # ... cancelling logic - end - end + class Post < ActiveRecord::Base + extend Commentable + has_many_comments -> { where(:bar) } 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. +* Add CollectionProxy#scope - *Jon Leighton* + This can be used to get a Relation from an association. -* In development mode the db:drop task also drops the test database. For symmetry with - the db:create task. *Dmitriy Kiriyenko* + Previously we had a #scoped method, but we're deprecating that for + AR::Base, so it doesn't make sense to have it here. -* Added ActiveRecord::Base.store for declaring simple single-column key/value stores *DHH* + This was requested by DHH, to facilitate code like this: - class User < ActiveRecord::Base - store :settings, accessors: [ :color, :homepage ] + 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 - 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.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' - -* Active Record's dynamic finder will now raise the error if you passing in less number of arguments than what you call in method signature. - - So if you were doing this and expecting the second argument to be nil: +* Add `Relation#load` - User.find_by_username_and_group("sikachu") + This method explicitly loads the records and then returns `self`. - You'll now get `ArgumentError: wrong number of arguments (1 for 2).` You'll then have to do this: + 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. - User.find_by_username_and_group("sikachu", nil) - - *Prem Sichanugrist* - - -## 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 - -* 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 + @posts = Post.where(published: true).load *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' +* `Model.all` now returns an `ActiveRecord::Relation`, rather than an + array of records. Use ``Relation#to_a` if you really want an array. - *lighthouse #6262* + 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. -* 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. + 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. - This prevents namespace pollution, separates concerns, and will allow further refactorings. + `Model.scoped` is deprecated in favour of `Model.all`. - 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. + `Relation#all` still returns an array, but is deprecated (since it + would serve no purpose if we made it return a `Relation`). *Jon Leighton* -* 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: - - class Post < ActiveRecord::Base - has_many :taggings - has_many :tags, :through => :taggings - end - - 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 - - post = Post.first - tag = post.tags.build :name => "ruby" - tag.save # will save a Taggable linking to the post - - *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. - - *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. - - 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. - - If you wish to destroy the records themselves, you can do records.association.each(&:destroy) - - *Jon Leighton* - -* 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: - - change_table(:users, :bulk => true) do |t| - t.string :company_name - t.change :birthdate, :datetime - end - - This will now result in: - - ALTER TABLE `users` ADD COLUMN `company_name` varchar(255), CHANGE `updated_at` `updated_at` datetime DEFAULT NULL - -* 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: +* `:finder_sql` and `:counter_sql` options on collection associations + are deprecated. Please transition to using scopes. - Model.limit(10).scoping { Model.all.size } - - *Aaron Patterson* - - -## 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* - -* Quote the ORDER BY clause in batched finds - fixes #6620 *Andrew White* - -* 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. +* `: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 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) ## - -* Changed update_attribute to not run callbacks and update the record directly in the database *Neeraj Singh* - -* Add scoping and unscoped as the syntax to replace the old with_scope and with_exclusive_scope *José Valim* - -* New rake task, db:migrate:status, displays status of migrations #4947 *Kevin Skoglund* - -* 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* + rails g migration create_join_table_for_artists_and_musics artist_id:index music_id -* SQLServer: cope with columns with "desc" in the name. #1950 *Ron Lusk, Ryan Tomayko* + *Aleksey Magusev* -* SQLServer: cope with primary keys with "select" in the name. #3057 *rdifrango@captechventures.com* +* Add `add_reference` and `remove_reference` schema statements. Aliases, `add_belongs_to` + and `remove_belongs_to` are acceptable. References are reversible. + Examples: -* Oracle: active? performs a select instead of a commit. #3133 *Michael Schoen* + # Create a user_id column + add_reference(:products, :user) + # Create a supplier_id, supplier_type columns and appropriate index + add_reference(:products, :supplier, polymorphic: true, index: true) + # Remove polymorphic reference + remove_reference(:products, :supplier, polymorphic: true) -* MySQL: more robust test for nullified result hashes. #3124 *Stefan Kaes* + *Aleksey Magusev* -* Reloading an instance refreshes its aggregations as well as its associations. #3024 *François Beausoleil* +* Add `:default` and `:null` options to `column_exists?`. -* Fixed that using :include together with :conditions array in Base.find would cause NoMethodError #2887 *Paul Hammmond* + column_exists?(:testings, :taggable_id, :integer, null: false) + column_exists?(:testings, :taggable_type, :string, default: 'Photo') -* PostgreSQL: more robust sequence name discovery. #3087 *Rick Olson* + *Aleksey Magusev* -* Oracle: use syntax compatible with Oracle 8. #3131 *Michael Schoen* +* `ActiveRecord::Relation#inspect` now makes it clear that you are + dealing with a `Relation` object rather than an array:. -* 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* + User.where(:age => 30).inspect + # => <ActiveRecord::Relation [#<User ...>, #<User ...>, ...]> -* Added preliminary support for polymorphic associations *David Heinemeier Hansson* + User.where(:age => 30).to_a.inspect + # => [#<User ...>, #<User ...>] -* Added preliminary support for join models *David Heinemeier Hansson* + The number of records displayed will be limited to 10. -* Allow validate_uniqueness_of to be scoped by more than just one column. #1559. *jeremy@jthopple.com, Marcel Molina Jr.* + *Brian Cardarella, Jon Leighton & Damien Mathieu* -* 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 a 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]. +* Add `collation` and `ctype` support to PostgreSQL. These are available for PostgreSQL 8.4 or later. 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 a 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: + username: foo + password: bar 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: + collation: ja_JP.UTF8 + ctype: ja_JP.UTF8 - 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 + *kennyj* - This doesn't yet work with the DB2 or MS SQL adapters. Patches to make that happen are encouraged. +* 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. -* 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* + *Nick Monje & Brent Wheeldon* -* Improved the performance of the OCI8 adapter for Oracle #723 *pilx/gjenkins* +* `FinderMethods#exists?` now returns `false` with the `false` argument. -* 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* + *Egor Lynko* -* Added with additional settings for working with transactional fixtures and pre-loaded test databases #865 *mindel* +* Added support for specifying the precision of a timestamp in the postgresql + adapter. So, instead of having to incorrectly specify the precision using the + `:limit` option, you may use `:precision`, as intended. For example, in a migration: -* 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 + def change + create_table :foobars do |t| + t.timestamps :precision => 0 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. + *Tony Schneider* - === Collections +* Allow `ActiveRecord::Relation#pluck` to accept multiple columns. Returns an + array of arrays containing the typecasted values: - * 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. + Person.pluck(:id, :name) + # SELECT people.id, people.name FROM people + # [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] -* Added replace to associations, so you can do project.manager.replace(new_manager) or project.milestones.replace(new_milestones) #402 *Tim Bates* + *Jeroen van Ingen & Carlos Antonio da Silva* -* Added build and create methods to has_one and belongs_to associations, so you can now do project.manager.build(attributes) #402 *Tim Bates* +* Improve the derivation of HABTM join table name to take account of nesting. + It now takes the table names of the two models, sorts them lexically and + then joins them, stripping any common prefix from the second table name. -* 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* + Some examples: -* Fixed that Base#== wouldn't work for multiple references to the same unsaved object #402 *Tim Bates* + Top level models (Category <=> Product) + Old: categories_products + New: categories_products -* Fixed binary support for PostgreSQL #444 *alex@byzantine.no* + Top level models with a global table_name_prefix (Category <=> Product) + Old: site_categories_products + New: site_categories_products -* 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. + Nested models in a module without a table_name_prefix method (Admin::Category <=> Admin::Product) + Old: categories_products + New: categories_products -* 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* + Nested models in a module with a table_name_prefix method (Admin::Category <=> Admin::Product) + Old: categories_products + New: admin_categories_products -* Fixed that foreign keys named the same as the association would cause stack overflow #437 *Eric Anderson* + Nested models in a parent model (Catalog::Category <=> Catalog::Product) + Old: categories_products + New: catalog_categories_products -* Fixed default scope of acts_as_list from "1" to "1 = 1", so it'll work in PostgreSQL (among other places) #427 *Alexey* + Nested models in different parent models (Catalog::Category <=> Content::Page) + Old: categories_pages + New: catalog_categories_content_pages -* 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 + *Andrew White* -* Added Base.validate_presence as an alternative to implementing validate and doing errors.add_on_empty yourself. +* Move HABTM validity checks to `ActiveRecord::Reflection`. One side effect of + this is to move when the exceptions are raised from the point of declaration + to when the association is built. This is consistant with other association + validity checks. -* 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". + *Andrew White* - class Person < ActiveRecord::Base - validates_uniqueness_of :user_name - end +* Added `stored_attributes` hash which contains the attributes stored using + `ActiveRecord::Store`. This allows you to retrieve the list of attributes + you've defined. - 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. + class User < ActiveRecord::Base + store :settings, accessors: [:color, :homepage] + end + User.stored_attributes[:settings] # [:color, :homepage] -* Added Base.validates_confirmation_of that encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: + *Joost Baaij & Carlos Antonio da Silva* - Model: - class Person < ActiveRecord::Base - validates_confirmation_of :password - end +* 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. - View: - <%= password_field "person", "password" %> - <%= password_field "person", "password_confirmation" %> + *kennyj* - 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. +* Add uuid datatype support to PostgreSQL adapter. *Konstantin Shabanov* +* Added `ActiveRecord::Migration.check_pending!` that raises an error if + migrations are pending. *Richard Schneeman* -* 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: +* Added `#destroy!` which acts like `#destroy` but will raise an + `ActiveRecord::RecordNotDestroyed` exception instead of returning `false`. - class Person < ActiveRecord::Base - validates_acceptance_of :terms_of_service - end + *Marc-André Lafortune* - The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed both on create and update. +* Allow blocks for `count` with `ActiveRecord::Relation`, to work similar as + `Array#count`: - 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. + Person.where("age > 26").count { |person| person.gender == 'female' } + *Chris Finne & Carlos Antonio da Silva* -* Added validation macros to make the stackable just like the life cycle callbacks. Examples: +* Added support to `CollectionAssociation#delete` for passing `fixnum` + or `string` values as record ids. This finds the records responding + to the `id` and executes delete on them. 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 a 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 + has_many :pets 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. + person.pets.delete("1") # => [#<Pet id: 1>] + person.pets.delete(2, 3) # => [#<Pet id: 2>, #<Pet id: 3>] -* Fixed unnecessary calls to SET AUTOCOMMIT=0/1 for MySQL adapter *Andreas Schwarz* + *Francesco Rodriguez* -* Fixed PostgreSQL defaults are now handled gracefully *Dave Steinberg* +* Deprecated most of the 'dynamic finder' methods. All dynamic methods + except for `find_by_...` and `find_by_...!` are deprecated. Here's + how you can rewrite the code: -* Fixed increment/decrement_counter are now atomic updates *Andreas Schwarz* + * `find_all_by_...` can be rewritten using `where(...)` + * `find_last_by_...` can be rewritten using `where(...).last` + * `scoped_by_...` can be rewritten using `where(...)` + * `find_or_initialize_by_...` can be rewritten using + `where(...).first_or_initialize` + * `find_or_create_by_...` can be rewritten using + `where(...).first_or_create` + * `find_or_create_by_...!` can be rewritten using + `where(...).first_or_create!` -* Fixed the problems the Inflector had turning Attachment into attuchments and Cases into Casis *radsaq/Florian Gross* + The implementation of the deprecated dynamic finders has been moved + to the `activerecord-deprecated_finders` gem. See below for details. -* 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: + *Jon Leighton* - 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". +* Deprecated the old-style hash based finder API. This means that + methods which previously accepted "finder options" no longer do. For + example this: - 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: + Post.find(:all, :conditions => { :comments_count => 10 }, :limit => 5) - class Mouse < ActiveRecord::Base - def self.table_name() "mice" end - end + Should be rewritten in the new style which has existed since Rails 3: - This conversion is now done through an external class called Inflector residing in lib/active_record/support/inflector.rb. + Post.where(comments_count: 10).limit(5) -* Added find_all_in_collection to has_many defined collections. Works like this: + Note that as an interim step, it is possible to rewrite the above as: - class Firm < ActiveRecord::Base - has_many :clients - end + Post.all.merge(:where => { :comments_count => 10 }, :limit => 5) - firm.id # => 1 - firm.find_all_in_clients "revenue > 1000" # SELECT * FROM clients WHERE firm_id = 1 AND revenue > 1000 + This could save you a lot of work if there is a lot of old-style + finder usage in your application. - *Requested by Dave Thomas* + `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: -* Fixed finders for inheritance hierarchies deeper than one level *Florian Weber* + * `:conditions` becomes `:where` + * `:include` becomes `:includes` + * `:extend` becomes `:extending` -* Added add_on_boundry_breaking to errors to accompany add_on_empty as a default validation method. It's used like this: + The code to implement the deprecated features has been moved out to + 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 + the Rails core team until Rails 5.0 is released. - class Person < ActiveRecord::Base - protected - def validation - errors.add_on_boundry_breaking "password", 3..20 - end - end + *Jon Leighton* - 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. +* It's not possible anymore to destroy a model marked as read only. -* 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. + *Johannes Barre* -* Allow for domain sockets in PostgreSQL by not assuming localhost when no host is specified *Scott Barron* +* Added ability to ActiveRecord::Relation#from to accept other ActiveRecord::Relation objects -* Fixed that bignums are saved properly instead of attempted to be YAMLized *Andreas Schwartz* + Record.from(subquery) + Record.from(subquery, :a) -* Fixed a bug in the GEM where the rdoc options weren't being passed according to spec *Chad Fowler* + *Radoslav Stankov* -* Fixed a bug with the exclusively_dependent option for has_many +* Added custom coders support for ActiveRecord::Store. Now you can set + your custom coder like this: + store :settings, accessors: [ :color, :homepage ], coder: JSON -## 0.9.4 ## + *Andrey Voronkov* -* Correctly guesses the primary key when the class is inside a module [Dave Steinberg]. +* `mysql` and `mysql2` connections will set `SQL_MODE=STRICT_ALL_TABLES` by + default to avoid silent data loss. This can be disabled by specifying + `strict: false` in your `database.yml`. -* Added [] and []= as alternatives to read_attribute and write_attribute *Dave Steinberg* + *Michael Pearson* -* has_and_belongs_to_many now accepts an :order key to determine in which order the collection is returned [radsaq]. +* Added default order to `first` to assure consistent results among + diferent database engines. Introduced `take` as a replacement to + the old behavior of `first`. -* The ids passed to find and find_on_conditions are now automatically sanitized. + *Marcelo Silveira* -* Added escaping of plings in YAML content. +* Added an :index option to automatically create indexes for references + and belongs_to statements in migrations. -* Multi-parameter assigns where all the parameters are empty will now be set to nil instead of a new instance of their class. + The `references` and `belongs_to` methods now support an `index` + option that receives either a boolean value or an options hash + that is identical to options available to the add_index method: -* Proper type within an inheritance hierarchy is now ensured already at object initialization (instead of first at create) + create_table :messages do |t| + t.references :person, :index => true + end + Is the same as: -## 0.9.3 ## + create_table :messages do |t| + t.references :person + end + add_index :messages, :person_id -* Fixed bug with using a different primary key name together with has_and_belongs_to_many *Investigation by Scott* + Generators have also been updated to use the new syntax. -* Added :exclusively_dependent option to the has_many association macro. The doc reads: + [Joshua Wood] - 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 bang methods for mutating `ActiveRecord::Relation` objects. + For example, while `foo.where(:bar)` will return a new object + leaving `foo` unchanged, `foo.where!(:bar)` will mutate the foo + object -* 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. + *Jon Leighton* -* 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* +* Added `#find_by` and `#find_by!` to mirror the functionality + provided by dynamic finders in a way that allows dynamic input more + easily: -* Fixed the use of floats (was broken since 0.9.0+) + Post.find_by name: 'Spartacus', rating: 4 + Post.find_by "published_at < ?", 2.weeks.ago + Post.find_by! name: 'Spartacus' -* Fixed PostgreSQL adapter so default values are displayed properly when used in conjunction with - Action Pack scaffolding. + *Jon Leighton* -* Fixed booleans support for PostgreSQL (use real true/false on boolean fields instead of 0/1 on tinyints) *radsaq* +* Added ActiveRecord::Base#slice to return a hash of the given methods with + their names as keys and returned values as values. + *Guillermo Iguaran* -## 0.9.2 ## +* Deprecate eager-evaluated scopes. -* Added static method for instantly updating a record + Don't use this: -* Treat decimal and numeric as Ruby floats *Andreas Schwartz* + scope :red, where(color: 'red') + default_scope where(color: 'red') -* Treat chars as Ruby strings (fixes problem for Action Pack form helpers too) + Use this: -* Removed debugging output accidently left in (which would screw web applications) + scope :red, -> { where(color: 'red') } + default_scope { where(color: 'red') } + The former has numerous issues. It is a common newbie gotcha to do + the following: -## 0.9.1 ## + scope :recent, where(published_at: Time.now - 2.weeks) -* Added MIT license + Or a more subtle variant: -* Added natural object-style assignment for has_and_belongs_to_many associations. Consider the following model: + scope :recent, -> { where(published_at: Time.now - 2.weeks) } + scope :recent_red, recent.where(color: 'red') - class Event < ActiveRecord::Base - has_one_and_belongs_to_many :sponsors - end + Eager scopes are also very complex to implement within Active + Record, and there are still bugs. For example, the following does + not do what you expect: - class Sponsor < ActiveRecord::Base - has_one_and_belongs_to_many :sponsors - end + scope :remove_conditions, except(:where) + where(...).remove_conditions # => still has conditions - Earlier, you'd have to use synthetic methods for creating associations between two objects of the above class: + *Jon Leighton* - roskilde_festival.add_to_sponsors(carlsberg) - roskilde_festival.remove_from_sponsors(carlsberg) +* Remove IdentityMap - nike.add_to_events(world_cup) - nike.remove_from_events(world_cup) + IdentityMap has never graduated to be an "enabled-by-default" feature, due + to some inconsistencies with associations, as described in this commit: - Now you can use regular array-styled methods: + https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6 - roskilde_festival.sponsors << carlsberg - roskilde_festival.sponsors.delete(carlsberg) + Hence the removal from the codebase, until such issues are fixed. - nike.events << world_cup - nike.events.delete(world_cup) + *Carlos Antonio da Silva* -* 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: +* Added the schema cache dump feature. - class Post < ActiveRecord::Base - has_many :comments - end + `Schema cache dump` feature was implemetend. This feature can dump/load internal state of `SchemaCache` instance + because we want to boot rails more quickly when we have many models. - class Comment < ActiveRecord::Base - belongs_to :post - end + Usage notes: - You could do something like: + 1) execute rake task. + RAILS_ENV=production bundle exec rake db:schema:cache:dump + => generate db/schema_cache.dump - funny_comment.has_post? # => true - announcement.comments.delete(funny_comment) - funny_comment.has_post? # => false + 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 + => use db/schema_cache.dump -## 0.9.0 ## + 4) If you remove clear dumped cache, execute rake task. + RAILS_ENV=production bundle exec rake db:schema:cache:clear + => remove db/schema_cache.dump -* 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* + *kennyj* -* 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 support for partial indices to PostgreSQL adapter -* Added callback objects and procs as options for implementing the target for callback macros. + The `add_index` method now supports a `where` option that receives a + string with the partial index criteria. -* Added "counter_cache" option to belongs_to that automates the usage of increment_counter and decrement_counter. Consider: + add_index(:accounts, :code, :where => "active") - class Post < ActiveRecord::Base - has_many :comments - end + Generates - class Comment < ActiveRecord::Base - belongs_to :post - end + CREATE INDEX index_accounts_on_code ON accounts(code) WHERE active - Iterating over 100 posts like this: + *Marcelo Silveira* - <% 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 +* Implemented ActiveRecord::Relation#none method - Earlier, assignments would work like following regardless of which way the assignment told the best story: + The `none` method returns a chainable relation with zero records + (an instance of the NullRelation class). - active_record.manager_id = david.id + Any subsequent condition chained to the returned relation will continue + generating an empty relation and will not fire any query to the database. - Now you can do it either from the belonging side: + *Juanjo Bazán* - david.project = active_record +* Added the `ActiveRecord::NullRelation` class implementing the null + object pattern for the Relation class. *Juanjo Bazán* - ...or from the having side: +* Added new `:dependent => :restrict_with_error` option. This will add + an error to the model, rather than raising an exception. - active_record.manager = david + The `:restrict` option is renamed to `:restrict_with_exception` to + make this distinction explicit. - 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. + *Manoj Kumar & Jon Leighton* -* Added natural object-style assignment for has_many associations [Florian Weber]. Consider the following model: +* Added `create_join_table` migration helper to create HABTM join tables - class Project < ActiveRecord::Base - has_many :milestones - end + create_join_table :products, :categories + # => + # create_table :categories_products, :id => false do |td| + # td.integer :product_id, :null => false + # td.integer :category_id, :null => false + # end - class Milestone < ActiveRecord::Base - belongs_to :project - end + *Rafael Mendonça França* - Earlier, assignments would work like following regardless of which way the assignment told the best story: +* The primary key is always initialized in the @attributes hash to nil (unless + another value has been specified). - deadline.project_id = active_record.id +* In previous releases, the following would generate a single query with + an `OUTER JOIN comments`, rather than two separate queries: - Now you can do it either from the belonging side: + Post.includes(:comments) + .where("comments.name = 'foo'") - deadline.project = active_record + This behaviour relies on matching SQL string, which is an inherently + flawed idea unless we write an SQL parser, which we do not wish to + do. - ...or from the having side: + Therefore, it is now deprecated. - active_record.milestones << deadline + To avoid deprecation warnings and for future compatibility, you must + explicitly state which tables you reference, when using SQL snippets: - The milestone is automatically saved with the new foreign key. + Post.includes(:comments) + .where("comments.name = 'foo'") + .references(:comments) -* 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: + Note that you do not need to explicitly specify references in the + following cases, as they can be automatically inferred: - user = User.find(1) - user.preferences = { "background" => "black", "display" => large } - user.save + Post.where(comments: { name: 'foo' }) + Post.where('comments.name' => 'foo') + Post.order('comments.name') - User.find(1).preferences # => { "background" => "black", "display" => large } + You also do not need to worry about this unless you are doing eager + loading. Basically, don't worry unless you see a deprecation warning + or (in future releases) an SQL error due to a missing JOIN. - 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. + [Jon Leighton] -* 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: +* Support for the `schema_info` table has been dropped. Please + switch to `schema_migrations`. - class Song < ActiveRecord::Base - # Uses an integer of seconds to hold the length of the song +* Connections *must* be closed at the end of a thread. If not, your + connection pool can fill and an exception will be raised. - def length=(minutes) - write_attribute("length", minutes * 60) - end +* Added the `ActiveRecord::Model` module which can be included in a + class as an alternative to inheriting from `ActiveRecord::Base`: - def length - read_attribute("length") / 60 - end + class Post + include ActiveRecord::Model 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* + Please note: - User.find "jdoe" - Product.find "PDKEY-INT-12" + * Up until now it has been safe to assume that all AR models are + descendants of `ActiveRecord::Base`. This is no longer a safe + assumption, but it may transpire that there are areas of the + code which still make this assumption. So there may be + 'teething difficulties' with this feature. (But please do try it + and report bugs.) -* 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". + * Plugins & libraries etc that add methods to `ActiveRecord::Base` + will not be compatible with `ActiveRecord::Model`. Those libraries + should add to `ActiveRecord::Model` instead (which is included in + `Base`), or better still, avoid monkey-patching AR and instead + provide a module that users can include where they need it. -* Added an overwriteable primary_key method that'll instruct AR to the name of the - id column *Aredridele/earlier work by Guan Yang* + * To minimise the risk of conflicts with other code, it is + advisable to include `ActiveRecord::Model` early in your class + definition. - class Project < ActiveRecord::Base - def self.primary_key() "project_id" end - end + *Jon Leighton* -* Fixed that Active Records can safely associate inside and out of modules. +* PostgreSQL hstore records can be created. - 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 +* PostgreSQL hstore types are automatically deserialized from the database. -* Fixed that Active Records can safely live inside modules *Aredridel* +## Rails 3.2.8 (Aug 9, 2012) ## - class MyApplication::Account < ActiveRecord::Base - end +* Do not consider the numeric attribute as changed if the old value is zero and the new value + is not a string. + Fixes #7237. + *Rafael Mendonça França* - _Misc_ -* Added freeze call to value object assignments to ensure they remain immutable *Spotted by Gavin Sinclair* +* Removes the deprecation of `update_attribute`. *fxn* -* 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: +* Reverted the deprecation of `composed_of`. *Rafael Mendonça França* - class AuditObserver < ActiveRecord::Observer - def self.observed_class() Account end - def after_update(account) - AuditTrail.new(account, "UPDATED") - end - end +* Reverted the deprecation of `*_sql` association options. They will + be deprecated in 4.0 instead. *Jon Leighton* - *Suggested by Gavin Sinclair* +* 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 -* Create new Active Record objects by setting the attributes through a block. Like this: + *Xavier Noria* - person = Person.new do |p| - p.name = 'Freddy' - p.age = 19 - end +* 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. - *Suggested by Gavin Sinclair* + *Rafael Mendonça França* -## 0.8.2 ## +## Rails 3.2.7 (Jul 26, 2012) ## -* 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: +* `:finder_sql` and `:counter_sql` options on collection associations + are deprecated. Please transition to using scopes. - class Topic < ActiveRecord::Base - before_destroy :destroy_author, 'puts "I'm an inline fragment"' - end + *Jon Leighton* - Learn more in link:classes/ActiveRecord/Callbacks.html +* `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many` + associations are deprecated. Please transition to using `has_many + :through` -* Added :dependent option to has_many and has_one, which will automatically destroy associated objects when - the holder is destroyed: + *Jon Leighton* - class Album < ActiveRecord::Base - has_many :tracks, :dependent => true - end +* `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. - All the associated tracks are destroyed when the album is. + *Steve Klabnik* -* Added Base.create as a factory that'll create, save, and return a new object in one step. +* `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`. -* Automatically convert strings in config hashes to symbols for the _connection methods. This allows you - to pass the argument hashes directly from yaml. (Luke) + *Steve Klabnik* -* 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 +## Rails 3.2.6 (Jun 12, 2012) ## +* protect against the nesting of hashes changing the + table context in the next call to build_from_hash. This fix + covers this case as well. -## 0.8.1 ## + CVE-2012-2695 -* Added object-level transactions *Austin Ziegler* +* 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. -* 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) + *Jon Leighton* - 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* +## Rails 3.2.5 (Jun 1, 2012) ## -* Fixed SQLite adapter so dates are returned as Date objects, not Time objects *Spotted by Gavin Sinclair* +* Restore behavior of Active Record 3.2.3 scopes. + A series of commits relating to preloading and scopes caused a regression. -* Fixed requirement of date class, so date conversions are succesful regardless of whether you - manually require date or not. + *Andrew White* -## 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 30a66ff5f0..d080e0b0f5 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -61,10 +61,10 @@ A short rundown of some of the major features: * Validation rules that can differ for new or existing objects. class Account < ActiveRecord::Base - validates_presence_of :subdomain, :name, :email_address, :password - validates_uniqueness_of :subdomain - validates_acceptance_of :terms_of_service, :on => :create - validates_confirmation_of :password, :email_address, :on => :create + validates :subdomain, :name, :email_address, :password, presence: true + validates :subdomain, uniqueness: true + validates :terms_of_service, acceptance: true, on: :create + validates :password, :email_address, confirmation: true, on: :create end {Learn more}[link:classes/ActiveRecord/Validations.html] diff --git a/activerecord/RUNNING_UNIT_TESTS b/activerecord/RUNNING_UNIT_TESTS index 6a2e23b01f..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 build_postgresql and build_mysql 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,14 +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. -== Identity Map - -By default the tests run with the Identity Map turned off. But all tests should pass whether or -not the identity map is on or off. You can turn it on using the IM env variable: - - $ IM=true ruby -Itest test/case/base_test.rb - -== Config file +== Custom Config file By default, the config file is expected to be at the path test/config.yml. You can specify a custom location with the ARCONFIG environment variable. diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 4090293b56..a29d7b0e99 100755..100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rubygems/package_task' @@ -115,6 +114,16 @@ namespace :postgresql do config = ARTest.config['connections']['postgresql'] %x( createdb -E UTF8 #{config['arunit']['database']} ) %x( createdb -E UTF8 #{config['arunit2']['database']} ) + + # prepare hstore + version = %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") + %w(arunit arunit2).each do |db| + if version < "9.1.0" + puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html" + else + %x( psql #{config[db]['database']} -c "CREATE EXTENSION hstore;" ) + end + end end desc 'Drop the PostgreSQL test databases' diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 8f4c957dbd..53791d96ef 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -8,6 +8,7 @@ Gem::Specification.new do |s| s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.' s.required_ruby_version = '>= 1.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' @@ -21,5 +22,7 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) - s.add_dependency('arel', '~> 3.0.0') + s.add_dependency('arel', '~> 3.0.2') + + 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 73c8a06ab7..1675127ab0 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -22,15 +22,48 @@ #++ require 'active_support' -require 'active_support/i18n' +require 'active_support/rails' require 'active_model' require 'arel' +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' @@ -52,45 +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 :DynamicFinderMatch - autoload :DynamicScopeMatch - autoload :Explain - autoload :IdentityMap - 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 :Scoping - autoload :Serialization - autoload :SessionStore - autoload :Store - autoload :Timestamp - autoload :Transactions - autoload :Translation - autoload :Validations end module Coders @@ -139,12 +137,33 @@ module ActiveRecord end end + module Tasks + extend ActiveSupport::Autoload + + autoload :DatabaseTasks + autoload :SQLiteDatabaseTasks, 'active_record/tasks/sqlite_database_tasks' + autoload :MySQLDatabaseTasks, 'active_record/tasks/mysql_database_tasks' + autoload :PostgreSQLDatabaseTasks, + 'active_record/tasks/postgresql_database_tasks' + end + autoload :TestCase autoload :TestFixtures, 'active_record/fixtures' + + def self.eager_load! + super + ActiveRecord::Locking.eager_load! + ActiveRecord::Scoping.eager_load! + ActiveRecord::Associations.eager_load! + ActiveRecord::AttributeMethods.eager_load! + ActiveRecord::ConnectionAdapters.eager_load! + end end ActiveSupport.on_load(:active_record) do Arel::Table.engine = self end -I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml' +ActiveSupport.on_load(:i18n) do + I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml' +end diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 5a8addc4e4..3db8e0716b 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -10,9 +10,9 @@ module ActiveRecord # 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 + # 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 @@ -46,7 +46,7 @@ module ActiveRecord # # def <=>(other_money) # if currency == other_money.currency - # amount <=> amount + # amount <=> other_money.amount # else # amount <=> other_money.exchange_to(currency).amount # end @@ -71,7 +71,7 @@ module ActiveRecord # Now it's possible to access attributes from the database through the value objects instead. If # you choose to name the composition the same as the attribute's name, it will be the only way to # access that attribute. That's the case with our +balance+ attribute. You interact with the value - # objects just like you would any other attribute, though: + # 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 @@ -86,6 +86,12 @@ module ActiveRecord # 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" @@ -101,8 +107,8 @@ module ActiveRecord # ActiveRecord::Base classes are entity objects. # # It's also important to treat the value objects as immutable. Don't allow the Money object to have - # its amount changed after creation. Create a new Money object with the new value instead. This - # is exemplified by the Money#exchange_to method that returns a new value object instead of changing + # its 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. # @@ -119,7 +125,7 @@ module ActiveRecord # 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 + # 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. # @@ -187,7 +193,8 @@ module ActiveRecord # * <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>. + # 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) @@ -216,7 +223,7 @@ module ActiveRecord reader_method(name, class_name, mapping, allow_nil, constructor) writer_method(name, class_name, mapping, allow_nil, converter) - create_reflection(:composed_of, part_id, options, self) + create_reflection(:composed_of, part_id, nil, options, self) end private @@ -235,16 +242,15 @@ module ActiveRecord 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 - unless part.is_a?(class_name.constantize) || converter.nil? - part = converter.respond_to?(:call) ? - converter.call(part) : - class_name.constantize.send(converter, part) - end - mapping.each { |pair| self[pair.first] = part.send(pair.last) } @aggregation_cache[name] = part.freeze end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 958821add6..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 @@ -543,7 +555,7 @@ module ActiveRecord # end # # @group = Group.first - # @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group + # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group # @group.avatars # selects all avatars by going through the User join model. # # An important caveat with going through +has_one+ or +has_many+ associations on the @@ -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, for example, want to do a join but not include the joined columns. Do not forget - # to include the primary and foreign keys, otherwise it will raise an error. # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:through] @@ -1157,16 +1132,16 @@ 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] # Specifies the name of the <tt>belongs_to</tt> association on the associated object # that is the inverse of this <tt>has_many</tt> association. Does not work in combination @@ -1174,24 +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 @@ -1239,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, for example, - # you want to do a join but not include the joined columns. Do not forget to include the - # primary and foreign keys, otherwise it will raise an error. # [:through] # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>, # <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the @@ -1280,14 +1236,14 @@ module ActiveRecord # [:source_type] # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source # association is a polymorphic +belongs_to+. - # [:readonly] - # If true, the associated object is readonly through the association. # [:validate] # If +false+, don't validate the associated object when saving the parent object. +false+ by default. # [:autosave] # If true, always save the associated object or destroy it if marked for destruction, # when saving the parent object. If false, never save or destroy the associated object. # By default, only save the associated object if it's a new record. + # + # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] # Specifies the name of the <tt>belongs_to</tt> association on the associated object # that is the inverse of this <tt>has_one</tt> association. Does not work in combination @@ -1298,14 +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 @@ -1350,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, for example, you want to do a join but not include the joined columns. Do not - # forget to include the primary and foreign keys, otherwise it will raise an error. # [: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> @@ -1382,19 +1331,17 @@ module ActiveRecord # and +decrement_counter+. The counter cache is incremented when an object of this # class is created and decremented when it's destroyed. This requires that a column # named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class) - # is used on the associate class (such as a Post class). You can also specify a custom counter + # is used on the associate class (such as a Post class) - that is the migration for + # <tt>#{table_name}_count</tt> is created on the associate class (such that Post.comments_count will + # return the count cached, see note below). You can also specify a custom counter # cache column by providing a column name instead of a +true+/+false+ value to this # 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] @@ -1402,6 +1349,8 @@ module ActiveRecord # saving the parent object. # If false, never save or destroy the associated object. # By default, only save the associated object if it's a new record. + # + # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [:touch] # If true, the associated object will be touched (the updated_at/on attributes set to now) # when this record is either saved or destroyed. If you specify a symbol, that attribute @@ -1413,24 +1362,24 @@ 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 # intermediate join table. Unless the join table is explicitly specified as an option, it is # guessed using the lexical order of the class names. So a join between Developer and Project - # will give the default join table name of "developers_projects" because "D" outranks "P". + # will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically. # Note that this precedence is calculated using the <tt><</tt> operator for String. This # means that if the strings are of different lengths, and the strings are equal when compared # up to the shortest length, then the longer string is considered of higher @@ -1512,8 +1461,8 @@ module ActiveRecord # * <tt>Developer#projects.size</tt> # * <tt>Developer#projects.find(id)</tt> # * <tt>Developer#projects.exists?(...)</tt> - # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>) - # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>) + # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>) + # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>) # The declaration may include an options hash to specialize the behavior of the association. # # === Options @@ -1536,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, for example, - # you want to do a join but not include the joined columns. Do not forget to include the primary - # and foreign keys, otherwise it will raise an error. # [:readonly] # If true, all the associated objects are readonly through the association. # [:validate] @@ -1587,16 +1495,16 @@ module ActiveRecord # If false, never save or destroy the associated objects. # By default, only save associated objects that are new records. # + # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. + # # Option examples: # has_and_belongs_to_many :projects - # has_and_belongs_to_many :projects, :include => [ :milestones, :manager ] - # has_and_belongs_to_many :nations, :class_name => "Country" - # has_and_belongs_to_many :categories, :join_table => "prods_cats" - # has_and_belongs_to_many :categories, :readonly => true - # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => - # "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/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 0248c7483c..84540a7000 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -5,12 +5,13 @@ module ActiveRecord # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and # ActiveRecord::Associations::ThroughAssociationScope class AliasTracker # :nodoc: - attr_reader :aliases, :table_joins + attr_reader :aliases, :table_joins, :connection # table_joins is an array of arel joins which might conflict with the aliases we assign here - def initialize(table_joins = []) + def initialize(connection = ActiveRecord::Model.connection, table_joins = []) @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) } @table_joins = table_joins + @connection = connection end def aliased_table_for(table_name, aliased_name = nil) @@ -70,10 +71,6 @@ module ActiveRecord def truncate(name) name.slice(0, connection.table_alias_length - 2) end - - def connection - ActiveRecord::Base.connection - end end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 7887d59aad..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 @@ -25,9 +24,7 @@ module ActiveRecord def initialize(owner, reflection) reflection.check_validity! - @target = nil @owner, @reflection = owner, reflection - @updated = false reset reset_scope @@ -38,14 +35,14 @@ module ActiveRecord # post.comments.aliased_table_name # => "comments" # def aliased_table_name - reflection.klass.table_name + klass.table_name end # Resets the \loaded flag to +false+ and sets the \target to +nil+. def reset @loaded = false - IdentityMap.remove(target) if IdentityMap.enabled? && target @target = nil + @stale_state = nil end # Reloads the \target and returns +self+ on success. @@ -83,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 @@ -120,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. @@ -134,17 +136,8 @@ module ActiveRecord # ActiveRecord::RecordNotFound is rescued within the method, and it is # not reraised. The proxy is \reset and +nil+ is the return value. def load_target - if find_target? - begin - if IdentityMap.enabled? && association_class && association_class.respond_to?(:base_class) - @target = IdentityMap.get(association_class, owner[reflection.foreign_key]) - end - rescue NameError - nil - ensure - @target ||= find_target - end - end + @target = find_target if (@stale_state && stale_target?) || find_target? + loaded! unless loaded? target rescue ActiveRecord::RecordNotFound @@ -159,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? @@ -168,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] @@ -225,13 +233,10 @@ module ActiveRecord def stale_state end - def association_class - @reflection.klass - end - def build_record(attributes, options) reflection.build_association(attributes, options) do |record| - record.assign_attributes(create_scope.except(*record.changed), :without_protection => true) + attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) + record.assign_attributes(attributes, :without_protection => true) end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 0209ce36df..1303822868 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -6,32 +6,37 @@ 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 - @alias_tracker = AliasTracker.new + @alias_tracker = AliasTracker.new klass.connection end def scope scope = klass.unscoped - scope = 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 = scope.apply_finder_options(options.slice( - :readonly, :include, :references, :order, :limit, :joins, :group, :having, :offset, :select)) + scope.merge! eval_scope(klass, reflection.scope) if reflection.scope + add_constraints(scope) + end - if options[:through] && !options[:include] - scope = scope.includes(source_options[:include]) - end + private - scope = scope.uniq if options[:uniq] + def column_for(table_name, column_name) + columns = alias_tracker.connection.schema_cache.columns_hash[table_name] + columns[column_name] + end - add_constraints(scope) + def bind_value(scope, column, value) + substitute = alias_tracker.connection.substitute_at( + column, scope.bind_values.length) + scope.bind_values += [[column, value]] + substitute end - private + def bind(scope, table_name, column_name, value) + column = column_for table_name, column_name + bind_value scope, column, value + end def add_constraints(scope) tables = construct_tables @@ -64,21 +69,14 @@ module ActiveRecord foreign_key = reflection.active_record_primary_key end - conditions = self.conditions[i] - if reflection == chain.last - scope = scope.where(table[key].eq(owner[foreign_key])) + bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key] + scope = scope.where(table[key].eq(bind_val)) if reflection.type - scope = scope.where(table[reflection.type].eq(owner.class.base_class.name)) - end - - conditions.each do |condition| - if options[:through] && condition.is_a?(Hash) - condition = { table.name => condition } - end - - scope = scope.where(interpolate(condition)) + value = owner.class.base_class.name + bind_val = bind scope, table.table_name, reflection.type.to_s, value + scope = scope.where(table[reflection.type].eq(bind_val)) end else constraint = table[key].eq(foreign_table[foreign_key]) @@ -89,13 +87,15 @@ module ActiveRecord end scope = scope.joins(join(foreign_table, constraint)) + end - conditions.each do |condition| - condition = interpolate(condition) - condition = { (table.table_alias || table.name) => 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 @@ -117,6 +117,13 @@ module ActiveRecord end end + def eval_scope(klass, scope) + if scope.is_a?(Relation) + scope + else + klass.unscoped.instance_exec(owner, &scope) + end + end 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 97f531d064..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 @@ -14,6 +19,11 @@ module ActiveRecord self.target = record end + def reset + super + @updated = false + end + def updated? @updated end @@ -72,7 +82,7 @@ module ActiveRecord end def stale_state - owner[reflection.foreign_key].to_s + owner[reflection.foreign_key] && owner[reflection.foreign_key].to_s end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 2ee5dbbd70..88ce03a3cd 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -27,7 +27,8 @@ module ActiveRecord end def stale_state - [super, owner[reflection.foreign_type].to_s] + foreign_key = super + foreign_key && [foreign_key.to_s, owner[reflection.foreign_type].to_s] end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 2059d8acdf..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_many or has_one associations - if send(name).respond_to?(:exists?) ? send(name).exists? : !send(name).nil? - if dependent_restrict_raises? - raise ActiveRecord::DeleteRestrictionError.new(name) - else - key = association(name).reflection.macro == :has_one ? "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 0b634ab944..bdac02b5bf 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -1,57 +1,39 @@ module ActiveRecord::Associations::Builder class HasAndBelongsToMany < CollectionAssociation #:nodoc: - self.macro = :has_and_belongs_to_many + def macro + :has_and_belongs_to_many + end - self.valid_options += [:join_table, :association_foreign_key, :delete_sql, :insert_sql] + def valid_options + super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql] + end def build reflection = super - check_validity(reflection) define_destroy_hook reflection end - private + def show_deprecation_warnings + super - def define_destroy_hook - name = self.name - model.send(:include, Module.new { - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy_associations - association(#{name.to_sym.inspect}).delete_all_on_destroy - super - end - RUBY - }) - end - - # TODO: These checks should probably be moved into the Reflection, and we should not be - # redefining the options[:join_table] value - instead we should define a - # reflection.join_table method. - def check_validity(reflection) - if reflection.association_foreign_key == reflection.foreign_key - raise ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) + [:delete_sql, :insert_sql].each do |name| + if options.include? name + ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).") end - - reflection.options[:join_table] ||= join_table_name( - model.send(:undecorated_table_name, model.to_s), - model.send(:undecorated_table_name, reflection.class_name) - ) end + end - # Generates a join table name from two provided table names. - # The names in the join table names end up in lexicographic order. - # - # join_table_name("members", "clubs") # => "clubs_members" - # join_table_name("members", "special_clubs") # => "members_special_clubs" - def join_table_name(first_table_name, second_table_name) - if first_table_name < second_table_name - join_table = "#{first_table_name}_#{second_table_name}" - else - join_table = "#{second_table_name}_#{first_table_name}" - end - - model.table_name_prefix + join_table + model.table_name_suffix - end + def define_destroy_hook + name = self.name + model.send(:include, Module.new { + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def destroy_associations + association(:#{name}).delete_all + super + end + RUBY + }) + end end end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 9ddfd433e4..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_on_destroy - 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 b2136605e1..b15df4f308 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -6,6 +6,15 @@ module ActiveRecord # ease the implementation of association proxies that represent # collections. See the class hierarchy in AssociationProxy. # + # CollectionAssociation: + # HasAndBelongsToManyAssociation => has_and_belongs_to_many + # HasManyAssociation => has_many + # HasManyThroughAssociation + ThroughAssociation => has_many :through + # + # CollectionAssociation class provides common methods to the collections + # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with + # +:through association+ option. + # # You need to be careful with assumptions regarding the target: The proxy # does not fetch records from the database until it needs them, but new # ones created with +build+ are added to the target. So, the target may be @@ -16,12 +25,6 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: - attr_reader :proxy - - def initialize(owner, reflection) - super - @proxy = CollectionProxy.new(self) - end # Implements the reader method, e.g. foo.items for Foo.has_many :items def reader(force_reload = false) @@ -31,7 +34,7 @@ module ActiveRecord reload end - proxy + CollectionProxy.new(self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -47,18 +50,7 @@ module ActiveRecord end else column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - relation = scoped - - including = (relation.eager_load_values + relation.includes_values).uniq - - if including.any? - join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, []) - relation = join_dependency.join_associations.inject(relation) do |r, association| - association.join_relation(r) - end - end - - relation.pluck(column) + scope.pluck(column) end end @@ -71,7 +63,7 @@ module ActiveRecord end def reset - @loaded = false + super @target = [] end @@ -79,7 +71,7 @@ module ActiveRecord if block_given? load_target.select.each { |e| yield e } else - scoped.select(select) + scope.select(select) end end @@ -90,7 +82,7 @@ module ActiveRecord if options[:finder_sql] find_by_scan(*args) else - scoped.find(*args) + scope.find(*args) end end end @@ -121,8 +113,9 @@ module ActiveRecord create_record(attributes, options, true, &block) end - # Add +records+ to this association. Returns +self+ so method calls may be chained. - # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. + # Add +records+ to this association. Returns +self+ so method calls may + # be chained. Since << flattens its argument list and inserts each record, + # +push+ and +concat+ behave identically. def concat(*records) load_target if owner.new_record? @@ -148,23 +141,16 @@ module ActiveRecord end end - # Remove all records from this association + # Remove all records from this association. # # See delete for more info. def delete_all - delete(load_target).tap do + delete(:all).tap do reset loaded! end end - # Called when the association is declared as :dependent => :delete_all. This is - # an optimised version which avoids loading the records into memory. Not really - # for public consumption. - def delete_all_on_destroy - scoped.delete_all - end - # Destroy all the records from this association. # # See destroy for more info. @@ -175,12 +161,12 @@ module ActiveRecord end end - # Calculate sum using SQL, not Enumerable + # 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 @@ -197,13 +183,13 @@ module ActiveRecord reflection.klass.count_by_sql(custom_counter_sql) else - if options[:uniq] + if association_scope.uniq_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. column_name ||= reflection.klass.primary_key - count_options.merge!(:distinct => true) + count_options[:distinct] = true end - value = scoped.count(column_name, count_options) + value = scope.count(column_name, count_options) limit = options[:limit] offset = options[:offset] @@ -224,7 +210,18 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) - delete_or_destroy(records, options[:dependent]) + dependent = options[:dependent] + + if records.first == :all + if loaded? || dependent == :destroy + delete_or_destroy(load_target, dependent) + else + delete_records(:all, dependent) + end + else + records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } + delete_or_destroy(records, dependent) + end end # Destroy +records+ and remove them from this association calling @@ -248,11 +245,15 @@ module ActiveRecord # This method is abstract in the sense that it relies on # +count_records+, which is a method descendants have to provide. def size - if !find_target? || (loaded? && !options[:uniq]) - target.size - elsif !loaded? && options[:group] + if !find_target? || loaded? + if association_scope.uniq_value + target.uniq.size + else + target.size + end + 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 @@ -269,13 +270,24 @@ module ActiveRecord load_target.size end - # Equivalent to <tt>collection.size.zero?</tt>. If the collection has - # not been already loaded and you are going to fetch the records anyway - # it is better to check <tt>collection.length.zero?</tt>. + # 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. + # Equivalent to +!collection.empty?+. def any? if block_given? load_target.any? { |*block_args| yield(*block_args) } @@ -284,7 +296,8 @@ module ActiveRecord end end - # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1. + # Returns true if the collection has more than 1 record. + # Equivalent to +collection.size > 1+. def many? if block_given? load_target.many? { |*block_args| yield(*block_args) } @@ -293,15 +306,15 @@ 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 - # Replace this collection with +other_array+ - # This will perform a diff and delete/add only records that have changed. + # Replace this collection with +other_array+. This will perform a diff + # and delete/add only records that have changed. def replace(other_array) other_array.each { |val| raise_on_type_mismatch(val) } original_target = load_target.dup @@ -319,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 @@ -339,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 @@ -375,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 @@ -436,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) @@ -474,6 +486,8 @@ module ActiveRecord raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ "new records could not be saved." end + + target end def concat_records(records) @@ -544,7 +558,7 @@ module ActiveRecord # If using a custom finder_sql, #find scans the entire collection. def find_by_scan(*args) expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.uniq.map { |arg| arg.to_i } + ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq if ids.size == 1 id = ids.first @@ -559,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 5eda0387c4..ee8b816ef4 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -33,26 +33,812 @@ module ActiveRecord # # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. - class CollectionProxy # :nodoc: - alias :proxy_extend :extend + class CollectionProxy < Relation + def initialize(association) #:nodoc: + @association = association + super association.klass, association.klass.arel_table + merge! association.scope + end - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } + def target + @association.target + end - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, - :lock, :readonly, :having, :pluck, :to => :scoped + def load_target + @association.load_target + end - delegate :target, :load_target, :loaded?, :to => :@association + def loaded? + @association.loaded? + end - 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 + ## + # Works in two ways. + # + # *First:* Specify a subset of fields to be selected from the result set. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.select(:name) + # # => [ + # # #<Pet id: nil, name: "Fancy-Fancy">, + # # #<Pet id: nil, name: "Spook">, + # # #<Pet id: nil, name: "Choo-Choo"> + # # ] + # + # person.pets.select([:id, :name]) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy">, + # # #<Pet id: 2, name: "Spook">, + # # #<Pet id: 3, name: "Choo-Choo"> + # # ] + # + # Be careful because this also means you’re initializing a model + # object with only the fields that you’ve selected. If you attempt + # to access a field that is not in the initialized record you’ll + # receive: + # + # person.pets.select(:name).first.person_id + # # => ActiveModel::MissingAttributeError: missing attribute: person_id + # + # *Second:* You can pass a block so it can be used just like Array#select. + # This build an array of objects from the database for the scope, + # converting them into an array and iterating through them using + # Array#select. + # + # person.pets.select { |pet| pet.name =~ /oo/ } + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.select(:name) { |pet| pet.name =~ /oo/ } + # # => [ + # # #<Pet id: 2, name: "Spook">, + # # #<Pet id: 3, name: "Choo-Choo"> + # # ] + def select(select = nil, &block) + @association.select(select, &block) + end - def initialize(association) - @association = association - Array(association.options[:extend]).each { |ext| proxy_extend(ext) } + ## + # 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. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4 + # + # person.pets.find(2) { |pet| pet.name.downcase! } + # # => #<Pet id: 2, name: "fancy-fancy", person_id: 1> + # + # person.pets.find(2, 3) + # # => [ + # # #<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 + + ## + # 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. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.first # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.first(2) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.first # => nil + # another_person_without.pets.first(3) # => [] + def first(*args) + @association.first(*args) + end + + ## + # 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. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.last # => #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # + # person.pets.last(2) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.last # => nil + # another_person_without.pets.last(3) # => [] + def last(*args) + @association.last(*args) + end + + ## + # 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 + # with the new objects. + # + # class Person + # has_many :pets + # end + # + # person.pets.build + # # => #<Pet id: nil, name: nil, person_id: 1> + # + # person.pets.build(name: 'Fancy-Fancy') + # # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}]) + # # => [ + # # #<Pet id: nil, name: "Spook", person_id: 1>, + # # #<Pet id: nil, name: "Choo-Choo", person_id: 1>, + # # #<Pet id: nil, name: "Brain", person_id: 1> + # # ] + # + # 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 + + ## + # 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). + # + # class Person + # has_many :pets + # end + # + # person.pets.create(name: 'Fancy-Fancy') + # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.create([{name: 'Spook'}, {name: 'Choo-Choo'}]) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 3 + # person.pets.count # => 3 + # + # person.pets.find(1, 2, 3) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<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 + + ## + # Like +create+, except that if the record is invalid, raises an exception. + # + # class Person + # has_many :pets + # end + # + # class Pet + # attr_accessible :name + # validates :name, presence: true + # end + # + # 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 + + ## + # 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+ + # so method calls may be chained. + # + # class Person < ActiveRecord::Base + # pets :has_many + # end + # + # person.pets.size # => 0 + # person.pets.concat(Pet.new(name: 'Fancy-Fancy')) + # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')) + # person.pets.size # => 3 + # + # person.id # => 1 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')]) + # person.pets.size # => 5 + def concat(*records) + @association.concat(*records) + end + + ## + # Replace this collection with +other_array+. This will perform a diff + # and delete/add only records that have changed. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>] + # + # other_pets = [Pet.new(name: 'Puff', group: 'celebrities'] + # + # person.pets.replace(other_pets) + # + # person.pets + # # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>] + # + # If the supplied array has an incorrect association type, it raises + # an <tt>ActiveRecord::AssociationTypeMismatch</tt> error: + # + # person.pets.replace(["doo", "ggie", "gaga"]) + # # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String + def replace(other_array) + @association.replace(other_array) + end + + ## + # 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. + # + # If no <tt>:dependent</tt> option is given, then it will follow the + # default strategy. The default strategy is <tt>:nullify</tt>. This + # sets the foreign keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, + # the default strategy is +delete_all+. + # + # class Person < ActiveRecord::Base + # has_many :pets # dependent: :nullify option by default + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1, 2, 3) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>, + # # #<Pet id: 2, name: "Spook", person_id: nil>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: nil> + # # ] + # + # If it is set to <tt>:destroy</tt> all the objects from the collection + # are removed by calling their +destroy+ method. See +destroy+ for more + # information. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :destroy + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1, 2, 3) + # # => ActiveRecord::RecordNotFound + # + # If it is set to <tt>:delete_all</tt>, all the objects are deleted + # *without* calling their +destroy+ method. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :delete_all + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1, 2, 3) + # # => ActiveRecord::RecordNotFound + def delete_all + @association.delete_all + end + + ## + # Deletes the records of the collection directly from the database. + # This will _always_ remove the records ignoring the +:dependent+ + # option. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy_all + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1) # => Couldn't find Pet with id=1 + def destroy_all + @association.destroy_all + end + + ## + # 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 + # deleted records. + # + # If no <tt>:dependent</tt> option is given, then it will follow the default + # strategy. The default strategy is <tt>:nullify</tt>. This sets the foreign + # keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, the default + # strategy is +delete_all+. + # + # class Person < ActiveRecord::Base + # has_many :pets # dependent: :nullify option by default + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete(Pet.find(1)) + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1) + # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: nil> + # + # If it is set to <tt>:destroy</tt> all the +records+ are removed by calling + # their +destroy+ method. See +destroy+ for more information. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :destroy + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete(Pet.find(1), Pet.find(3)) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 1 + # person.pets + # # => [#<Pet id: 2, name: "Spook", person_id: 1>] + # + # Pet.find(1, 3) + # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3) + # + # If it is set to <tt>:delete_all</tt>, all the +records+ are deleted + # *without* calling their +destroy+ method. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :delete_all + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete(Pet.find(1)) + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1) + # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1 + # + # You can pass +Fixnum+ or +String+ values, it finds the records + # responding to the +id+ and executes delete on them. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete("1") + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.delete(2, 3) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + def delete(*records) + @association.delete(*records) + end + + ## + # 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. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy(Pet.find(1)) + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy(Pet.find(2), Pet.find(3)) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3) + # + # You can pass +Fixnum+ or +String+ values, it finds the records + # responding to the +id+ and then deletes them from the database. + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.destroy("4") + # # => #<Pet id: 4, name: "Benny", person_id: 1> + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.destroy(5, 6) + # # => [ + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # 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 + + ## + # Specifies whether the records should be unique or not. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.select(:name) + # # => [ + # # #<Pet name: "Fancy-Fancy">, + # # #<Pet name: "Fancy-Fancy"> + # # ] + # + # person.pets.select(:name).uniq + # # => [#<Pet name: "Fancy-Fancy">] + def uniq + @association.uniq + end + + ## + # Count all records using SQL. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<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 + + ## + # Returns the size of the collection. If the collection hasn't been loaded, + # it executes a <tt>SELECT COUNT(*)</tt> query. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1 + # + # person.pets # This will execute a SELECT * FROM query + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # 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 + + ## + # Returns the size of the collection calling +size+ on the target. + # If the collection has been already loaded, +length+ and +size+ are + # equivalent. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.length # => 3 + # # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1 + # + # # Because the collection is loaded, you can + # # call the collection with no additional queries: + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + def length + @association.length + end + + ## + # Returns +true+ if the collection is empty. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count # => 1 + # person.pets.empty? # => false + # + # person.pets.delete_all + # + # person.pets.count # => 0 + # person.pets.empty? # => true + def empty? + @association.empty? + end + + ## + # Returns +true+ if the collection is not empty. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count # => 0 + # person.pets.any? # => false + # + # person.pets << Pet.new(name: 'Snoop') + # person.pets.count # => 0 + # person.pets.any? # => true + # + # You can also pass a block to define criteria. The behaviour + # is the same, it returns true if the collection based on the + # criteria is not empty. + # + # person.pets + # # => [#<Pet name: "Snoop", group: "dogs">] + # + # person.pets.any? do |pet| + # pet.group == 'cats' + # end + # # => false + # + # person.pets.any? do |pet| + # pet.group == 'dogs' + # end + # # => true + def any?(&block) + @association.any?(&block) + end + + ## + # Returns true if the collection has more than one record. + # Equivalent to <tt>collection.size > 1</tt>. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count #=> 1 + # person.pets.many? #=> false + # + # person.pets << Pet.new(name: 'Snoopy') + # person.pets.count #=> 2 + # person.pets.many? #=> true + # + # You can also pass a block to define criteria. The + # behaviour is the same, it returns true if the collection + # based on the criteria has more than one record. + # + # person.pets + # # => [ + # # #<Pet name: "Gorby", group: "cats">, + # # #<Pet name: "Puff", group: "cats">, + # # #<Pet name: "Snoop", group: "dogs"> + # # ] + # + # person.pets.many? do |pet| + # pet.group == 'dogs' + # end + # # => false + # + # person.pets.many? do |pet| + # pet.group == 'cats' + # end + # # => true + def many?(&block) + @association.many?(&block) + end + + ## + # Returns +true+ if the given object is present in the collection. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets # => [#<Pet id: 20, name: "Snoop">] + # + # person.pets.include?(Pet.find(20)) # => true + # person.pets.include?(Pet.find(21)) # => false + def include?(record) + @association.include?(record) end alias_method :new, :build @@ -61,67 +847,143 @@ module ActiveRecord @association end - def scoped + # We don't want this object to be put on the scoping stack, because + # that could create an infinite loop where we call an @association + # method, which gets the current scope, which is this object, which + # delegates to @association, and so on. + def scoping + @association.scope.scoping { yield } + end + + # Returns a <tt>Relation</tt> object for the records in this association + def scope association = @association - association.scoped.extending do + + @association.scope.extending! do define_method(:proxy_association) { association } end end - def respond_to?(name, include_private = false) - super || - (load_target && target.respond_to?(name, include_private)) || - proxy_association.klass.respond_to?(name, include_private) - end - - def method_missing(method, *args, &block) - match = DynamicFinderMatch.match(method) - if match && match.instantiator? - send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r| - proxy_association.send :set_owner_attributes, r - proxy_association.send :add_to_target, r - yield(r) if block_given? - end - - elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method)) - if load_target - if target.respond_to?(method) - target.send(method, *args, &block) - else - begin - super - rescue NoMethodError => e - raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}") - end - end - end - - else - scoped.readonly(nil).send(method, *args, &block) - end - end + # :nodoc: + alias spawn scope - # Forwards <tt>===</tt> explicitly to the \target because the instance method - # removal above doesn't catch it. Loads the \target if needed. - def ===(other) - other === load_target + # 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 + # +false+. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1> + # # ] + # + # other = person.pets.to_ary + # + # person.pets == other + # # => true + # + # other = [Pet.new(id: 1), Pet.new(id: 2)] + # + # person.pets == other + # # => false + def ==(other) + load_target == other end + # Returns a new array of objects from the collection. If the collection + # hasn't been loaded, it fetches the records from the database. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # other_pets = person.pets.to_ary + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # other_pets.replace([Pet.new(name: 'BooGoo')]) + # + # other_pets + # # => [#<Pet id: nil, name: "BooGoo", person_id: 1>] + # + # person.pets + # # This is not affected by replace + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] def to_ary load_target.dup end alias_method :to_a, :to_ary + # Adds one or more +records+ to the collection by setting their foreign keys + # to the association‘s primary key. Returns +self+, so several appends may be + # chained together. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 0 + # person.pets << Pet.new(name: 'Fancy-Fancy') + # person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')] + # person.pets.size # => 3 + # + # person.id # => 1 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] def <<(*records) proxy_association.concat(records) && self end alias_method :push, :<< + # Equivalent to +delete_all+. The difference is that returns +self+, instead + # of an array with the deleted objects, so methods can be chained. See + # +delete_all+ for more information. def clear delete_all self end + # Reloads the collection from the database. Returns +self+. + # Equivalent to <tt>collection(true)</tt>. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets # uses the pets cache + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets.reload # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets(true) # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload proxy_association.reload self diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index a4cea99372..93618721bb 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -5,7 +5,7 @@ module ActiveRecord attr_reader :join_table def initialize(owner, reflection) - @join_table = Arel::Table.new(reflection.options[:join_table]) + @join_table = Arel::Table.new(reflection.join_table) super end @@ -32,10 +32,6 @@ module ActiveRecord record end - # ActiveRecord::Relation#delete_all needs to support joins before we can use a - # SQL-only implementation. - alias delete_all_on_destroy delete_all - private def count_records @@ -44,13 +40,20 @@ module ActiveRecord def delete_records(records, method) if sql = options[:delete_sql] + records = load_target if records == :all records.each { |record| owner.connection.delete(interpolate(sql, record)) } else - relation = join_table - stmt = relation.where(relation[reflection.foreign_key].eq(owner.id). - and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) - ).compile_delete - owner.connection.delete stmt + relation = join_table + condition = relation[reflection.foreign_key].eq(owner.id) + + unless records == :all + condition = condition.and( + relation[reflection.association_foreign_key] + .in(records.map { |x| x.id }.compact) + ) + end + + owner.connection.delete(relation.where(condition).compile_delete) end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 059e6c77bc..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) @@ -89,8 +111,12 @@ module ActiveRecord records.each { |r| r.destroy } update_counter(-records.length) unless inverse_updates_counter_cache? else - keys = records.map { |r| r[reflection.association_primary_key] } - scope = scoped.where(reflection.association_primary_key => keys) + if records == :all + scope = self.scope + else + keys = records.map { |r| r[reflection.association_primary_key] } + scope = self.scope.where(reflection.association_primary_key => keys) + end if method == :delete_all update_counter(-scope.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 9657cb081d..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 @@ -54,10 +53,6 @@ module ActiveRecord record end - # ActiveRecord::Relation#delete_all needs to support joins before we can use a - # SQL-only implementation. - alias delete_all_on_destroy delete_all - private def through_association @@ -73,7 +68,9 @@ module ActiveRecord # association def build_through_record(record) @through_records[record.object_id] ||= begin - through_record = through_association.build(construct_join_attributes(record)) + ensure_mutable + + through_record = through_association.build through_record.send("#{source_reflection.name}=", record) through_record end @@ -124,7 +121,12 @@ module ActiveRecord def delete_records(records, method) ensure_not_nested - scope = through_association.scoped.where(construct_join_attributes(*records)) + # This is unoptimised; it will load all the target records + # even when we just want to delete everything. + records = load_target if records == :all + + scope = through_association.scope + scope.where! construct_join_attributes(*records) case method when :destroy @@ -168,7 +170,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - scoped.all + scope.to_a end # NOTE - not sure that we can actually cope with inverses here diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 2131edbc20..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_attribute(reflection.foreign_key, nil) + target.update_columns(reflection.foreign_key => nil) end end end @@ -52,16 +71,19 @@ module ActiveRecord end def remove_target!(method) - if method.in?([:delete, :destroy]) - target.send(method) - else - nullify_owner_attributes(target) + case method + when :delete + target.delete + when :destroy + target.destroy + else + nullify_owner_attributes(target) - if target.persisted? && owner.persisted? && !target.save - set_owner_attributes(target) - raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + - "The record failed to save when after its foreign key was set to nil." - end + if target.persisted? && owner.persisted? && !target.save + set_owner_attributes(target) + raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + + "The record failed to save after its foreign key was set to nil." + end end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 827b01c5ac..cd366ac8b7 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -13,7 +13,7 @@ module ActiveRecord @join_parts = [JoinBase.new(base)] @associations = {} @reflections = [] - @alias_tracker = AliasTracker.new(joins) + @alias_tracker = AliasTracker.new(base.connection, joins) @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 build(associations) end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 0d7d28e458..0d3b4dbab1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -92,14 +92,21 @@ module ActiveRecord constraint = build_constraint(reflection, table, key, foreign_table, foreign_key) - conditions = self.conditions[i].dup - conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type + scope_chain_items = scope_chain[i] - conditions.each do |condition| - condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) - condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) + if reflection.type + scope_chain_items += [ + ActiveRecord::Relation.new(reflection.klass, table) + .where(reflection.type => foreign_klass.base_class.name) + ] + end + + scope_chain_items.each do |item| + unless item.is_a?(Relation) + item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item) + end - constraint = constraint.and(condition) + constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty? end relation.from(join(table, constraint)) @@ -137,18 +144,8 @@ module ActiveRecord table.table_alias || table.name end - def conditions - @conditions ||= reflection.conditions.reverse - end - - private - - def interpolate(conditions) - if conditions.respond_to?(:to_proc) - instance_eval(&conditions) - else - conditions - end + def scope_chain + @scope_chain ||= reflection.scope_chain.reverse end end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index cea6ad6944..5a41b40c8f 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -19,7 +19,7 @@ module ActiveRecord if reflection.source_macro == :has_and_belongs_to_many tables << alias_tracker.aliased_table_for( - (reflection.source_reflection || reflection).options[:join_table], + (reflection.source_reflection || reflection).join_table, table_alias_for(reflection, true) ) end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index fafed94ff2..ce5bf15f10 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -12,7 +12,7 @@ module ActiveRecord # and all of its books via a single query: # # SELECT * FROM authors - # LEFT OUTER JOIN books ON authors.id = books.id + # LEFT OUTER JOIN books ON authors.id = books.author_id # WHERE authors.name = 'Ken Akamatsu' # # However, this could result in many rows that contain redundant data. After @@ -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 253998fb23..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 @@ -77,7 +77,7 @@ module ActiveRecord # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) # Make several smaller queries if necessary or make one query if the adapter supports it sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size) - records = sliced.map { |slice| records_for(slice) }.flatten + records = sliced.map { |slice| records_for(slice).to_a }.flatten end # Each record may have multiple owners, and vice-versa @@ -92,33 +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.scoped + scope = klass.unscoped + scope.default_scoped = true - scope = scope.where(interpolate(options[:conditions])) - scope = scope.where(interpolate(preload_options[:conditions])) + values = reflection_scope.values + preload_values = preload_scope.values - scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star]) - scope = scope.includes(preload_options[:include] || options[:include]) + scope.where_values = Array(values[:where]) + Array(preload_values[:where]) + scope.references_values = Array(values[:references]) + Array(preload_values[:references]) + + scope.select! preload_values[:select] || values[:select] || table[Arel.star] + scope.includes! preload_values[:includes] || values[:includes] if options[:as] - scope = scope.where( - klass.table_name => { - reflection.type => model.base_class.sti_name - } - ) + scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end scope end - - def interpolate(conditions) - if conditions.respond_to?(:to_proc) - klass.send(:instance_eval, &conditions) - else - conditions - end - end end end end diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb index c248aeaaf6..e6cd35e7a1 100644 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -6,7 +6,7 @@ module ActiveRecord private def build_scope - super.order(preload_options[:order] || options[:order]) + super.order(preload_scope.values[:order] || reflection_scope.values[:order]) end def preload diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb index b77b667219..8e8925f0a9 100644 --- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb @@ -6,7 +6,7 @@ module ActiveRecord def initialize(klass, records, reflection, preload_options) super - @join_table = Arel::Table.new(options[:join_table]).alias('t0') + @join_table = Arel::Table.new(reflection.join_table).alias('t0') end # Unlike the other associations, we want to get a raw array of rows so that we can diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb index c6e9ede356..9a662d3f53 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -6,7 +6,7 @@ module ActiveRecord def associated_records_by_owner super.each do |owner, records| - records.uniq! if options[:uniq] + records.uniq! if reflection_scope.uniq_value end end end diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb index 848448bb48..24728e9f01 100644 --- a/activerecord/lib/active_record/associations/preloader/has_one.rb +++ b/activerecord/lib/active_record/associations/preloader/has_one.rb @@ -14,7 +14,7 @@ module ActiveRecord private def build_scope - super.order(preload_options[:order] || options[:order]) + super.order(preload_scope.values[:order] || reflection_scope.values[:order]) end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index ad6374d09a..1c1ba11c44 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -14,10 +14,7 @@ module ActiveRecord def associated_records_by_owner through_records = through_records_by_owner - ActiveRecord::Associations::Preloader.new( - through_records.values.flatten, - source_reflection.name, options - ).run + Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run through_records.each do |owner, records| records.map! { |r| r.send(source_reflection.name) }.flatten! @@ -28,10 +25,7 @@ module ActiveRecord private def through_records_by_owner - ActiveRecord::Associations::Preloader.new( - owners, through_reflection.name, - through_options - ).run + Preloader.new(owners, through_reflection.name, through_scope).run Hash[owners.map do |owner| through_records = Array.wrap(owner.send(through_reflection.name)) @@ -45,21 +39,22 @@ module ActiveRecord end] end - def through_options - through_options = {} + def through_scope + through_scope = through_reflection.klass.unscoped if options[:source_type] - through_options[:conditions] = { reflection.foreign_type => options[:source_type] } + through_scope.where! reflection.foreign_type => options[:source_type] else - if options[:conditions] - through_options[:include] = options[:include] || options[:source] - through_options[:conditions] = options[:conditions] + unless reflection_scope.where_values.empty? + through_scope.includes_values = reflection_scope.values[:includes] || options[:source] + through_scope.where_values = reflection_scope.values[:where] end - through_options[:order] = options[:order] + through_scope.order! reflection_scope.values[:order] + through_scope.references! reflection_scope.values[:references] end - through_options + through_scope end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index a1a921bcb4..b84cb4922d 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -35,11 +35,11 @@ module ActiveRecord private def create_scope - scoped.scope_for_create.stringify_keys.except(klass.primary_key) + scope.scope_for_create.stringify_keys.except(klass.primary_key) end def find_target - scoped.first.tap { |record| set_inverse_instance(record) } + scope.first.tap { |record| set_inverse_instance(record) } end # Implemented by subclasses diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f95e5337c2..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 @@ -37,9 +37,7 @@ module ActiveRecord # situation it is more natural for the user to just create or modify their join records # directly as required. def construct_join_attributes(*records) - if source_reflection.macro != :belongs_to - raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) - end + ensure_mutable join_attributes = { source_reflection.foreign_key => @@ -64,7 +62,7 @@ module ActiveRecord # properly support stale-checking for nested associations. def stale_state if through_reflection.macro == :belongs_to - owner[through_reflection.foreign_key].to_s + owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s end end @@ -73,6 +71,12 @@ module ActiveRecord !owner[through_reflection.foreign_key].nil? end + def ensure_mutable + if source_reflection.macro != :belongs_to + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + end + end + def ensure_not_nested if reflection.nested? raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index bf9fe70b31..d9989274c8 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -1,11 +1,24 @@ -require 'active_support/concern' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :whitelist_attributes, instance_accessor: false + mattr_accessor :mass_assignment_sanitizer, instance_accessor: false + end + module AttributeAssignment extend ActiveSupport::Concern include ActiveModel::MassAssignmentSecurity + included do + initialize_mass_assignment_sanitizer + end + module ClassMethods + def inherited(child) # :nodoc: + child.send :initialize_mass_assignment_sanitizer if self == Base + super + end + private # The primary key and inheritance column can never be set by mass-assignment for security reasons. @@ -14,6 +27,11 @@ module ActiveRecord default << 'id' unless primary_key.eql? 'id' default end + + def initialize_mass_assignment_sanitizer + attr_accessible(nil) if Model.whitelist_attributes + self.mass_assignment_sanitizer = Model.mass_assignment_sanitizer if Model.mass_assignment_sanitizer + end end # Allows you to set all the attributes at once by passing in a hash with keys @@ -64,12 +82,13 @@ module ActiveRecord # user.name # => "Josh" # user.is_admin? # => true def assign_attributes(new_attributes, options = {}) - return unless new_attributes + return if new_attributes.blank? - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] + attributes = new_attributes.stringify_keys + multi_parameter_attributes = [] nested_parameter_attributes = [] - @mass_assignment_options = options + previous_options = @mass_assignment_options + @mass_assignment_options = options unless options[:without_protection] attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) @@ -78,24 +97,17 @@ module ActiveRecord attributes.each do |k, v| if k.include?("(") multi_parameter_attributes << [ k, v ] - elsif respond_to?("#{k}=") - if v.is_a?(Hash) - nested_parameter_attributes << [ k, v ] - else - send("#{k}=", v) - end + elsif v.is_a?(Hash) + nested_parameter_attributes << [ k, v ] else - raise(UnknownAttributeError, "unknown attribute: #{k}") + _assign_attribute(k, v) end end - # assign any deferred nested attributes after the base attributes have been set - nested_parameter_attributes.each do |k,v| - send("#{k}=", v) - end - - @mass_assignment_options = nil - assign_multiparameter_attributes(multi_parameter_attributes) + assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? + assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? + ensure + @mass_assignment_options = previous_options end protected @@ -110,6 +122,21 @@ module ActiveRecord private + def _assign_attribute(k, v) + public_send("#{k}=", v) + rescue NoMethodError + if respond_to?("#{k}=") + raise + else + raise UnknownAttributeError, "unknown attribute: #{k}" + end + end + + # Assign any deferred nested attributes after the base attributes have been set. + def assign_nested_parameter_attributes(pairs) + pairs.each { |k, v| _assign_attribute(k, v) } + end + # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done # by calling new on the column type or aggregation type (through composed_of) object with these parameters. # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate @@ -123,84 +150,27 @@ module ActiveRecord ) end - def instantiate_time_object(name, values) - if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) - Time.zone.local(*values) - else - Time.time_with_datetime_fallback(self.class.default_timezone, *values) - end - end - def execute_callstack_for_multiparameter_attributes(callstack) errors = [] callstack.each do |name, values_with_empty_parameters| begin - send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) + send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value) rescue => ex - errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name) + errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end end unless errors.empty? - raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes" - end - end - - def read_value_from_parameter(name, values_hash_from_param) - klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass - if values_hash_from_param.values.all?{|v|v.nil?} - nil - elsif klass == Time - read_time_parameter_value(name, values_hash_from_param) - elsif klass == Date - read_date_parameter_value(name, values_hash_from_param) - else - read_other_parameter_value(klass, name, values_hash_from_param) - end - end - - def read_time_parameter_value(name, values_hash_from_param) - # If Date bits were not provided, error - raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)} - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) - # If Date bits were provided but blank, then return nil - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - - set_values = (1..max_position).collect{|position| values_hash_from_param[position] } - # If Time bits are not there, then default to 0 - (3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]} - instantiate_time_object(name, set_values) - end - - def read_date_parameter_value(name, values_hash_from_param) - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]] - begin - Date.new(*set_values) - rescue ArgumentError # if Date.new raises an exception on an invalid date - instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates + error_descriptions = errors.map { |ex| ex.message }.join(",") + raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]" end end - def read_other_parameter_value(klass, name, values_hash_from_param) - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) - values = (1..max_position).collect do |position| - raise "Missing Parameter" if !values_hash_from_param.has_key?(position) - values_hash_from_param[position] - end - klass.new(*values) - end - - def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) - [values_hash_from_param.keys.max,upper_cap].min - end - def extract_callstack_for_multiparameter_attributes(pairs) attributes = { } - pairs.each do |pair| - multiparameter_name, value = pair + pairs.each do |(multiparameter_name, value)| attribute_name = multiparameter_name.split("(").first - attributes[attribute_name] = {} unless attributes.include?(attribute_name) + attributes[attribute_name] ||= {} parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value @@ -217,5 +187,100 @@ module ActiveRecord multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i end + class MultiparameterAttribute #:nodoc: + attr_reader :object, :name, :values, :column + + def initialize(object, name, values) + @object = object + @name = name + @values = values + end + + def read_value + return if values.values.compact.empty? + + @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name) + klass = column.klass + + if klass == Time + read_time + elsif klass == Date + read_date + else + read_other(klass) + end + end + + private + + def instantiate_time_object(set_values) + if object.class.send(:create_time_zone_conversion_attribute?, name, column) + Time.zone.local(*set_values) + else + Time.time_with_datetime_fallback(object.class.default_timezone, *set_values) + end + end + + def read_time + # If column is a :time (and not :date or :timestamp) there is no need to validate if + # there are year/month/day fields + if column.type == :time + # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil + { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| + values[key] ||= value + end + else + # else column is a timestamp, so if Date bits were not provided, error + validate_missing_parameters!([1,2,3]) + + # If Date bits were provided but blank, then return nil + return if blank_date_parameter? + end + + max_position = extract_max_param(6) + set_values = values.values_at(*(1..max_position)) + # If Time bits are not there, then default to 0 + (3..5).each { |i| set_values[i] = set_values[i].presence || 0 } + instantiate_time_object(set_values) + end + + def read_date + return if blank_date_parameter? + set_values = values.values_at(1,2,3) + begin + Date.new(*set_values) + rescue ArgumentError # if Date.new raises an exception on an invalid date + instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates + end + end + + def read_other(klass) + max_position = extract_max_param + positions = (1..max_position) + validate_missing_parameters!(positions) + + set_values = values.values_at(*positions) + klass.new(*set_values) + end + + # Checks whether some blank date parameter exists. Note that this is different + # than the validate_missing_parameters! method, since it just checks for blank + # positions instead of missing ones, and does not raise in case one blank position + # exists. The caller is responsible to handle the case of this returning true. + def blank_date_parameter? + (1..3).any? { |position| values[position].blank? } + end + + # If some position is not provided, it errors out a missing parameter exception. + def validate_missing_parameters!(positions) + if missing_parameter = positions.detect { |position| !values.key?(position) } + raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})") + end + end + + def extract_max_param(upper_cap = 100) + [values.keys.max, upper_cap].min + end + end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 889c80386f..ced15bc330 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/enumerable' -require 'active_support/deprecation' module ActiveRecord # = Active Record Attribute Methods @@ -16,19 +15,6 @@ module ActiveRecord include TimeZoneConversion include Dirty include Serialization - - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, - # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). - # (Alias for the protected read_attribute method). - def [](attr_name) - read_attribute(attr_name) - end - - # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. - # (Alias for the protected write_attribute method). - def []=(attr_name, value) - write_attribute(attr_name, value) - end end module ClassMethods @@ -49,14 +35,6 @@ module ActiveRecord @attribute_methods_generated ||= false end - # We will define the methods as instance methods, but will call them as singleton - # methods. This allows us to use method_defined? to check if the method exists, - # which is fast and won't give any false positives from the ancestors (because - # there are no ancestors). - def generated_external_attribute_methods - @generated_external_attribute_methods ||= Module.new { extend self } - end - def undefine_attribute_methods super if attribute_methods_generated? @attribute_methods_generated = false @@ -157,7 +135,9 @@ module ActiveRecord # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. def attributes - Hash[@attributes.map { |name, _| [name, read_attribute(name)] }] + attribute_names.each_with_object({}) { |name, attrs| + attrs[name] = read_attribute(name) + } end # Returns an <tt>#inspect</tt>-like string for the value of the @@ -189,7 +169,7 @@ module ActiveRecord # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). def attribute_present?(attribute) value = read_attribute(attribute) - !value.nil? || (value.respond_to?(:empty?) && !value.empty?) + !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end # Returns the column object for the named attribute. @@ -198,6 +178,19 @@ module ActiveRecord self.class.columns_hash[name.to_s] end + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, + # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). + # (Alias for the protected read_attribute method). + def [](attr_name) + read_attribute(attr_name) + end + + # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. + # (Alias for the protected write_attribute method). + def []=(attr_name, value) + write_attribute(attr_name, value) + end + protected def clone_attributes(reader_method = :read_attribute, attributes = {}) @@ -214,37 +207,64 @@ module ActiveRecord value end - # Returns a copy of the attributes hash where all the values have been safely quoted for use in - # an Arel insert/update method. - def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) - attrs = {} - klass = self.class - arel_table = klass.arel_table + def arel_attributes_with_values_for_create(pk_attribute_allowed) + arel_attributes_with_values(attributes_for_create(pk_attribute_allowed)) + end - attribute_names.each do |name| - if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) + def arel_attributes_with_values_for_update(attribute_names) + arel_attributes_with_values(attributes_for_update(attribute_names)) + end - if include_readonly_attributes || !self.class.readonly_attributes.include?(name) + def attribute_method?(attr_name) + defined?(@attributes) && @attributes.include?(attr_name) + end - value = if klass.serialized_attributes.include?(name) - @attributes[name].serialized_value - else - # FIXME: we need @attributes to be used consistently. - # If the values stored in @attributes were already type - # casted, this code could be simplified - read_attribute(name) - end + private - attrs[arel_table[name]] = value - end - end - end + # Returns a Hash of the Arel::Attributes and attribute values that have been + # type casted for use in an Arel insert/update method. + def arel_attributes_with_values(attribute_names) + attrs = {} + arel_table = self.class.arel_table + attribute_names.each do |name| + attrs[arel_table[name]] = typecasted_attribute_value(name) + end attrs end - def attribute_method?(attr_name) - attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name)) + # Filters the primary keys and readonly attributes from the attribute names. + def attributes_for_update(attribute_names) + attribute_names.select do |name| + column_for_attribute(name) && !pk_attribute?(name) && !readonly_attribute?(name) + end + end + + # Filters out the primary keys, from the attribute names, when the primary + # key is to be generated (e.g. the id attribute has no value). + def attributes_for_create(pk_attribute_allowed) + @attributes.keys.select do |name| + column_for_attribute(name) && (pk_attribute_allowed || !pk_attribute?(name)) + end + end + + def readonly_attribute?(name) + self.class.readonly_attributes.include?(name) + end + + def pk_attribute?(name) + column_for_attribute(name).primary + end + + def typecasted_attribute_value(name) + if self.class.serialized_attributes.include?(name) + @attributes[name].serialized_value + else + # FIXME: we need @attributes to be used consistently. + # If the values stored in @attributes were already typecasted, this code + # could be simplified + read_attribute(name) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index bde11d0494..d4f529acbf 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -20,11 +20,7 @@ module ActiveRecord # Handle *_before_type_cast for method_missing. def attribute_before_type_cast(attribute_name) - if attribute_name == 'id' - read_attribute_before_type_cast(self.class.primary_key) - else - read_attribute_before_type_cast(attribute_name) - end + read_attribute_before_type_cast(attribute_name) end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 40e4a97e73..60e5b0e2bb 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,12 +1,16 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :partial_updates, instance_accessor: false + self.partial_updates = true + end + module AttributeMethods module Dirty extend ActiveSupport::Concern + include ActiveModel::Dirty - include AttributeMethods::Write included do if self < ::ActiveRecord::Timestamp @@ -14,7 +18,6 @@ module ActiveRecord end config_attribute :partial_updates - self.partial_updates = true end # Attempts to +save+ the record and clears changed attributes if successful. @@ -22,8 +25,6 @@ module ActiveRecord if status = super @previously_changed = changes @changed_attributes.clear - elsif IdentityMap.enabled? - IdentityMap.remove(self) end status end @@ -34,9 +35,6 @@ module ActiveRecord @previously_changed = changes @changed_attributes.clear end - rescue - IdentityMap.remove(self) if IdentityMap.enabled? - raise end # <tt>reload</tt> the record and clears changed attributes. @@ -55,12 +53,10 @@ module ActiveRecord # The attribute already has an unsaved change. if attribute_changed?(attr) old = @changed_attributes[attr] - @changed_attributes.delete(attr) unless field_changed?(attr, old, value) + @changed_attributes.delete(attr) unless _field_changed?(attr, old, value) else old = clone_attribute_value(:read_attribute, attr) - # Save Time objects as TimeWithZone if time_zone_aware_attributes == true - old = old.in_time_zone if clone_with_time_zone_conversion_attribute?(attr, old) - @changed_attributes[attr] = old if field_changed?(attr, old, value) + @changed_attributes[attr] = old if _field_changed?(attr, old, value) end # Carry on. @@ -77,13 +73,10 @@ module ActiveRecord end end - def field_changed?(attr, old, value) + def _field_changed?(attr, old, value) if column = column_for_attribute(attr) - if column.number? && column.null && (old.nil? || old == 0) && value.blank? - # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. - # Hence we don't record it as a change if the value changes from nil to ''. - # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll - # be typecast back to 0 (''.to_i => 0) + if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || + changes_from_zero_to_string?(old, value)) value = nil else value = column.type_cast(value) @@ -93,8 +86,17 @@ module ActiveRecord old != value end - def clone_with_time_zone_conversion_attribute?(attr, old) - old.class.name == "Time" && time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(attr.to_sym) + def changes_from_nil_to_empty_string?(column, old, value) + # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. + # Hence we don't record it as a change if the value changes from nil to ''. + # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll + # be typecast back to 0 (''.to_i => 0) + column.null && (old.nil? || old == 0) && value.blank? + end + + def changes_from_zero_to_string?(old, value) + # For columns with old 0 and value non-empty string + old == 0 && value.is_a?(String) && value.present? && value != '0' end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 7c59664703..7b7811a706 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -1,3 +1,5 @@ +require 'set' + module ActiveRecord module AttributeMethods module PrimaryKey @@ -24,23 +26,30 @@ module ActiveRecord query_attribute(self.class.primary_key) end + # Returns the primary key value before type cast + def id_before_type_cast + read_attribute_before_type_cast(self.class.primary_key) + end + + protected + + def attribute_method?(attr_name) + attr_name == 'id' || super + end + module ClassMethods def define_method_attribute(attr_name) super if attr_name == primary_key && attr_name != 'id' generated_attribute_methods.send(:alias_method, :id, primary_key) - generated_external_attribute_methods.module_eval <<-CODE, __FILE__, __LINE__ - def id(v, attributes, attributes_cache, attr_name) - attr_name = '#{primary_key}' - send(attr_name, attributes[attr_name], attributes, attributes_cache, attr_name) - end - CODE end end + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast).to_set + def dangerous_attribute_method?(method_name) - super && !['id', 'id=', 'id?'].include?(method_name) + super && !ID_ATTRIBUTE_METHODS.include?(method_name) end # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the @@ -96,9 +105,8 @@ module ActiveRecord # end # Project.primary_key # => "foo_id" def primary_key=(value) - @original_primary_key = @primary_key if defined?(@primary_key) - @primary_key = value && value.to_s - @quoted_primary_key = nil + @primary_key = value && value.to_s + @quoted_primary_key = nil 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 c129dc8c52..1a4cb25dd7 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,4 +1,8 @@ module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :attribute_types_cached_by_default, instance_accessor: false + end + module AttributeMethods module Read extend ActiveSupport::Concern @@ -6,8 +10,7 @@ module ActiveRecord ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] included do - config_attribute :attribute_types_cached_by_default, :global => true - self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT + config_attribute :attribute_types_cached_by_default end module ClassMethods @@ -29,35 +32,8 @@ module ActiveRecord cached_attributes.include?(attr_name) end - def undefine_attribute_methods - generated_external_attribute_methods.module_eval do - instance_methods.each { |m| undef_method(m) } - end - - super - end - - def type_cast_attribute(attr_name, attributes, cache = {}) #:nodoc: - return unless attr_name - attr_name = attr_name.to_s - - if generated_external_attribute_methods.method_defined?(attr_name) - if attributes.has_key?(attr_name) || attr_name == 'id' - generated_external_attribute_methods.send(attr_name, attributes[attr_name], attributes, cache, attr_name) - end - elsif !attribute_methods_generated? - # If we haven't generated the caster methods yet, do that and - # then try again - define_attribute_methods - type_cast_attribute(attr_name, attributes, cache) - else - # If we get here, the attribute has no associated DB column, so - # just return it verbatim. - attributes[attr_name] - end - end - protected + # We want to generate the methods via module_eval rather than define_method, # because define_method is slower on dispatch and uses more memory (because it # creates a closure). @@ -67,19 +43,9 @@ module ActiveRecord # we first define with the __temp__ identifier, and then use alias method to # rename it to what we want. def define_method_attribute(attr_name) - cast_code = attribute_cast_code(attr_name) - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def __temp__ - #{internal_attribute_access_code(attr_name, cast_code)} - end - alias_method '#{attr_name}', :__temp__ - undef_method :__temp__ - STR - - generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__(v, attributes, attributes_cache, attr_name) - #{external_attribute_access_code(attr_name, cast_code)} + read_attribute(:'#{attr_name}') { |n| missing_attribute(n, caller) } end alias_method '#{attr_name}', :__temp__ undef_method :__temp__ @@ -87,47 +53,54 @@ module ActiveRecord end private - def cacheable_column?(column) - attribute_types_cached_by_default.include?(column.type) - end - - def internal_attribute_access_code(attr_name, cast_code) - access_code = "v = @attributes.fetch(attr_name) { missing_attribute(attr_name, caller) };" - - access_code << "v && #{cast_code};" - if cache_attribute?(attr_name) - access_code = "@attributes_cache[attr_name] ||= (#{access_code})" + def cacheable_column?(column) + if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT + ! serialized_attributes.include? column.name + else + attribute_types_cached_by_default.include?(column.type) end - - "attr_name = '#{attr_name}'; #{access_code}" - end - - def external_attribute_access_code(attr_name, cast_code) - access_code = "v && #{cast_code}" - - if cache_attribute?(attr_name) - access_code = "attributes_cache[attr_name] ||= (#{access_code})" - end - - access_code - end - - def attribute_cast_code(attr_name) - columns_hash[attr_name].type_cast_code('v') 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) - self.class.type_cast_attribute(attr_name, @attributes, @attributes_cache) + return unless attr_name + name_sym = attr_name.to_sym + + # If it's cached, just return it + # 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_sym == :id && self.class.primary_key != name + read_attribute(self.class.primary_key) + end + } + } + + value = @attributes.fetch(name) { + return block_given? ? yield(name) : nil + } + + if self.class.cache_attribute?(name) + @attributes_cache[name_sym] = column.type_cast(value) + else + column.type_cast value + end + } end private - def attribute(attribute_name) - read_attribute(attribute_name) - end + + def attribute(attribute_name) + read_attribute(attribute_name) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 7efef73472..bdda5bc009 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -6,30 +6,10 @@ module ActiveRecord included do # Returns a hash of all the attributes that have been specified for serialization as # keys and their class restriction as values. - config_attribute :serialized_attributes + class_attribute :serialized_attributes, instance_accessor: false self.serialized_attributes = {} end - class Attribute < Struct.new(:coder, :value, :state) - def unserialized_value - state == :serialized ? unserialize : value - end - - def serialized_value - state == :unserialized ? serialize : value - end - - def unserialize - self.state = :unserialized - self.value = coder.load(value) - end - - def serialize - self.state = :serialized - self.value = coder.dump(value) - 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. @@ -47,6 +27,8 @@ module ActiveRecord # 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 @@ -57,43 +39,91 @@ module ActiveRecord # 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 + end + + def type_cast(value) + value.unserialized_value + end + + def type + @column.type + end + end + + class Attribute < Struct.new(:coder, :value, :state) + def unserialized_value + state == :serialized ? unserialize : value + end + + def serialized_value + state == :unserialized ? serialize : value + end + + def unserialize + self.state = :unserialized + self.value = coder.load(value) + end + + def serialize + self.state = :serialized + self.value = coder.dump(value) + end + 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 - def initialize_attributes(attributes) #:nodoc: - super + 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 2f86e32f41..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,61 +1,92 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/inclusion' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :time_zone_aware_attributes, instance_accessor: false + self.time_zone_aware_attributes = false + + mattr_accessor :skip_time_zone_conversion_for_attributes, instance_accessor: false + self.skip_time_zone_conversion_for_attributes = [] + end + module AttributeMethods module TimeZoneConversion + class Type # :nodoc: + def initialize(column) + @column = column + end + + def type_cast(value) + value = @column.type_cast(value) + value.acts_like?(:time) ? value.in_time_zone : value + end + + def type + @column.type + end + end + extend ActiveSupport::Concern included do - config_attribute :time_zone_aware_attributes, :global => true - self.time_zone_aware_attributes = false - + config_attribute :time_zone_aware_attributes, global: true config_attribute :skip_time_zone_conversion_for_attributes - self.skip_time_zone_conversion_for_attributes = [] end module ClassMethods protected - # The enhanced read method automatically converts the UTC time stored in the database to the time - # zone stored in Time.zone. - def attribute_cast_code(attr_name) - column = columns_hash[attr_name] - - if create_time_zone_conversion_attribute?(attr_name, column) - typecast = "v = #{super}" - time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v" - - "((#{typecast}) && (#{time_zone_conversion}))" - else - super - end + # The enhanced read method automatically converts the UTC time stored in the database to the time + # zone stored in Time.zone. + def attribute_cast_code(attr_name) + column = columns_hash[attr_name] + + if create_time_zone_conversion_attribute?(attr_name, column) + typecast = "v = #{super}" + time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v" + + "((#{typecast}) && (#{time_zone_conversion}))" + else + super end + end - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. - def define_method_attribute=(attr_name) - if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) - method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}=(original_time) - time = original_time - unless time.acts_like?(:time) - time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time - end - time = time.in_time_zone rescue nil if time - write_attribute(:#{attr_name}, original_time) - @attributes_cache["#{attr_name}"] = time + # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. + # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. + def define_method_attribute=(attr_name) + if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) + method_body, line = <<-EOV, __LINE__ + 1 + def #{attr_name}=(original_time) + time = original_time + unless time.acts_like?(:time) + time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time + end + 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 - EOV - generated_attribute_methods.module_eval(method_body, __FILE__, line) - else - super - end + end + EOV + generated_attribute_methods.module_eval(method_body, __FILE__, line) + else + super end + end private - def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && column.type.in?([:datetime, :timestamp]) - end + def create_time_zone_conversion_attribute?(name, column) + time_zone_aware_attributes && + !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && + [:datetime, :timestamp].include?(column.type) + end + end + + private + def round_usec(value) + return unless value + value.change(:usec => 0) end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 6b0230384b..5a39cb0125 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -25,9 +25,15 @@ 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.to_sym] = value + end + if column || @attributes.has_key?(attr_name) @attributes[attr_name] = type_cast_attribute_for_write(column, value) else diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index d468663084..290f57659d 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -28,7 +28,7 @@ module ActiveRecord # Association with autosave option defines several callbacks on your # model (before_save, after_create, after_update). Please note that # callbacks are executed in the order they were defined in - # model. You should avoid modyfing the association content, before + # model. You should avoid modifying the association content, before # autosave callbacks are executed. Placing your callbacks after # associations is usually a good practice. # @@ -78,7 +78,7 @@ module ActiveRecord # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved: # # class Post - # has_many :comments # :autosave option is no declared + # has_many :comments # :autosave option is not declared # end # # post = Post.new(:title => 'ruby rocks') @@ -93,7 +93,8 @@ module ActiveRecord # post.comments.create(:body => 'hello world') # post.save # => saves both post and comment # - # When <tt>:autosave</tt> is true all children is saved, no matter whether they are new records: + # When <tt>:autosave</tt> is true all children are saved, no matter whether they + # are new records or not: # # class Post # has_many :comments, :autosave => true @@ -126,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 @@ -190,23 +185,21 @@ module ActiveRecord # Doesn't use after_save as that would save associations added in after_create/after_update twice after_create save_method after_update save_method + elsif reflection.macro == :has_one + define_method(save_method) { save_has_one_association(reflection) } + # Configures two callbacks instead of a single after_save so that + # the model may rely on their execution order relative to its + # own callbacks. + # + # For example, given that after_creates run before after_saves, if + # we configured instead an after_save there would be no way to fire + # a custom after_create callback after the child association gets + # created. + after_create save_method + after_update save_method else - if reflection.macro == :has_one - define_method(save_method) { save_has_one_association(reflection) } - # Configures two callbacks instead of a single after_save so that - # the model may rely on their execution order relative to its - # own callbacks. - # - # For example, given that after_creates run before after_saves, if - # we configured instead an after_save there would be no way to fire - # a custom after_create callback after the child association gets - # created. - after_create save_method - after_update save_method - else - define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) } - before_save save_method - end + define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) } + before_save save_method end end @@ -329,14 +322,14 @@ module ActiveRecord autosave = reflection.options[:autosave] if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) - begin + records_to_destroy = [] records.each do |record| next if record.destroyed? saved = true if autosave && record.marked_for_destruction? - association.proxy.destroy(record) + records_to_destroy << record elsif autosave != false && (@new_record_before_save || record.new_record?) if autosave saved = association.insert_record(record, false) @@ -349,11 +342,10 @@ module ActiveRecord raise ActiveRecord::Rollback unless saved end - rescue - records.each {|x| IdentityMap.remove(x) } if IdentityMap.enabled? - raise - end + records_to_destroy.each do |record| + association.destroy(record) + end end # reconstruct the scope now that we know the owner's id diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index d4d0220fb7..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' @@ -201,6 +197,9 @@ module ActiveRecord #:nodoc: # # Now 'Bob' exist and is an 'admin' # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true } # + # Adding an exclamation point (!) on to the end of <tt>find_or_create_by_</tt> will + # raise an <tt>ActiveRecord::RecordInvalid</tt> error if the new record is invalid. + # # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without # saving it first. Protected attributes won't be set unless they are given in a block. # @@ -301,21 +300,22 @@ module ActiveRecord #:nodoc: # (or a bad spelling of an existing one). # * AssociationTypeMismatch - The object assigned to the association wasn't of the type # specified in the association definition. - # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. - # * ConnectionNotEstablished+ - No connection has been established. Use <tt>establish_connection</tt> + # * AttributeAssignmentError - An error occurred while doing a mass assignment through the + # <tt>attributes=</tt> method. + # You can inspect the +attribute+ property of the exception object to determine which attribute + # triggered the error. + # * ConnectionNotEstablished - No connection has been established. Use <tt>establish_connection</tt> # before querying. - # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist - # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal - # nothing was found, please check its documentation for further details. - # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the # <tt>attributes=</tt> 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 - # <tt>attributes=</tt> method. - # You can inspect the +attribute+ property of the exception object to determine which attribute - # triggered the error. + # * RecordInvalid - raised by save! and create! when the record is invalid. + # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist + # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal + # nothing was found, please check its documentation for further details. + # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. + # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. # # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level). # So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all @@ -325,4 +325,4 @@ module ActiveRecord #:nodoc: end end -ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy) +ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy.new) diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index a050fabf35..111208d0b9 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -34,7 +34,7 @@ module ActiveRecord # Examples: # class CreditCard < ActiveRecord::Base # # Strip everything but digits, so the user can specify "555 234 34" or - # # "5552-3434" or both will mean "55523434" + # # "5552-3434" and both will mean "55523434" # before_validation(:on => :create) do # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number") # end @@ -231,30 +231,6 @@ module ActiveRecord # Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model. # module Callbacks - # We can't define callbacks directly on ActiveRecord::Model because - # it is a module. So we queue up the definitions and execute them - # when ActiveRecord::Model is included. - module Register #:nodoc: - def self.extended(base) - base.config_attribute :_callbacks_register - base._callbacks_register = [] - end - - def self.setup(base) - base._callbacks_register.each do |item| - base.send(*item) - end - end - - def define_callbacks(*args) - self._callbacks_register << [:define_callbacks, *args] - end - - def define_model_callbacks(*args) - self._callbacks_register << [:define_model_callbacks, *args] - end - end - extend ActiveSupport::Concern CALLBACKS = [ diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 77af540c3e..f17e7158de 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -1,12 +1,10 @@ +require 'yaml' + module ActiveRecord # :stopdoc: module Coders class YAMLColumn - RESCUE_ERRORS = [ ArgumentError ] - - if defined?(Psych) && defined?(Psych::SyntaxError) - RESCUE_ERRORS << Psych::SyntaxError - end + RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] attr_accessor :object_class @@ -15,7 +13,13 @@ module ActiveRecord end def dump(obj) - YAML.dump(obj) unless obj.nil? + return if obj.nil? + + unless obj.is_a?(object_class) + raise SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" + end + YAML.dump obj end def load(yaml) 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 d69f02d504..7a3d9bfd3e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -11,9 +11,6 @@ module ActiveRecord # Raised when a connection pool is full and another connection is requested class PoolFullError < ConnectionNotEstablished - def initialize size, timeout - super("Connection pool of size #{size} and timeout #{timeout}s is full") - end end module ConnectionAdapters @@ -57,13 +54,146 @@ module ActiveRecord # # == Options # - # There are two connection-pooling-related options that you can add to + # There are several connection-pooling-related options that you can add to # your database connection configuration: # # * +pool+: number indicating size of connection pool (default 5) - # * +wait_timeout+: number of seconds to block and wait for a connection + # * +checkout_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). + # * +reaping_frequency+: frequency in seconds to periodically run the + # Reaper, which attempts to find and close dead connections, which can + # occur if a programmer forgets to close a connection at the end of a + # thread or a thread dies unexpectedly. (Default nil, which means don't + # run the Reaper). + # * +dead_connection_timeout+: number of seconds from last checkout + # after which the Reaper will consider a connection reapable. (default + # 5 seconds). class ConnectionPool + # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool + # with which it shares a Monitor. But could be a generic Queue. + # + # The Queue in stdlib's 'thread' could replace this class except + # stdlib's doesn't support waiting with a timeout. + class Queue + def initialize(lock = Monitor.new) + @lock = lock + @cond = @lock.new_cond + @num_waiting = 0 + @queue = [] + end + + # Test if any threads are currently waiting on the queue. + def any_waiting? + synchronize do + @num_waiting > 0 + end + end + + # Return the number of threads currently waiting on this + # queue. + def num_waiting + synchronize do + @num_waiting + end + end + + # Add +element+ to the queue. Never blocks. + def add(element) + synchronize do + @queue.push element + @cond.signal + end + end + + # If +element+ is in the queue, remove and return it, or nil. + def delete(element) + synchronize do + @queue.delete(element) + end + end + + # Remove all elements from the queue. + def clear + synchronize do + @queue.clear + end + end + + # Remove the head of the queue. + # + # If +timeout+ is not given, remove and return the head the + # queue if the number of available elements is strictly + # greater than the number of threads currently waiting (that + # is, don't jump ahead in line). Otherwise, return nil. + # + # If +timeout+ is given, block if it there is no element + # available, waiting up to +timeout+ seconds for an element to + # become available. + # + # Raises: + # - ConnectionTimeoutError if +timeout+ is given and no element + # becomes available after +timeout+ seconds, + def poll(timeout = nil) + synchronize do + if timeout + no_wait_poll || wait_poll(timeout) + else + no_wait_poll + end + end + end + + private + + def synchronize(&block) + @lock.synchronize(&block) + end + + # Test if the queue currently contains any elements. + def any? + !@queue.empty? + end + + # A thread can remove an element from the queue without + # waiting if an only if the number of currently available + # connections is strictly greater than the number of waiting + # threads. + def can_remove_no_wait? + @queue.size > @num_waiting + end + + # Removes and returns the head of the queue if possible, or nil. + def remove + @queue.shift + end + + # Remove and return the head the queue if the number of + # available elements is strictly greater than the number of + # threads currently waiting. Otherwise, return nil. + def no_wait_poll + remove if can_remove_no_wait? + end + + # Waits on the queue up to +timeout+ seconds, then removes and + # returns the head of the queue. + def wait_poll(timeout) + @num_waiting += 1 + + t0 = Time.now + elapsed = 0 + loop do + @cond.wait(timeout - elapsed) + + return remove if any? + + elapsed = Time.now - t0 + raise ConnectionTimeoutError if elapsed >= timeout + end + ensure + @num_waiting -= 1 + end + end + # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. # A reaper instantiated with a nil frequency will never reap the # connection pool. @@ -91,7 +221,7 @@ module ActiveRecord include MonitorMixin - attr_accessor :automatic_reconnect, :timeout + attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout attr_reader :spec, :connections, :size, :reaper # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification @@ -108,7 +238,8 @@ module ActiveRecord # The cache of reserved connections mapped to threads @reserved_connections = {} - @timeout = spec.config[:wait_timeout] || 5 + @checkout_timeout = spec.config[:checkout_timeout] || 5 + @dead_connection_timeout = spec.config[:dead_connection_timeout] @reaper = Reaper.new self, spec.config[:reaping_frequency] @reaper.run @@ -117,6 +248,16 @@ module ActiveRecord @connections = [] @automatic_reconnect = true + + @available = Queue.new self + end + + # Hack for tests to be able to add connections. Do not call outside of tests + def insert_connection_for_test!(c) #:nodoc: + synchronize do + @connections << c + @available.add c + end end # Retrieve the connection associated with the current thread, or call @@ -125,21 +266,28 @@ module ActiveRecord # #connection can be called any number of times; the connection is # held in a hash keyed by the thread id. def connection - @reserved_connections[current_connection_id] ||= checkout + synchronize do + @reserved_connections[current_connection_id] ||= checkout + end end - # Check to see if there is an active connection in this connection - # pool. + # Is there an open connection that is being used for the current thread? def active_connection? - active_connections.any? + synchronize do + @reserved_connections.fetch(current_connection_id) { + return false + }.in_use? + end end # Signal that the thread is finished with the current connection. # #release_connection releases the connection-thread association # and returns the connection to the pool. def release_connection(with_id = current_connection_id) - conn = @reserved_connections.delete(with_id) - checkin conn if conn + synchronize do + conn = @reserved_connections.delete(with_id) + checkin conn if conn + end end # If a connection already exists yield it to the block. If no connection @@ -167,6 +315,7 @@ module ActiveRecord conn.disconnect! end @connections = [] + @available.clear end end @@ -181,22 +330,17 @@ module ActiveRecord @connections.delete_if do |conn| conn.requires_reloading? end - end - end - - # Verify active connections and remove and disconnect connections - # associated with stale threads. - def verify_active_connections! #:nodoc: - synchronize do - @connections.each do |connection| - connection.verify! + @available.clear + @connections.each do |conn| + @available.add conn end end end def clear_stale_cached_connections! # :nodoc: + reap end - deprecate :clear_stale_cached_connections! + deprecate :clear_stale_cached_connections! => "Please use #reap instead" # Check-out a database connection from the pool, indicating that you want # to use it. You should call #checkin when you no longer need this. @@ -213,23 +357,10 @@ module ActiveRecord # Raises: # - PoolFullError: no connection can be obtained from the pool. def checkout - # Checkout an available connection synchronize do - # Try to find a connection that hasn't been leased, and lease it - conn = connections.find { |c| c.lease } - - # If all connections were leased, and we have room to expand, - # create a new connection and lease it. - if !conn && connections.size < size - conn = checkout_new_connection - conn.lease - end - - if conn - checkout_and_verify conn - else - raise PoolFullError.new(size, timeout) - end + conn = acquire_connection + conn.lease + checkout_and_verify(conn) end end @@ -243,6 +374,10 @@ module ActiveRecord conn.run_callbacks :checkin do conn.expire end + + release conn + + @available.add conn end end @@ -251,13 +386,13 @@ module ActiveRecord def remove(conn) synchronize do @connections.delete conn + @available.delete conn # FIXME: we might want to store the key on the connection so that removing # from the reserved hash will be a little easier. - thread_id = @reserved_connections.keys.find { |k| - @reserved_connections[k] == conn - } - @reserved_connections.delete thread_id if thread_id + release conn + + @available.add checkout_new_connection if @available.any_waiting? end end @@ -266,7 +401,7 @@ module ActiveRecord # or a thread dies unexpectedly. def reap synchronize do - stale = Time.now - @timeout + stale = Time.now - @dead_connection_timeout connections.dup.each do |conn| remove conn if conn.in_use? && stale > conn.last_use && !conn.active? end @@ -275,12 +410,49 @@ module ActiveRecord private + # Acquire a connection by one of 1) immediately removing one + # from the queue of available connections, 2) creating a new + # connection if the pool is not at capacity, 3) waiting on the + # queue for a connection to become available. + # + # Raises: + # - PoolFullError if a connection could not be acquired (FIXME: + # why not ConnectionTimeoutError? + def acquire_connection + if conn = @available.poll + conn + elsif @connections.size < @size + checkout_new_connection + else + t0 = Time.now + begin + @available.poll(@checkout_timeout) + rescue ConnectionTimeoutError + msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % + [@checkout_timeout, Time.now - t0] + raise PoolFullError, msg + end + end + end + + def release(conn) + thread_id = if @reserved_connections[current_connection_id] == conn + current_connection_id + else + @reserved_connections.keys.find { |k| + @reserved_connections[k] == conn + } + end + + @reserved_connections.delete thread_id if thread_id + end + def new_connection - ActiveRecord::Base.send(spec.adapter_method, spec.config) + ActiveRecord::Model.send(spec.adapter_method, spec.config) end def current_connection_id #:nodoc: - ActiveRecord::Base.connection_id ||= Thread.current.object_id + ActiveRecord::Model.connection_id ||= Thread.current.object_id end def checkout_new_connection @@ -298,10 +470,6 @@ module ActiveRecord end c end - - def active_connections - @connections.find_all { |c| c.in_use? } - end end # ConnectionHandler is a collection of ConnectionPool objects. It is used @@ -328,43 +496,40 @@ module ActiveRecord # ActiveRecord::Base.connection_handler. Active Record models use this to # determine that connection pool that they should use. class ConnectionHandler - attr_reader :connection_pools + def initialize + @owner_to_pool = Hash.new { |h,k| h[k] = {} } + @class_to_pool = Hash.new { |h,k| h[k] = {} } + end - def initialize(pools = {}) - @connection_pools = pools - @class_to_pool = {} + def connection_pools + owner_to_pool.values.compact end - def establish_connection(name, spec) - @connection_pools[spec] ||= ConnectionAdapters::ConnectionPool.new(spec) - @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! } - end - - # Verify active connections. - def verify_active_connections! #:nodoc: - @connection_pools.each_value {|pool| pool.verify_active_connections! } + connection_pools.each(&:disconnect!) end # Locate the connection of the nearest super class. This can be an @@ -387,21 +552,61 @@ 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) - pool = @class_to_pool[klass.name] - return pool if pool - return nil if ActiveRecord::Model == klass - retrieve_connection_pool klass.active_record_super + 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 owner_to_pool + @owner_to_pool[Process.pid] + end + + def class_to_pool + @class_to_pool[Process.pid] + end + + 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 + 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 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 eb8cff9610..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,10 +1,19 @@ 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) + def to_sql(arel, binds = []) if arel.respond_to?(:ast) - visitor.accept(arel.ast) + binds = binds.dup + visitor.accept(arel.ast) do + quote(*binds.shift.reverse) + end else arel end @@ -13,19 +22,19 @@ module ActiveRecord # Returns an array of record hashes with the column names as keys and # column values as values. def select_all(arel, name = nil, binds = []) - select(to_sql(arel), name, binds) + select(to_sql(arel, binds), name, binds) end # Returns a record hash with the column names as keys and column values # as values. - def select_one(arel, name = nil) - result = select_all(arel, name) + def select_one(arel, name = nil, binds = []) + result = select_all(arel, name, binds) result.first if result end # Returns a single value from a record - def select_value(arel, name = nil) - if result = select_one(arel, name) + def select_value(arel, name = nil, binds = []) + if result = select_one(arel, name, binds) result.values.first end end @@ -33,7 +42,7 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] def select_values(arel, name = nil) - result = select_rows(to_sql(arel), name) + result = select_rows(to_sql(arel, []), name) result.map { |v| v[0] } end @@ -55,21 +64,21 @@ module ActiveRecord end # Executes insert +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_insert(sql, name, binds) + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) exec_query(sql, name, binds) end # Executes delete +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_delete(sql, name, binds) exec_query(sql, name, binds) end # Executes update +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_update(sql, name, binds) exec_query(sql, name, binds) @@ -84,19 +93,19 @@ module ActiveRecord # If the next id was calculated in advance (as in Oracle), it should be # passed in as +id_value+. def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) - sql, binds = sql_for_insert(to_sql(arel), pk, id_value, sequence_name, binds) - value = exec_insert(sql, name, binds) + sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) + value = exec_insert(sql, name, binds, pk, sequence_name) id_value || last_inserted_id(value) end # Executes the update statement and returns the number of rows affected. def update(arel, name = nil, binds = []) - exec_update(to_sql(arel), name, binds) + exec_update(to_sql(arel, binds), name, binds) end # Executes the delete statement and returns the number of rows affected. def delete(arel, name = nil, binds = []) - exec_delete(to_sql(arel), name, binds) + exec_delete(to_sql(arel, binds), name, binds) end # Checks whether there is currently no transaction active. This is done @@ -164,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 @@ -222,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) @@ -310,13 +311,27 @@ module ActiveRecord # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in # an UPDATE statement, so in the mysql adapters we redefine this to do that. def join_to_update(update, select) #:nodoc: - subselect = select.clone - subselect.projections = [update.key] + key = update.key + subselect = subquery_for(key, select) + + update.where key.in(subselect) + end - update.where update.key.in(subselect) + def join_to_delete(delete, select, key) #:nodoc: + subselect = subquery_for(key, select) + + delete.where key.in(subselect) end protected + + # Return a subquery for the given key using the join information. + def subquery_for(key, select) + subselect = select.clone + subselect.projections = [key] + subselect + end + # Returns an array of record hashes with the column names as keys and # column values as values. def select(sql, name = nil, binds = []) @@ -353,7 +368,7 @@ module ActiveRecord records.uniq.each do |record| begin record.rolledback!(rollback) - rescue Exception => e + rescue => e record.logger.error(e) if record.respond_to?(:logger) && record.logger end end @@ -368,7 +383,7 @@ module ActiveRecord records.uniq.each do |record| begin record.committed! - rescue Exception => e + rescue => e record.logger.error(e) if record.respond_to?(:logger) && record.logger end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 6ba64bb88f..be6fda95b4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module QueryCache class << self - def included(base) + def included(base) #:nodoc: dirties_query_cache base, :insert, :update, :delete end @@ -56,8 +56,8 @@ module ActiveRecord end def select_all(arel, name = nil, binds = []) - if @query_cache_enabled - sql = to_sql(arel) + if @query_cache_enabled && !locked?(arel) + sql = to_sql(arel, binds) cache_sql(sql, binds) { super(sql, name, binds) } else super @@ -65,6 +65,7 @@ module ActiveRecord end private + def cache_sql(sql, binds) result = if @query_cache[sql].key?(binds) @@ -83,6 +84,10 @@ module ActiveRecord result.collect { |row| row.dup } end end + + def locked?(arel) + arel.respond_to?(:locked) && arel.locked + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 44ac37c498..60a9eee7c7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -31,9 +31,10 @@ module ActiveRecord # BigDecimals need to be put in a non-normalized form and quoted. when nil then "NULL" when BigDecimal then value.to_s('F') - when Numeric then value.to_s + when Numeric, ActiveSupport::Duration then value.to_s when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" + when Class then "'#{value.to_s}'" else "'#{quote_string(YAML.dump(value))}'" 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 132ca10f79..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' @@ -6,7 +5,10 @@ require 'bigdecimal/util' module ActiveRecord module ConnectionAdapters #:nodoc: - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders) #:nodoc: + # Abstract representation of an index definition on a table. Instances of + # this type are typically created and returned by methods in database + # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where) #:nodoc: end # Abstract representation of a column definition. Instances of this type @@ -20,7 +22,7 @@ module ActiveRecord end def sql_type - base.type_to_sql(type.to_sym, limit, precision, scale) rescue type + base.type_to_sql(type.to_sym, limit, precision, scale) end def to_sql @@ -62,11 +64,12 @@ module ActiveRecord class TableDefinition # An array of ColumnDefinition objects, representing the column changes # that have been defined. - attr_accessor :columns + attr_accessor :columns, :indexes def initialize(base) @columns = [] @columns_hash = {} + @indexes = {} @base = base end @@ -209,19 +212,22 @@ module ActiveRecord # # TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of - # options, these will be used when creating the <tt>_type</tt> column. So what can be written like this: + # options, these will be used when creating the <tt>_type</tt> column. The <tt>:index</tt> option + # will also create an index, similar to calling <tt>add_index</tt>. So what can be written like this: # # create_table :taggings do |t| # t.integer :tag_id, :tagger_id, :taggable_id # t.string :tagger_type # t.string :taggable_type, :default => 'Photo' # end + # add_index :taggings, :tag_id, :name => 'index_taggings_on_tag_id' + # add_index :taggings, [:tagger_id, :tagger_type] # # Can also be written as follows using references: # # create_table :taggings do |t| - # t.references :tag - # t.references :tagger, :polymorphic => true + # t.references :tag, :index => { :name => 'index_taggings_on_tag_id' } + # t.references :tagger, :polymorphic => true, :index => true # t.references :taggable, :polymorphic => { :default => 'Photo' } # end def column(name, type, options = {}) @@ -253,10 +259,18 @@ module ActiveRecord EOV end + # Adds index options to the indexes hash, keyed by column name + # This is primarily used to track indexes that need to be created after the table + # + # index(:account_id, :name => 'index_projects_on_account_id') + def index(column_name, options = {}) + indexes[column_name] = options + end + # 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 @@ -264,9 +278,11 @@ module ActiveRecord def references(*args) options = args.extract_options! polymorphic = options.delete(:polymorphic) + index_options = options.delete(:index) args.each do |col| column("#{col}_id", :integer, options) - column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil? + column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic + index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options end end alias :belongs_to :references @@ -331,7 +347,7 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. - # ===== Example + # # ====== Creating a simple column # t.column(:name, :string) def column(column_name, type, options = {}) @@ -346,7 +362,6 @@ module ActiveRecord # Adds a new index to the table. +column_name+ can be a single Symbol, or # an Array of Symbols. See SchemaStatements#add_index # - # ===== Examples # ====== Creating a simple index # t.index(:name) # ====== Creating a unique index @@ -363,7 +378,7 @@ module ActiveRecord end # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps - # ===== Example + # # t.timestamps def timestamps @base.add_timestamps(@table_name) @@ -371,7 +386,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # t.change(:name, :string, :limit => 80) # t.change(:description, :text) def change(column_name, type, options = {}) @@ -379,7 +394,7 @@ module ActiveRecord end # Sets a new default value for a column. See SchemaStatements#change_column_default - # ===== Examples + # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) def change_default(column_name, default) @@ -387,16 +402,15 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # t.remove(:qualification) # t.remove(:qualification, :experience) def remove(*column_names) - @base.remove_column(@table_name, column_names) + @base.remove_column(@table_name, *column_names) end # Removes the given index from the table. # - # ===== Examples # ====== Remove the index_table_name_on_column in the table_name table # t.remove_index :column # ====== Remove the index named index_table_name_on_branch_id in the table_name table @@ -410,14 +424,14 @@ module ActiveRecord end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. - # ===== Example + # # t.remove_timestamps def remove_timestamps @base.remove_timestamps(@table_name) end # Renames a column. - # ===== Example + # # t.rename(:description, :name) def rename(column_name, new_column_name) @base.rename_column(@table_name, column_name, new_column_name) @@ -425,38 +439,34 @@ module ActiveRecord # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. - # ===== Examples - # t.references(:goat) - # t.references(:goat, :polymorphic => true) - # t.belongs_to(:goat) + # + # t.references(:user) + # t.belongs_to(:supplier, polymorphic: true) + # def references(*args) options = args.extract_options! - polymorphic = options.delete(:polymorphic) - args.each do |col| - @base.add_column(@table_name, "#{col}_id", :integer, options) - @base.add_column(@table_name, "#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil? + args.each do |ref_name| + @base.add_reference(@table_name, ref_name, options) end end alias :belongs_to :references # Removes a reference. Optionally removes a +type+ column. # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. - # ===== Examples - # t.remove_references(:goat) - # t.remove_references(:goat, :polymorphic => true) - # t.remove_belongs_to(:goat) + # + # t.remove_references(:user) + # t.remove_belongs_to(:supplier, polymorphic: true) + # def remove_references(*args) options = args.extract_options! - polymorphic = options.delete(:polymorphic) - args.each do |col| - @base.remove_column(@table_name, "#{col}_id") - @base.remove_column(@table_name, "#{col}_type") unless polymorphic.nil? + args.each do |ref_name| + @base.remove_reference(@table_name, ref_name, options) end end - alias :remove_belongs_to :remove_references + alias :remove_belongs_to :remove_references # Adds a column or columns of a specified type - # ===== Examples + # # t.string(:goat) # t.string(:goat, :sheep) %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 1f9321edb6..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,5 +1,3 @@ -require 'active_support/deprecation/reporting' -require 'active_record/schema_migration' require 'active_record/migration/join_table' module ActiveRecord @@ -16,12 +14,11 @@ module ActiveRecord # Truncates a table alias according to the limits of the current adapter. def table_alias_for(table_name) - table_name[0...table_alias_length].gsub(/\./, '_') + table_name[0...table_alias_length].tr('.', '_') end # Checks to see if the table +table_name+ exists on the database. # - # === Example # table_exists?(:developers) def table_exists?(table_name) tables.include?(table_name.to_s) @@ -32,7 +29,6 @@ module ActiveRecord # Checks to see if an index exists on a table for a given index definition. # - # === Examples # # Check an index exists # index_exists?(:suppliers, :company_id) # @@ -60,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) # @@ -68,13 +63,18 @@ module ActiveRecord # column_exists?(:suppliers, :name, :string) # # # Check a column exists with a specific definition - # column_exists?(:suppliers, :name, :string, :limit => 100) + # column_exists?(:suppliers, :name, :string, limit: 100) + # column_exists?(:suppliers, :name, :string, default: 'default') + # column_exists?(:suppliers, :name, :string, null: false) + # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) def column_exists?(table_name, column_name, type = nil, options = {}) columns(table_name).any?{ |c| c.name == column_name.to_s && - (!type || c.type == type) && - (!options[:limit] || c.limit == options[:limit]) && - (!options[:precision] || c.precision == options[:precision]) && - (!options[:scale] || c.scale == options[:scale]) } + (!type || c.type == type) && + (!options.key?(:limit) || c.limit == options[:limit]) && + (!options.key?(:precision) || c.precision == options[:precision]) && + (!options.key?(:scale) || c.scale == options[:scale]) && + (!options.key?(:default) || c.default == options[:default]) && + (!options.key?(:null) || c.null == options[:null]) } end # Creates a new table with the name +table_name+. +table_name+ may either @@ -126,7 +126,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -163,7 +162,7 @@ module ActiveRecord yield td if block_given? if options[:force] && table_exists?(table_name) - drop_table(table_name) + drop_table(table_name, options) end create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " @@ -171,10 +170,11 @@ module ActiveRecord create_sql << td.to_sql create_sql << ") #{options[:options]}" execute create_sql + td.indexes.each_pair { |c,o| add_index table_name, c, o } end # Creates a new join table with the name created using the lexical order of the first two - # arguments. These arguments can be be a String or a Symbol. + # arguments. These arguments can be a String or a Symbol. # # # Creates a table called 'assemblies_parts' with no id. # create_join_table(:assemblies, :parts) @@ -192,7 +192,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -204,17 +203,19 @@ 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 # A block for changing columns in +table+. # - # === Example # # change_table() yields a Table instance # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -228,7 +229,6 @@ module ActiveRecord # # Defaults to false. # - # ===== Examples # ====== Add a column # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -287,14 +287,14 @@ module ActiveRecord end # Renames a table. - # ===== Example + # # rename_table('octopuses', 'octopi') def rename_table(table_name, new_name) raise NotImplementedError, "rename_table is not implemented" end # Drops a table from the database. - def drop_table(table_name) + def drop_table(table_name, options = {}) execute "DROP TABLE #{quote_table_name(table_name)}" end @@ -307,7 +307,7 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # remove_column(:suppliers, :qualification) # remove_columns(:suppliers, :qualification, :experience) def remove_column(table_name, *column_names) @@ -317,7 +317,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # change_column(:suppliers, :name, :string, :limit => 80) # change_column(:accounts, :description, :text) def change_column(table_name, column_name, type, options = {}) @@ -325,7 +325,7 @@ module ActiveRecord end # Sets a new default value for a column. - # ===== Examples + # # change_column_default(:suppliers, :qualification, 'new') # change_column_default(:accounts, :authorized, 1) # change_column_default(:users, :email, nil) @@ -334,7 +334,7 @@ module ActiveRecord end # Renames a column. - # ===== Example + # # rename_column(:suppliers, :description, :name) def rename_column(table_name, column_name, new_column_name) raise NotImplementedError, "rename_column is not implemented" @@ -346,8 +346,6 @@ module ActiveRecord # The index will be named after the table and the column name(s), unless # you pass <tt>:name</tt> as an option. # - # ===== Examples - # # ====== Creating a simple index # add_index(:suppliers, :name) # generates @@ -375,15 +373,22 @@ module ActiveRecord # Note: SQLite doesn't support index length # # ====== Creating an index with a sort order (desc or asc, asc is the default) - # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :part_id => :asc}) + # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :party_id => :asc}) # generates # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it) # + # ====== Creating a partial index + # add_index(:accounts, [:branch_id, :party_id], :unique => true, :where => "active") + # generates + # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active + # + # Note: only supported by PostgreSQL + # def add_index(table_name, column_name, options = {}) - index_name, index_type, index_columns = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})" + index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}" end # Remove the given index from the table. @@ -440,6 +445,42 @@ module ActiveRecord indexes(table_name).detect { |i| i.name == index_name } end + # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. + # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. + # + # ====== Create a user_id column + # add_reference(:products, :user) + # + # ====== Create a supplier_id and supplier_type columns + # add_belongs_to(:products, :supplier, polymorphic: true) + # + # ====== Create a supplier_id, supplier_type columns and appropriate index + # add_reference(:products, :supplier, polymorphic: true, index: true) + # + def add_reference(table_name, ref_name, options = {}) + polymorphic = options.delete(:polymorphic) + index_options = options.delete(:index) + add_column(table_name, "#{ref_name}_id", :integer, options) + add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic + add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options + end + alias :add_belongs_to :add_reference + + # Removes the reference(s). Also removes a +type+ column if one exists. + # <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. + # + # ====== Remove the reference + # remove_reference(:products, :user, index: true) + # + # ====== Remove polymorphic reference + # remove_reference(:products, :supplier, polymorphic: true) + # + def remove_reference(table_name, ref_name, options = {}) + remove_column(table_name, "#{ref_name}_id") + remove_column(table_name, "#{ref_name}_type") if options[:polymorphic] + end + alias :remove_belongs_to :remove_reference + # Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the # entire structure of the database. def structure_dump @@ -448,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 @@ -529,15 +570,15 @@ module ActiveRecord end # Adds timestamps (created_at and updated_at) columns to the named table. - # ===== Examples + # # add_timestamps(:suppliers) def add_timestamps(table_name) - add_column table_name, :created_at, :datetime, :null => false - add_column table_name, :updated_at, :datetime, :null => false + add_column table_name, :created_at, :datetime + add_column table_name, :updated_at, :datetime end # Removes the timestamp columns (created_at and updated_at) from the table definition. - # ===== Examples + # # remove_timestamps(:suppliers) def remove_timestamps(table_name) remove_column table_name, :updated_at @@ -549,7 +590,7 @@ module ActiveRecord if options.is_a?(Hash) && order = options[:order] case order when Hash - column_names.each {|name| option_strings[name] += " #{order[name].to_s.upcase}" if order.has_key?(name)} + column_names.each {|name| option_strings[name] += " #{order[name].upcase}" if order.has_key?(name)} when String column_names.each {|name| option_strings[name] += " #{order.upcase}"} end @@ -581,6 +622,9 @@ module ActiveRecord if Hash === options # legacy support, since this param was a string index_type = options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) + if supports_partial_index? + index_options = options[:where] ? " WHERE #{options[:where]}" : "" + end else index_type = options end @@ -593,7 +637,7 @@ module ActiveRecord end index_columns = quoted_columns_for_index(column_names, options).join(", ") - [index_name, index_type, index_columns] + [index_name, index_type, index_columns, index_options] end def index_name_for_remove(table_name, options = {}) @@ -607,8 +651,6 @@ module ActiveRecord end def columns_for_remove(table_name, *column_names) - column_names = column_names.flatten - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank? column_names.map {|column_name| quote_column_name(column_name) } end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index edea414db7..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' @@ -13,21 +12,24 @@ module ActiveRecord autoload :Column autoload :ConnectionSpecification - autoload_under 'abstract' do - autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions' - autoload :ColumnDefinition, 'active_record/connection_adapters/abstract/schema_definitions' - autoload :TableDefinition, 'active_record/connection_adapters/abstract/schema_definitions' - autoload :Table, 'active_record/connection_adapters/abstract/schema_definitions' + autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do + autoload :IndexDefinition + autoload :ColumnDefinition + autoload :TableDefinition + autoload :Table + end + autoload_at 'active_record/connection_adapters/abstract/connection_pool' do + autoload :ConnectionHandler + autoload :ConnectionManagement + end + + autoload_under 'abstract' do autoload :SchemaStatements autoload :DatabaseStatements autoload :DatabaseLimits autoload :Quoting - autoload :ConnectionPool - autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool' - autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool' - autoload :QueryCache end @@ -54,7 +56,7 @@ module ActiveRecord define_callbacks :checkout, :checkin attr_accessor :visitor, :pool - attr_reader :schema_cache, :last_use, :in_use + attr_reader :schema_cache, :last_use, :in_use, :logger alias :in_use? :in_use def initialize(connection, logger = nil, pool = nil) #:nodoc: @@ -83,6 +85,11 @@ module ActiveRecord end end + def schema_cache=(cache) + cache.connection = self + @schema_cache = cache + end + def expire @in_use = false end @@ -142,6 +149,11 @@ module ActiveRecord false end + # Does this adapter support partial indices? + def supports_partial_index? + false + end + # Does this adapter support explain? As of this writing sqlite3, # mysql2, and postgresql are the only ones that do. def supports_explain? @@ -150,15 +162,10 @@ module ActiveRecord # QUOTING ================================================== - # Override to return the quoted table name. Defaults to column quoting. - def quote_table_name(name) - quote_column_name(name) - end - # Returns a bind substitution value given a +column+ and list of current # +binds+ def substitute_at(column, index) - Arel.sql '?' + Arel::Nodes::BindParam.new '?' end # REFERENTIAL INTEGRITY ==================================== @@ -271,26 +278,25 @@ module ActiveRecord protected - def log(sql, name = "SQL", binds = []) - @instrumenter.instrument( - "sql.active_record", - :sql => sql, - :name => name, - :connection_id => object_id, - :binds => binds) { yield } - rescue Exception => e - message = "#{e.class.name}: #{e.message}: #{sql}" - @logger.debug message if @logger - exception = translate_exception(e, message) - exception.set_backtrace e.backtrace - raise exception - end - - def translate_exception(e, message) - # override in derived class - ActiveRecord::StatementInvalid.new(message) - end - + def log(sql, name = "SQL", binds = []) + @instrumenter.instrument( + "sql.active_record", + :sql => sql, + :name => name, + :connection_id => object_id, + :binds => binds) { yield } + rescue => e + message = "#{e.class.name}: #{e.message}: #{sql}" + @logger.error message if @logger + exception = translate_exception(e, message) + exception.set_backtrace e.backtrace + raise exception + end + + def translate_exception(exception, message) + # override in derived class + ActiveRecord::StatementInvalid.new(message) + end end end end 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 e1dad5b166..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,4 @@ -require 'active_support/core_ext/object/blank' +require 'arel/visitors/bind_visitor' module ActiveRecord module ConnectionAdapters @@ -71,6 +71,8 @@ module ActiveRecord when /^mediumint/i; 3 when /^smallint/i; 2 when /^tinyint/i; 1 + when /^enum\((.+)\)/i + $1.split(',').map{|enum| enum.strip.length - 2}.max else super end @@ -122,12 +124,21 @@ module ActiveRecord :boolean => { :name => "tinyint", :limit => 1 } } + class BindSubstitution < Arel::Visitors::MySQL # :nodoc: + include Arel::Visitors::BindVisitor + end + # FIXME: Make the first parameter more similar for the two adapters def initialize(connection, logger, connection_options, config) super(connection, logger) @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} - @visitor = Arel::Visitors::MySQL.new self + + if config.fetch(:prepared_statements) { true } + @visitor = Arel::Visitors::MySQL.new self + else + @visitor = BindSubstitution.new self + end end def adapter_name #:nodoc: @@ -241,7 +252,7 @@ module ActiveRecord end # MysqlAdapter has to free a result after using it, so we use this method to write - # stuff in a abstract way without concerning ourselves about whether it needs to be + # stuff in an abstract way without concerning ourselves about whether it needs to be # explicitly freed or not. def execute_and_free(sql, name = nil) #:nodoc: yield execute(sql, name) @@ -254,19 +265,19 @@ module ActiveRecord def begin_db_transaction execute "BEGIN" - rescue Exception + rescue # Transactions aren't supported end def commit_db_transaction #:nodoc: execute "COMMIT" - rescue Exception + rescue # Transactions aren't supported end def rollback_db_transaction #:nodoc: execute "ROLLBACK" - rescue Exception + rescue # Transactions aren't supported end @@ -284,19 +295,10 @@ module ActiveRecord # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support - # these, we must use a subquery. However, MySQL is too stupid to create a - # temporary table for this automatically, so we have to give it some prompting - # in the form of a subsubquery. Ugh! + # these, we must use a subquery. def join_to_update(update, select) #:nodoc: if select.limit || select.offset || select.orders.any? - subsubselect = select.clone - subsubselect.projections = [update.key] - - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(update.key.name) - subselect.from subsubselect.as('__active_record_temp') - - update.where update.key.in(subselect) + super else update.table select.source update.wheres = select.constraints @@ -312,10 +314,10 @@ module ActiveRecord sql = "SHOW TABLES" end - select_all(sql).map { |table| + 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).first['Create Table'] + ";\n\n" + exec_query(sql, 'SCHEMA').first['Create Table'] + ";\n\n" }.join end @@ -365,7 +367,7 @@ module ActiveRecord def tables(name = nil, database = nil, like = nil) #:nodoc: sql = "SHOW TABLES " - sql << "IN #{database} " if database + sql << "IN #{quote_table_name(database)} " if database sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| @@ -427,7 +429,7 @@ module ActiveRecord table, arguments = args.shift, args method = :"#{command}_sql" - if respond_to?(method) + if respond_to?(method, true) send(method, table, *arguments) else raise "Unknown method called : #{method}(#{arguments.inspect})" @@ -474,15 +476,26 @@ module ActiveRecord # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) - return super unless type.to_s == 'integer' - - case limit - when 1; 'tinyint' - when 2; 'smallint' - when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") + case type.to_s + when 'integer' + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + when 'text' + case limit + when 0..0xff; 'tinytext' + when nil, 0x100..0xffff; 'text' + when 0x10000..0xffffff; 'mediumtext' + when 0x1000000..0xffffffff; 'longtext' + else raise(ActiveRecordError, "No text type has character length #{limit}") + end + else + super end end @@ -496,7 +509,7 @@ module ActiveRecord # SHOW VARIABLES LIKE 'name' def show_variable(name) - variables = select_all("SHOW VARIABLES LIKE '#{name}'") + variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') variables.first['Value'] unless variables.empty? end @@ -504,8 +517,8 @@ module ActiveRecord def pk_and_sequence_for(table) execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result| create_table = each_hash(result).first[:"Create Table"] - if create_table.to_s =~ /PRIMARY KEY\s+\((.+)\)/ - keys = $1.split(",").map { |key| key.gsub(/[`"]/, "") } + if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/ + keys = $1.split(",").map { |key| key.delete('`"') } keys.length == 1 ? [keys.first, nil] : nil else nil @@ -537,6 +550,17 @@ module ActiveRecord protected + # MySQL is too stupid to create a temporary table for use subquery, so we have + # to give it some prompting in the form of a subsubquery. Ugh! + def subquery_for(key, select) + subsubselect = select.clone + subsubselect.projections = [key] + + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(key.name) + subselect.from subsubselect.as('__active_record_temp') + end + def add_index_length(option_strings, column_names, options = {}) if options.is_a?(Hash) && length = options[:length] case length @@ -607,7 +631,7 @@ module ActiveRecord raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" end - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" add_column_options!(rename_column_sql, options) rename_column_sql diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 34d88edff3..0390168461 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -66,6 +66,10 @@ module ActiveRecord end end + def binary? + type == :binary + end + # Casts a Ruby value to something appropriate for writing to the database. def type_cast_for_write(value) return value unless number? @@ -90,7 +94,7 @@ module ActiveRecord case type when :string, :text then value - when :integer then value.to_i rescue value ? 1 : 0 + when :integer then value.to_i when :float then value.to_f when :decimal then klass.value_to_decimal(value) when :datetime, :timestamp then klass.string_to_time(value) @@ -98,12 +102,14 @@ module ActiveRecord when :date then klass.value_to_date(value) when :binary then klass.binary_to_string(value) when :boolean then klass.value_to_boolean(value) - when :hstore then klass.cast_hstore(value) else value end end def type_cast_code(var_name) + ActiveSupport::Deprecation.warn("Column#type_cast_code is deprecated in favor of" \ + "using Column#type_cast only, and it is going to be removed in future Rails versions.") + klass = self.class.name case type @@ -116,7 +122,9 @@ module ActiveRecord when :date then "#{klass}.value_to_date(#{var_name})" when :binary then "#{klass}.binary_to_string(#{var_name})" when :boolean then "#{klass}.value_to_boolean(#{var_name})" - when :hstore then "#{klass}.cast_hstore(#{var_name})" + when :hstore then "#{klass}.string_to_hstore(#{var_name})" + when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})" + when :json then "#{klass}.string_to_json(#{var_name})" else var_name end end @@ -151,7 +159,7 @@ module ActiveRecord def value_to_date(value) if value.is_a?(String) - return nil if value.empty? + return nil if value.blank? fast_string_to_date(value) || fallback_string_to_date(value) elsif value.respond_to?(:to_date) value.to_date @@ -162,16 +170,22 @@ module ActiveRecord def string_to_time(string) return string unless string.is_a?(String) - return nil if string.empty? + return nil if string.blank? fast_string_to_time(string) || fallback_string_to_time(string) end def string_to_dummy_time(string) return string unless string.is_a?(String) - return nil if string.empty? + return nil if string.blank? - string_to_time "2000-01-01 #{string}" + dummy_time_string = "2000-01-01 #{string}" + + fast_string_to_time(dummy_time_string) || begin + time_hash = Date._parse(dummy_time_string) + return nil if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end end # convert something to a boolean @@ -201,7 +215,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) @@ -226,7 +240,7 @@ module ActiveRecord # Doesn't handle time zones. def fast_string_to_time(string) if string =~ Format::ISO_DATETIME - microsec = ($7.to_f * 1_000_000).to_i + microsec = ($7.to_r * 1_000_000).to_i new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec end end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 8491d42b86..dd40351a38 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -1,3 +1,5 @@ +require 'uri' + module ActiveRecord module ConnectionAdapters class ConnectionSpecification #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index c1332fde1a..8fc172f6e8 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -32,6 +32,7 @@ module ActiveRecord def initialize(connection, logger, connection_options, config) super + @visitor = BindSubstitution.new self configure_connection end @@ -65,10 +66,6 @@ module ActiveRecord @connection.escape(string) end - def substitute_at(column, index) - Arel.sql "\0" - end - # CONNECTION MANAGEMENT ==================================== def active? @@ -80,6 +77,7 @@ module ActiveRecord disconnect! connect end + alias :reset! :reconnect! # Disconnects from the database if already connected. # Otherwise, this method does nothing. @@ -90,15 +88,10 @@ module ActiveRecord end end - def reset! - disconnect! - connect - end - # DATABASE STATEMENTS ====================================== def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel)}" + sql = "EXPLAIN #{to_sql(arel, binds.dup)}" start = Time.now result = exec_query(sql, 'EXPLAIN', binds) elapsed = Time.now - start @@ -224,8 +217,7 @@ module ActiveRecord # Returns an array of record hashes with the column names as keys and # column values as values. def select(sql, name = nil, binds = []) - binds = binds.dup - exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name) + exec_query(sql, name) end def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) @@ -234,18 +226,12 @@ module ActiveRecord end alias :create :insert_sql - def exec_insert(sql, name, binds) - binds = binds.dup - - # Pretend to support bind parameters - execute sql.gsub("\0") { quote(*binds.shift.reverse) }, name + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + execute to_sql(sql, binds), name end def exec_delete(sql, name, binds) - binds = binds.dup - - # Pretend to support bind parameters - execute sql.gsub("\0") { quote(*binds.shift.reverse) }, name + execute to_sql(sql, binds), name @connection.affected_rows end alias :exec_update :exec_delete @@ -267,6 +253,14 @@ module ActiveRecord # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 variable_assignments = ['SQL_AUTO_IS_NULL=0'] + + # Make MySQL reject illegal values rather than truncating or + # blanking them. See + # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables + if @config.fetch(:strict, true) + variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'" + end + encoding = @config[:encoding] # make sure we set the encoding @@ -274,7 +268,7 @@ module ActiveRecord # increase timeout so mysql server doesn't disconnect us wait_timeout = @config[:wait_timeout] - wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) + wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) variable_assignments << "@@wait_timeout = #{wait_timeout}" execute("SET #{variable_assignments.join(', ')}", :skip_logging) diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 5905242747..6bf7af081f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -53,6 +53,7 @@ module ActiveRecord # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). + # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html) # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. @@ -119,7 +120,7 @@ module ActiveRecord private def cache - @cache[$$] + @cache[Process.pid] end end @@ -214,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 @@ -278,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 @@ -320,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 @@ -338,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 @@ -404,6 +543,13 @@ module ActiveRecord # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) + + # Make MySQL reject illegal values rather than truncating or + # blanking them. See + # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables + if @config.fetch(:strict, true) + execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging) + end end def select(sql, name = nil, binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb new file mode 100644 index 0000000000..b59195f98a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -0,0 +1,96 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLColumn < Column + module Cast + def string_to_time(string) + return string unless String === string + + case string + when 'infinity'; 1.0 / 0.0 + when '-infinity'; -1.0 / 0.0 + else + super + end + end + + def hstore_to_string(object) + if Hash === object + object.map { |k,v| + "#{escape_hstore(k)}=>#{escape_hstore(v)}" + }.join ',' + else + object + end + end + + def string_to_hstore(string) + if string.nil? + nil + elsif String === string + Hash[string.scan(HstorePair).map { |k,v| + v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + [k,v] + }] + else + string + end + end + + def json_to_string(object) + if Hash === object + ActiveSupport::JSON.encode(object) + else + object + end + end + + def string_to_json(string) + if String === string + ActiveSupport::JSON.decode(string) + else + string + end + end + + def string_to_cidr(string) + if string.nil? + nil + elsif String === string + IPAddr.new(string) + else + string + end + end + + def cidr_to_string(object) + if IPAddr === object + "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + else + object + end + end + + private + + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + + def escape_hstore(value) + if value.nil? + 'NULL' + else + if value == "" + '""' + else + '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb new file mode 100644 index 0000000000..eb3084e066 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -0,0 +1,234 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module DatabaseStatements + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel, binds)}" + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + end + + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # PostgreSQL shell: + # + # QUERY PLAN + # ------------------------------------------------------------------------------ + # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) + # Join Filter: (posts.user_id = users.id) + # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) + # Index Cond: (id = 1) + # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) + # Filter: (posts.user_id = 1) + # (6 rows) + # + def pp(result) + header = result.columns.first + lines = result.rows.map(&:first) + + # We add 2 because there's one char of padding at both sides, note + # the extra hyphens in the example above. + width = [header, *lines].map(&:length).max + 2 + + pp = [] + + pp << header.center(width).rstrip + pp << '-' * width + + pp += lines.map {|line| " #{line}"} + + nrows = result.rows.length + rows_label = nrows == 1 ? 'row' : 'rows' + pp << "(#{nrows} #{rows_label})" + + pp.join("\n") + "\n" + end + end + + # Executes a SELECT query and returns an array of rows. Each row is an + # array of field values. + def select_rows(sql, name = nil) + select_raw(sql, name).last + end + + # Executes an INSERT query and returns the new record's ID + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + unless pk + # Extract the table from the insert sql. Yuck. + table_ref = extract_table_ref_from_insert_sql(sql) + pk = primary_key(table_ref) if table_ref + end + + if pk && use_insert_returning? + select_value("#{sql} RETURNING #{quote_column_name(pk)}") + elsif pk + super + last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) + else + super + end + end + + def create + super.insert + end + + # create a 2D array representing the result set + def result_as_array(res) #:nodoc: + # check if we have any binary column and if they need escaping + ftypes = Array.new(res.nfields) do |i| + [i, res.ftype(i)] + end + + rows = res.values + return rows unless ftypes.any? { |_, x| + x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID + } + + typehash = ftypes.group_by { |_, type| type } + binaries = typehash[BYTEA_COLUMN_TYPE_OID] || [] + monies = typehash[MONEY_COLUMN_TYPE_OID] || [] + + rows.each do |row| + # unescape string passed BYTEA field (OID == 17) + binaries.each do |index, _| + row[index] = unescape_bytea(row[index]) + end + + # If this is a money type column and there are any currency symbols, + # then strip them off. Indeed it would be prettier to do this in + # PostgreSQLColumn.string_to_decimal but would break form input + # fields that call value_before_type_cast. + monies.each do |index, _| + data = row[index] + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + case data + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + data.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) + data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + end + end + end + end + + # Queries the database and returns the results in an Array-like object + def query(sql, name = nil) #:nodoc: + log(sql, name) do + result_as_array @connection.async_exec(sql) + end + end + + # Executes an SQL statement, returning a PGresult object on success + # or raising a PGError exception otherwise. + def execute(sql, name = nil) + log(sql, name) do + @connection.async_exec(sql) + end + end + + def substitute_at(column, index) + Arel::Nodes::BindParam.new "$#{index + 1}" + end + + def exec_query(sql, name = 'SQL', binds = []) + log(sql, name, binds) do + result = binds.empty? ? exec_no_cache(sql, binds) : + exec_cache(sql, binds) + + types = {} + result.fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| + warn "unknown OID: #{fname}(#{oid}) (#{sql})" + OID::Identity.new + } + end + + ret = ActiveRecord::Result.new(result.fields, result.values, types) + result.clear + return ret + end + end + + def exec_delete(sql, name = 'SQL', binds = []) + log(sql, name, binds) do + result = binds.empty? ? exec_no_cache(sql, binds) : + exec_cache(sql, binds) + affected = result.cmd_tuples + result.clear + affected + end + end + alias :exec_update :exec_delete + + def sql_for_insert(sql, pk, id_value, sequence_name, binds) + unless pk + # Extract the table from the insert sql. Yuck. + table_ref = extract_table_ref_from_insert_sql(sql) + pk = primary_key(table_ref) if table_ref + end + + if pk && use_insert_returning? + sql = "#{sql} RETURNING #{quote_column_name(pk)}" + end + + [sql, binds] + end + + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + val = exec_query(sql, name, binds) + if !use_insert_returning? && pk + unless sequence_name + table_ref = extract_table_ref_from_insert_sql(sql) + sequence_name = default_sequence_name(table_ref, pk) + return val unless sequence_name + end + last_insert_id_result(sequence_name) + else + val + end + end + + # Executes an UPDATE query and returns the number of affected tuples. + def update_sql(sql, name = nil) + super.cmd_tuples + end + + # Begins a transaction. + def begin_db_transaction + execute "BEGIN" + end + + # Commits a transaction. + def commit_db_transaction + execute "COMMIT" + end + + # Aborts a transaction. + def rollback_db_transaction + execute "ROLLBACK" + end + + def outside_transaction? + @connection.transaction_status == PGconn::PQTRANS_IDLE + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb new file mode 100644 index 0000000000..b8e7687b21 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -0,0 +1,262 @@ +require 'active_record/connection_adapters/abstract_adapter' + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module OID + 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 Bytea < Type + def type_cast(value) + PGconn.unescape_bytea value + end + end + + class Money < Type + def type_cast(value) + return if value.nil? + + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + + case value + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + value.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) + value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + end + + ConnectionAdapters::Column.value_to_decimal value + end + end + + class Vector < Type + attr_reader :delim, :subtype + + # +delim+ corresponds to the `typdelim` column in the pg_types + # table. +subtype+ is derived from the `typelem` column in the + # pg_types table. + def initialize(delim, subtype) + @delim = delim + @subtype = subtype + end + + # FIXME: this should probably split on +delim+ and use +subtype+ + # to cast the values. Unfortunately, the current Rails behavior + # is to just return the string. + 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 Boolean < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::Column.value_to_boolean value + end + end + + class Timestamp < Type + def type; :timestamp; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is PG + # specific + ConnectionAdapters::PostgreSQLColumn.string_to_time value + end + end + + class Date < Type + def type; :datetime; end + + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is PG + # specific + ConnectionAdapters::Column.value_to_date value + end + end + + class Time < Type + def type_cast(value) + return if value.nil? + + # FIXME: probably we can improve this since we know it is PG + # specific + ConnectionAdapters::Column.string_to_dummy_time value + end + end + + class Float < Type + 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 Hstore < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_hstore value + end + end + + class Cidr < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_cidr value + end + end + + class Json < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_json value + end + end + + class TypeMap + def initialize + @mapping = {} + end + + def []=(oid, type) + @mapping[oid] = type + end + + def [](oid) + @mapping[oid] + end + + def key?(oid) + @mapping.key? oid + end + + def fetch(ftype, fmod) + # The type for the numeric depends on the width of the field, + # so we'll do something special here. + # + # When dealing with decimal columns: + # + # places after decimal = fmod - 4 & 0xffff + # places before decimal = (fmod - 4) >> 16 & 0xffff + if ftype == 1700 && (fmod - 4 & 0xffff).zero? + ftype = 23 + end + + @mapping.fetch(ftype) { |oid| yield oid, fmod } + end + end + + TYPE_MAP = TypeMap.new # :nodoc: + + # When the PG adapter connects, the pg_type table is queried. The + # key of this hash maps to the `typname` column from the table. + # TYPE_MAP is then dynamically built with oids as the key and type + # objects as values. + NAMES = Hash.new { |h,k| # :nodoc: + h[k] = OID::Identity.new + } + + # Register an OID type named +name+ with a typcasting object in + # +type+. +name+ should correspond to the `typname` column in + # the `pg_type` table. + def self.register_type(name, type) + NAMES[name] = type + end + + # Alias the +old+ type to the +new+ type. + def self.alias_type(new, old) + NAMES[new] = NAMES[old] + end + + # Is +name+ a registered type? + def self.registered_type?(name) + NAMES.key? name + end + + register_type 'int2', OID::Integer.new + alias_type 'int4', 'int2' + alias_type 'int8', 'int2' + alias_type 'oid', 'int2' + + register_type 'numeric', OID::Decimal.new + register_type 'text', OID::Identity.new + alias_type 'varchar', 'text' + alias_type 'char', 'text' + alias_type 'bpchar', 'text' + alias_type 'xml', 'text' + + # FIXME: why are we keeping these types as strings? + alias_type 'tsvector', 'text' + alias_type 'interval', 'text' + alias_type 'bit', 'text' + alias_type 'varbit', 'text' + alias_type 'macaddr', 'text' + alias_type 'uuid', 'text' + + # FIXME: I don't think this is correct. We should probably be returning a parsed date, + # but the tests pass with a string returned. + register_type 'timestamptz', OID::Identity.new + + register_type 'money', OID::Money.new + register_type 'bytea', OID::Bytea.new + register_type 'bool', OID::Boolean.new + + register_type 'float4', OID::Float.new + alias_type 'float8', 'float4' + + register_type 'timestamp', OID::Timestamp.new + register_type 'date', OID::Date.new + register_type 'time', OID::Time.new + + register_type 'path', OID::Identity.new + register_type 'polygon', OID::Identity.new + register_type 'circle', OID::Identity.new + register_type 'hstore', OID::Hstore.new + register_type 'json', OID::Json.new + + register_type 'cidr', OID::Cidr.new + alias_type 'inet', 'cidr' + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb new file mode 100644 index 0000000000..152258a2f4 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -0,0 +1,124 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module Quoting + # Escapes binary strings for bytea input to the database. + def escape_bytea(value) + PGconn.escape_bytea(value) if value + end + + # Unescapes bytea output from a database to the binary string it represents. + # NOTE: This is NOT an inverse of escape_bytea! This is only to be used + # on escaped binary output from database drive. + def unescape_bytea(value) + PGconn.unescape_bytea(value) if value + end + + # Quotes PostgreSQL-specific data types for SQL input. + def quote(value, column = nil) #:nodoc: + return super unless column + + case value + when Hash + case column.sql_type + when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) + when 'json' then super(PostgreSQLColumn.json_to_string(value), column) + else super + end + when IPAddr + case column.sql_type + when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) + else super + end + when Float + if value.infinite? && column.type == :datetime + "'#{value.to_s.downcase}'" + elsif value.infinite? || value.nan? + "'#{value.to_s}'" + else + super + end + when Numeric + return super unless column.sql_type == 'money' + # Not truly string input, so doesn't require (or allow) escape string syntax. + "'#{value}'" + when String + case column.sql_type + when 'bytea' then "'#{escape_bytea(value)}'" + when 'xml' then "xml '#{quote_string(value)}'" + when /^bit/ + case value + when /^[01]*$/ then "B'#{value}'" # Bit-string notation + when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation + end + else + super + end + else + super + end + end + + def type_cast(value, column) + return super unless column + + case value + when String + return super unless 'bytea' == column.sql_type + { :value => value, :format => 1 } + when Hash + case column.sql_type + when 'hstore' then PostgreSQLColumn.hstore_to_string(value) + when 'json' then PostgreSQLColumn.json_to_string(value) + else super + end + when IPAddr + return super unless ['inet','cidr'].includes? column.sql_type + PostgreSQLColumn.cidr_to_string(value) + else + super + end + end + + # Quotes strings for use in SQL input. + def quote_string(s) #:nodoc: + @connection.escape(s) + end + + # Checks the following cases: + # + # - table_name + # - "table.name" + # - schema_name.table_name + # - schema_name."table.name" + # - "schema.name".table_name + # - "schema.name"."table.name" + def quote_table_name(name) + schema, name_part = extract_pg_identifier_from_name(name.to_s) + + unless name_part + quote_column_name(schema) + else + table_name, name_part = extract_pg_identifier_from_name(name_part) + "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" + end + end + + # Quotes column names for use in SQL queries. + def quote_column_name(name) #:nodoc: + PGconn.quote_ident(name.to_s) + end + + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. + def quoted_date(value) #:nodoc: + if value.acts_like?(:time) && value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb new file mode 100644 index 0000000000..16da3ea732 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module ReferentialIntegrity + def supports_disable_referential_integrity? #:nodoc: + true + end + + def disable_referential_integrity #:nodoc: + if supports_disable_referential_integrity? then + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) + end + yield + ensure + if supports_disable_referential_integrity? then + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb new file mode 100644 index 0000000000..60f01c297e --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -0,0 +1,446 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module SchemaStatements + # Drops the database specified on the +name+ attribute + # and creates it again using the provided +options+. + def recreate_database(name, options = {}) #:nodoc: + drop_database(name) + create_database(name, options) + end + + # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, + # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>, + # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses + # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). + # + # Example: + # create_database config[:database], config + # create_database 'foo_development', :encoding => 'unicode' + def create_database(name, options = {}) + options = options.reverse_merge(:encoding => "utf8") + + option_string = options.symbolize_keys.sum do |key, value| + case key + when :owner + " OWNER = \"#{value}\"" + when :template + " TEMPLATE = \"#{value}\"" + when :encoding + " ENCODING = '#{value}'" + when :collation + " LC_COLLATE = '#{value}'" + when :ctype + " LC_CTYPE = '#{value}'" + when :tablespace + " TABLESPACE = \"#{value}\"" + when :connection_limit + " CONNECTION LIMIT = #{value}" + else + "" + end + end + + execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}" + end + + # Drops a PostgreSQL database. + # + # Example: + # drop_database 'matt_development' + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" + end + + # Returns the list of all tables in the schema search path or a specified schema. + def tables(name = nil) + query(<<-SQL, 'SCHEMA').map { |row| row[0] } + SELECT tablename + FROM pg_tables + WHERE schemaname = ANY (current_schemas(false)) + SQL + end + + # Returns true if table exists. + # If the schema is not specified as part of +name+ then it will only find tables within + # the current schema search path (regardless of permissions to access tables in other schemas) + def table_exists?(name) + schema, table = Utils.extract_schema_and_table(name.to_s) + return false unless table + + binds = [[nil, table]] + binds << [nil, schema] if schema + + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind in ('v','r') + AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' + AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} + SQL + end + + # Returns true if schema exists. + def schema_exists?(name) + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_namespace + WHERE nspname = '#{name}' + SQL + end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + result = query(<<-SQL, 'SCHEMA') + SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid + FROM pg_class t + INNER JOIN pg_index d ON t.oid = d.indrelid + INNER JOIN pg_class i ON d.indexrelid = i.oid + WHERE i.relkind = 'i' + AND d.indisprimary = 'f' + AND t.relname = '#{table_name}' + AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + ORDER BY i.relname + SQL + + result.map do |row| + index_name = row[0] + unique = row[1] == 't' + indkey = row[2].split(" ") + inddef = row[3] + oid = row[4] + + columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")] + SELECT a.attnum, a.attname + FROM pg_attribute a + WHERE a.attrelid = #{oid} + AND a.attnum IN (#{indkey.join(",")}) + SQL + + column_names = columns.values_at(*indkey).compact + + # add info on sort order for columns (only desc order is explicitly specified, asc is the default) + desc_order_columns = inddef.scan(/(\w+) DESC/).flatten + orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} + where = inddef.scan(/WHERE (.+)$/).flatten[0] + + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) + end.compact + end + + # Returns the list of all column definitions for a table. + def columns(table_name) + # Limit, precision, and scale are all handled by the superclass. + column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| + oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) { + OID::Identity.new + } + PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') + end + end + + # Returns the current database name. + def current_database + query('select current_database()', 'SCHEMA')[0][0] + end + + # Returns the current schema name. + def current_schema + query('SELECT current_schema', 'SCHEMA')[0][0] + end + + # Returns the current database encoding format. + def encoding + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database + WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns the current database collation. + def collation + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns the current database ctype. + def ctype + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns an array of schema names. + def schema_names + query(<<-SQL, 'SCHEMA').flatten + SELECT nspname + FROM pg_namespace + WHERE nspname !~ '^pg_.*' + AND nspname NOT IN ('information_schema') + ORDER by nspname; + SQL + end + + # Creates a schema for the given schema name. + def create_schema schema_name + execute "CREATE SCHEMA #{schema_name}" + end + + # Drops the schema for the given schema name. + def drop_schema schema_name + execute "DROP SCHEMA #{schema_name} CASCADE" + end + + # Sets the schema search path to a string of comma-separated schema names. + # Names beginning with $ have to be quoted (e.g. $user => '$user'). + # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html + # + # This should be not be called manually but set in database.yml. + def schema_search_path=(schema_csv) + if schema_csv + execute("SET search_path TO #{schema_csv}", 'SCHEMA') + @schema_search_path = schema_csv + end + end + + # Returns the active schema search path. + def schema_search_path + @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] + end + + # Returns the current client message level. + def client_min_messages + query('SHOW client_min_messages', 'SCHEMA')[0][0] + end + + # Set the client message level. + def client_min_messages=(level) + execute("SET client_min_messages TO '#{level}'", 'SCHEMA') + end + + # Returns the sequence name for a table's primary key or some other specified key. + def default_sequence_name(table_name, pk = nil) #:nodoc: + result = serial_sequence(table_name, pk || 'id') + return nil unless result + result.split('.').last + rescue ActiveRecord::StatementInvalid + "#{table_name}_#{pk || 'id'}_seq" + end + + def serial_sequence(table, column) + result = exec_query(<<-eosql, 'SCHEMA') + SELECT pg_get_serial_sequence('#{table}', '#{column}') + eosql + result.rows.first.first + end + + # Resets the sequence of a table's primary key to the maximum value. + def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: + unless pk and sequence + default_pk, default_sequence = pk_and_sequence_for(table) + + pk ||= default_pk + sequence ||= default_sequence + end + + if @logger && pk && !sequence + @logger.warn "#{table} has primary key #{pk} with no default sequence" + end + + if pk && sequence + quoted_sequence = quote_table_name(sequence) + + select_value <<-end_sql, 'Reset sequence' + SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) + end_sql + end + end + + # Returns a table's primary key and belonging sequence. + def pk_and_sequence_for(table) #:nodoc: + # First try looking for a sequence with a dependency on the + # given table's primary key. + result = query(<<-end_sql, 'PK and serial sequence')[0] + SELECT attr.attname, seq.relname + FROM pg_class seq, + pg_attribute attr, + pg_depend dep, + pg_namespace name, + pg_constraint cons + WHERE seq.oid = dep.objid + AND seq.relkind = 'S' + AND attr.attrelid = dep.refobjid + AND attr.attnum = dep.refobjsubid + AND attr.attrelid = cons.conrelid + AND attr.attnum = cons.conkey[1] + AND cons.contype = 'p' + AND dep.refobjid = '#{quote_table_name(table)}'::regclass + end_sql + + if result.nil? or result.empty? + # If that fails, try parsing the primary key's default value. + # Support the 7.x and 8.0 nextval('foo'::text) as well as + # the 8.1+ nextval('foo'::regclass). + result = query(<<-end_sql, 'PK and custom sequence')[0] + SELECT attr.attname, + CASE + WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN + substr(split_part(def.adsrc, '''', 2), + strpos(split_part(def.adsrc, '''', 2), '.')+1) + ELSE split_part(def.adsrc, '''', 2) + END + FROM pg_class t + JOIN pg_attribute attr ON (t.oid = attrelid) + JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) + JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) + WHERE t.oid = '#{quote_table_name(table)}'::regclass + AND cons.contype = 'p' + AND def.adsrc ~* 'nextval' + end_sql + end + + [result.first, result.last] + rescue + nil + end + + # Returns just a table's primary key + def primary_key(table) + row = exec_query(<<-end_sql, 'SCHEMA').rows.first + SELECT DISTINCT(attr.attname) + FROM pg_attribute attr + INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid + INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + WHERE cons.contype = 'p' + AND dep.refobjid = '#{table}'::regclass + end_sql + + row && row.first + 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. + # See TableDefinition#column for details of the options you can use. + def add_column(table_name, column_name, type, options = {}) + clear_cache! + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + + execute add_column_sql + end + + # Changes the column of a table. + def change_column(table_name, column_name, type, options = {}) + clear_cache! + quoted_table_name = quote_table_name(table_name) + + execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + + change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) + change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + end + + # Changes the default value of a table column. + def change_column_default(table_name, column_name, default) + clear_cache! + execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" + end + + def change_column_null(table_name, column_name, null, default = nil) + clear_cache! + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") + end + + # Renames a column in a table. + def rename_column(table_name, column_name, new_column_name) + clear_cache! + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" + end + + def remove_index!(table_name, index_name) #:nodoc: + execute "DROP INDEX #{quote_table_name(index_name)}" + end + + def rename_index(table_name, old_name, new_name) + execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" + end + + def index_name_length + 63 + end + + # Maps logical Rails types to PostgreSQL-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + case type.to_s + when 'binary' + # PostgreSQL doesn't support limits on binary (bytea) columns. + # The hard limit is 1Gb, because of a 32-bit size field, and TOAST. + case limit + when nil, 0..0x3fffffff; super(type) + else raise(ActiveRecordError, "No binary type has byte size #{limit}.") + end + when 'integer' + return 'integer' unless limit + + case limit + when 1, 2; 'smallint' + when 3, 4; 'integer' + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + end + when 'datetime' + return super unless precision + + case precision + when 0..6; "timestamp(#{precision})" + else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") + end + else + super + end + end + + # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. + # + # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and + # requires that the ORDER BY include the distinct column. + # + # distinct("posts.id", "posts.created_at desc") + def distinct(columns, orders) #:nodoc: + return "DISTINCT #{columns}" if orders.empty? + + # Construct a clean list of column names from the ORDER BY clause, removing + # any ASC/DESC modifiers + order_columns = orders.collect do |s| + s = s.to_sql unless s.is_a?(String) + s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + end + order_columns.delete_if { |c| c.blank? } + order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } + + "DISTINCT #{columns}, #{order_columns * ', '}" + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 194c814e5b..c508afb33e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,11 +1,19 @@ 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 'active_record/connection_adapters/postgresql/cast' +require 'active_record/connection_adapters/postgresql/quoting' +require 'active_record/connection_adapters/postgresql/schema_statements' +require 'active_record/connection_adapters/postgresql/database_statements' +require 'active_record/connection_adapters/postgresql/referential_integrity' +require 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values gem 'pg', '~> 0.11' require 'pg' +require 'ipaddr' + module ActiveRecord module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects @@ -14,8 +22,8 @@ module ActiveRecord # Forward any unused config params to PGconn.connect. [:statement_limit, :encoding, :min_messages, :schema_search_path, - :schema_order, :adapter, :pool, :wait_timeout, :template, - :reaping_frequency].each do |key| + :schema_order, :adapter, :pool, :checkout_timeout, :template, + :reaping_frequency, :insert_returning].each do |key| conn_params.delete key end conn_params.delete_if { |k,v| v.nil? } @@ -34,67 +42,97 @@ module ActiveRecord # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: # Instantiates a new PostgreSQL column definition in a table. - def initialize(name, default, sql_type = nil, null = true) + def initialize(name, default, oid_type, sql_type = nil, null = true) + @oid_type = oid_type super(name, self.class.extract_value_from_default(default), sql_type, null) end # :stopdoc: class << self - attr_accessor :money_precision - def string_to_time(string) - return string unless String === string + include ConnectionAdapters::PostgreSQLColumn::Cast - case string - when 'infinity' then 1.0 / 0.0 - when '-infinity' then -1.0 / 0.0 - else - super - end - end + attr_accessor :money_precision + end + # :startdoc: - def cast_hstore(object) - if Hash === object - object.map { |k,v| - "#{escape_hstore(k)}=>#{escape_hstore(v)}" - }.join ', ' + # Extracts the value from a PostgreSQL column default definition. + def self.extract_value_from_default(default) + # This is a performance optimization for Ruby 1.9.2 in development. + # If the value is nil, we return nil straight away without checking + # the regular expressions. If we check each regular expression, + # Regexp#=== will call NilClass#to_str, which will trigger + # method_missing (defined by whiny nil in ActiveSupport) which + # makes this method very very slow. + return default unless default + + case default + # Numeric types + when /\A\(?(-?\d+(\.\d*)?\)?)\z/ + $1 + # Character types + when /\A'(.*)'::(?:character varying|bpchar|text)\z/m + $1 + # Character types (8.1 formatting) + when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m + $1.gsub(/\\(\d\d\d)/) { $1.oct.chr } + # Binary data types + when /\A'(.*)'::bytea\z/m + $1 + # Date/time types + when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ + $1 + when /\A'(.*)'::interval\z/ + $1 + # Boolean type + when 'true' + true + when 'false' + false + # Geometric types + when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ + $1 + # Network address types + when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ + $1 + # Bit string types + when /\AB'(.*)'::"?bit(?: varying)?"?\z/ + $1 + # XML type + when /\A'(.*)'::xml\z/m + $1 + # Arrays + when /\A'(.*)'::"?\D+"?\[\]\z/ + $1 + # Hstore + when /\A'(.*)'::hstore\z/ + $1 + # JSON + when /\A'(.*)'::json\z/ + $1 + # Object identifier types + when /\A-?\d+\z/ + $1 else - kvs = object.scan(/(?<!\\)".*?(?<!\\)"/).map { |o| - unescape_hstore(o[1...-1]) - } - Hash[kvs.each_slice(2).to_a] - end + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + nil end + end - private - HSTORE_ESCAPE = { - ' ' => '\\ ', - '\\' => '\\\\', - '"' => '\\"', - '=' => '\\=', - } - HSTORE_ESCAPE_RE = Regexp.union(HSTORE_ESCAPE.keys) - HSTORE_UNESCAPE = HSTORE_ESCAPE.invert - HSTORE_UNESCAPE_RE = Regexp.union(HSTORE_UNESCAPE.keys) - - def unescape_hstore(value) - value.gsub(HSTORE_UNESCAPE_RE) do |match| - HSTORE_UNESCAPE[match] - end - end + def type_cast(value) + return if value.nil? + return super if encoded? - def escape_hstore(value) - value.gsub(HSTORE_ESCAPE_RE) do |match| - HSTORE_ESCAPE[match] - end - end + @oid_type.type_cast value end - # :startdoc: private + def extract_limit(sql_type) case sql_type when /^bigint/i; 8 when /^smallint/i; 2 + when /^timestamp/i; nil else super end end @@ -109,6 +147,8 @@ module ActiveRecord def extract_precision(sql_type) if sql_type == 'money' self.class.money_precision + elsif sql_type =~ /timestamp/i + $1.to_i if sql_type =~ /\((\d+)\)/ else super end @@ -125,6 +165,13 @@ module ActiveRecord :decimal when 'hstore' :hstore + # Network address types + when 'inet' + :inet + when 'cidr' + :cidr + when 'macaddr' + :macaddr # Character types when /^(?:character varying|bpchar)(?:\(\d+\))?$/ :string @@ -139,9 +186,6 @@ module ActiveRecord # Geometric types when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ :string - # Network address types - when /^(?:cidr|inet|macaddr)$/ - :string # Bit strings when /^bit(?: varying)?(?:\(\d+\))?$/ :string @@ -159,7 +203,10 @@ module ActiveRecord :integer # UUID type when 'uuid' - :string + :uuid + # JSON type + when 'json' + :json # Small and big integer types when /^(?:small|big)int$/ :integer @@ -168,64 +215,6 @@ module ActiveRecord super end end - - # Extracts the value from a PostgreSQL column default definition. - def self.extract_value_from_default(default) - # This is a performance optimization for Ruby 1.9.2 in development. - # If the value is nil, we return nil straight away without checking - # the regular expressions. If we check each regular expression, - # Regexp#=== will call NilClass#to_str, which will trigger - # method_missing (defined by whiny nil in ActiveSupport) which - # makes this method very very slow. - return default unless default - - case default - # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?)\z/ - $1 - # Character types - when /\A'(.*)'::(?:character varying|bpchar|text)\z/m - $1 - # Character types (8.1 formatting) - when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m - $1.gsub(/\\(\d\d\d)/) { $1.oct.chr } - # Binary data types - when /\A'(.*)'::bytea\z/m - $1 - # Date/time types - when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ - $1 - when /\A'(.*)'::interval\z/ - $1 - # Boolean type - when 'true' - true - when 'false' - false - # Geometric types - when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ - $1 - # Network address types - when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ - $1 - # Bit string types - when /\AB'(.*)'::"?bit(?: varying)?"?\z/ - $1 - # XML type - when /\A'(.*)'::xml\z/m - $1 - # Arrays - when /\A'(.*)'::"?\D+"?\[\]\z/ - $1 - # Object identifier types - when /\A-?\d+\z/ - $1 - else - # Anything else is blank, some user type, or some function - # and we can't know the value of that, so return nil. - nil - end - end end # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver. @@ -244,6 +233,8 @@ module ActiveRecord # <encoding></tt> call on the connection. # * <tt>:min_messages</tt> - An optional client min messages that is used in a # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. + # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT<tt> statements + # defaults to true. # # Any further options are used as connection parameters to libpq. See # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the @@ -266,27 +257,58 @@ module ActiveRecord def hstore(name, options = {}) column(name, 'hstore', options) end + + def inet(name, options = {}) + column(name, 'inet', options) + end + + def cidr(name, options = {}) + column(name, 'cidr', options) + end + + def macaddr(name, options = {}) + column(name, 'macaddr', options) + end + + def uuid(name, options = {}) + column(name, 'uuid', options) + end + + def json(name, options = {}) + column(name, 'json', options) + end end ADAPTER_NAME = 'PostgreSQL' NATIVE_DATABASE_TYPES = { - :primary_key => "serial primary key", - :string => { :name => "character varying", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "integer" }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "timestamp" }, - :timestamp => { :name => "timestamp" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "bytea" }, - :boolean => { :name => "boolean" }, - :xml => { :name => "xml" }, - :tsvector => { :name => "tsvector" } + primary_key: "serial primary key", + string: { name: "character varying", limit: 255 }, + text: { name: "text" }, + integer: { name: "integer" }, + float: { name: "float" }, + decimal: { name: "decimal" }, + datetime: { name: "timestamp" }, + timestamp: { name: "timestamp" }, + time: { name: "time" }, + date: { name: "date" }, + binary: { name: "bytea" }, + boolean: { name: "boolean" }, + xml: { name: "xml" }, + tsvector: { name: "tsvector" }, + hstore: { name: "hstore" }, + inet: { name: "inet" }, + cidr: { name: "cidr" }, + macaddr: { name: "macaddr" }, + uuid: { name: "uuid" }, + json: { name: "json" } } + include Quoting + include ReferentialIntegrity + include SchemaStatements + include DatabaseStatements + # Returns 'PostgreSQL' as adapter name for identification purposes. def adapter_name ADAPTER_NAME @@ -302,6 +324,10 @@ module ActiveRecord true end + def supports_partial_index? + true + end + class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) super @@ -339,26 +365,39 @@ module ActiveRecord end private - def cache - @cache[$$] - end - def dealloc(key) - @connection.query "DEALLOCATE #{key}" if connection_active? - end + def cache + @cache[Process.pid] + end - def connection_active? - @connection.status == PGconn::CONNECTION_OK - rescue PGError - false - end + def dealloc(key) + @connection.query "DEALLOCATE #{key}" if connection_active? + end + + def connection_active? + @connection.status == PGconn::CONNECTION_OK + rescue PGError + false + end + end + + class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc: + include Arel::Visitors::BindVisitor end # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) super(connection, logger) + + if config.fetch(:prepared_statements) { true } + @visitor = Arel::Visitors::PostgreSQL.new self + else + @visitor = BindSubstitution.new self + end + + connection_parameters.delete :prepared_statements + @connection_parameters, @config = connection_parameters, config - @visitor = Arel::Visitors::PostgreSQL.new self # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil @@ -372,7 +411,9 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end + initialize_type_map @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] + @use_insert_returning = @config.key?(:insert_returning) ? @config[:insert_returning] : true end # Clears the prepared statements cache. @@ -382,7 +423,8 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - @connection.status == PGconn::CONNECTION_OK + @connection.query 'SELECT 1' + true rescue PGError false end @@ -391,6 +433,7 @@ module ActiveRecord def reconnect! clear_cache! @connection.reset + @open_transactions = 0 configure_connection end @@ -448,101 +491,7 @@ module ActiveRecord # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length - @table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i - end - - # QUOTING ================================================== - - # Escapes binary strings for bytea input to the database. - def escape_bytea(value) - PGconn.escape_bytea(value) if value - end - - # Unescapes bytea output from a database to the binary string it represents. - # NOTE: This is NOT an inverse of escape_bytea! This is only to be used - # on escaped binary output from database drive. - def unescape_bytea(value) - PGconn.unescape_bytea(value) if value - end - - # Quotes PostgreSQL-specific data types for SQL input. - def quote(value, column = nil) #:nodoc: - return super unless column - - case value - when Float - return super unless value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" - when Numeric - return super unless column.sql_type == 'money' - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" - when String - case column.sql_type - when 'bytea' then "'#{escape_bytea(value)}'" - when 'xml' then "xml '#{quote_string(value)}'" - when /^bit/ - case value - when /^[01]*$/ then "B'#{value}'" # Bit-string notation - when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation - end - else - super - end - else - super - end - end - - def type_cast(value, column) - return super unless column - - case value - when String - return super unless 'bytea' == column.sql_type - { :value => value, :format => 1 } - else - super - end - end - - # Quotes strings for use in SQL input. - def quote_string(s) #:nodoc: - @connection.escape(s) - end - - # Checks the following cases: - # - # - table_name - # - "table.name" - # - schema_name.table_name - # - schema_name."table.name" - # - "schema.name".table_name - # - "schema.name"."table.name" - def quote_table_name(name) - schema, name_part = extract_pg_identifier_from_name(name.to_s) - - unless name_part - quote_column_name(schema) - else - table_name, name_part = extract_pg_identifier_from_name(name_part) - "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" - end - end - - # Quotes column names for use in SQL queries. - def quote_column_name(name) #:nodoc: - PGconn.quote_ident(name.to_s) - end - - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.acts_like?(:time) && value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end + @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i end # Set the authorized user for this session @@ -551,569 +500,6 @@ module ActiveRecord exec_query "SET SESSION AUTHORIZATION #{user}" end - # REFERENTIAL INTEGRITY ==================================== - - def supports_disable_referential_integrity? #:nodoc: - true - end - - def disable_referential_integrity #:nodoc: - if supports_disable_referential_integrity? then - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) - end - yield - ensure - if supports_disable_referential_integrity? then - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) - end - end - - # DATABASE STATEMENTS ====================================== - - def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # PostgreSQL shell: - # - # QUERY PLAN - # ------------------------------------------------------------------------------ - # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) - # Join Filter: (posts.user_id = users.id) - # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) - # Index Cond: (id = 1) - # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) - # Filter: (posts.user_id = 1) - # (6 rows) - # - def pp(result) - header = result.columns.first - lines = result.rows.map(&:first) - - # We add 2 because there's one char of padding at both sides, note - # the extra hyphens in the example above. - width = [header, *lines].map(&:length).max + 2 - - pp = [] - - pp << header.center(width).rstrip - pp << '-' * width - - pp += lines.map {|line| " #{line}"} - - nrows = result.rows.length - rows_label = nrows == 1 ? 'row' : 'rows' - pp << "(#{nrows} #{rows_label})" - - pp.join("\n") + "\n" - end - end - - # Executes a SELECT query and returns an array of rows. Each row is an - # array of field values. - def select_rows(sql, name = nil) - select_raw(sql, name).last - end - - # Executes an INSERT query and returns the new record's ID - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - - if pk - select_value("#{sql} RETURNING #{quote_column_name(pk)}") - else - super - end - end - alias :create :insert - - # create a 2D array representing the result set - def result_as_array(res) #:nodoc: - # check if we have any binary column and if they need escaping - ftypes = Array.new(res.nfields) do |i| - [i, res.ftype(i)] - end - - rows = res.values - return rows unless ftypes.any? { |_, x| - x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID - } - - typehash = ftypes.group_by { |_, type| type } - binaries = typehash[BYTEA_COLUMN_TYPE_OID] || [] - monies = typehash[MONEY_COLUMN_TYPE_OID] || [] - - rows.each do |row| - # unescape string passed BYTEA field (OID == 17) - binaries.each do |index, _| - row[index] = unescape_bytea(row[index]) - end - - # If this is a money type column and there are any currency symbols, - # then strip them off. Indeed it would be prettier to do this in - # PostgreSQLColumn.string_to_decimal but would break form input - # fields that call value_before_type_cast. - monies.each do |index, _| - data = row[index] - # Because money output is formatted according to the locale, there are two - # cases to consider (note the decimal separators): - # (1) $12,345,678.12 - # (2) $12.345.678,12 - case data - when /^-?\D+[\d,]+\.\d{2}$/ # (1) - data.gsub!(/[^-\d.]/, '') - when /^-?\D+[\d.]+,\d{2}$/ # (2) - data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') - end - end - end - end - - - # Queries the database and returns the results in an Array-like object - def query(sql, name = nil) #:nodoc: - log(sql, name) do - result_as_array @connection.async_exec(sql) - end - end - - # Executes an SQL statement, returning a PGresult object on success - # or raising a PGError exception otherwise. - def execute(sql, name = nil) - log(sql, name) do - @connection.async_exec(sql) - end - end - - def substitute_at(column, index) - Arel.sql("$#{index + 1}") - end - - def exec_query(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = binds.empty? ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - - ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) - result.clear - return ret - end - end - - def exec_delete(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = binds.empty? ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - affected = result.cmd_tuples - result.clear - affected - end - end - alias :exec_update :exec_delete - - def sql_for_insert(sql, pk, id_value, sequence_name, binds) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - - sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk - - [sql, binds] - end - - # Executes an UPDATE query and returns the number of affected tuples. - def update_sql(sql, name = nil) - super.cmd_tuples - end - - # Begins a transaction. - def begin_db_transaction - execute "BEGIN" - end - - # Commits a transaction. - def commit_db_transaction - execute "COMMIT" - end - - # Aborts a transaction. - def rollback_db_transaction - execute "ROLLBACK" - end - - def outside_transaction? - @connection.transaction_status == PGconn::PQTRANS_IDLE - end - - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") - end - - # SCHEMA STATEMENTS ======================================== - - # Drops the database specified on the +name+ attribute - # and creates it again using the provided +options+. - def recreate_database(name, options = {}) #:nodoc: - drop_database(name) - create_database(name, options) - end - - # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, - # <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses - # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). - # - # Example: - # create_database config[:database], config - # create_database 'foo_development', :encoding => 'unicode' - def create_database(name, options = {}) - options = options.reverse_merge(:encoding => "utf8") - - option_string = options.symbolize_keys.sum do |key, value| - case key - when :owner - " OWNER = \"#{value}\"" - when :template - " TEMPLATE = \"#{value}\"" - when :encoding - " ENCODING = '#{value}'" - when :tablespace - " TABLESPACE = \"#{value}\"" - when :connection_limit - " CONNECTION LIMIT = #{value}" - else - "" - end - end - - execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}" - end - - # Drops a PostgreSQL database. - # - # Example: - # drop_database 'matt_development' - def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" - end - - # Returns the list of all tables in the schema search path or a specified schema. - def tables(name = nil) - query(<<-SQL, 'SCHEMA').map { |row| row[0] } - SELECT tablename - FROM pg_tables - WHERE schemaname = ANY (current_schemas(false)) - SQL - end - - # Returns true if table exists. - # If the schema is not specified as part of +name+ then it will only find tables within - # the current schema search path (regardless of permissions to access tables in other schemas) - def table_exists?(name) - schema, table = Utils.extract_schema_and_table(name.to_s) - return false unless table - - binds = [[nil, table]] - binds << [nil, schema] if schema - - exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind in ('v','r') - AND c.relname = $1 - AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'} - SQL - end - - # Returns true if schema exists. - def schema_exists?(name) - exec_query(<<-SQL, 'SCHEMA', [[nil, name]]).rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_namespace - WHERE nspname = $1 - SQL - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) - result = query(<<-SQL, name) - SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid - FROM pg_class t - INNER JOIN pg_index d ON t.oid = d.indrelid - INNER JOIN pg_class i ON d.indexrelid = i.oid - WHERE i.relkind = 'i' - AND d.indisprimary = 'f' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) - ORDER BY i.relname - SQL - - - result.map do |row| - index_name = row[0] - unique = row[1] == 't' - indkey = row[2].split(" ") - inddef = row[3] - oid = row[4] - - columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")] - SELECT a.attnum, a.attname - FROM pg_attribute a - WHERE a.attrelid = #{oid} - AND a.attnum IN (#{indkey.join(",")}) - SQL - - column_names = columns.values_at(*indkey).compact - - # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - desc_order_columns = inddef.scan(/(\w+) DESC/).flatten - orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} - - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders) - end.compact - end - - # Returns the list of all column definitions for a table. - def columns(table_name) - # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).collect do |column_name, type, default, notnull| - PostgreSQLColumn.new(column_name, default, type, notnull == 'f') - end - end - - # Returns the current database name. - def current_database - query('select current_database()')[0][0] - end - - # Returns the current schema name. - def current_schema - query('SELECT current_schema', 'SCHEMA')[0][0] - end - - # Returns the current database encoding format. - def encoding - query(<<-end_sql)[0][0] - SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database - WHERE pg_database.datname LIKE '#{current_database}' - end_sql - end - - # Sets the schema search path to a string of comma-separated schema names. - # Names beginning with $ have to be quoted (e.g. $user => '$user'). - # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html - # - # This should be not be called manually but set in database.yml. - def schema_search_path=(schema_csv) - if schema_csv - execute "SET search_path TO #{schema_csv}" - @schema_search_path = schema_csv - end - end - - # Returns the active schema search path. - def schema_search_path - @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] - end - - # Returns the current client message level. - def client_min_messages - query('SHOW client_min_messages', 'SCHEMA')[0][0] - end - - # Set the client message level. - def client_min_messages=(level) - execute("SET client_min_messages TO '#{level}'", 'SCHEMA') - end - - # Returns the sequence name for a table's primary key or some other specified key. - def default_sequence_name(table_name, pk = nil) #:nodoc: - serial_sequence(table_name, pk || 'id').split('.').last - rescue ActiveRecord::StatementInvalid - "#{table_name}_#{pk || 'id'}_seq" - end - - def serial_sequence(table, column) - result = exec_query(<<-eosql, 'SCHEMA', [[nil, table], [nil, column]]) - SELECT pg_get_serial_sequence($1, $2) - eosql - result.rows.first.first - end - - # Resets the sequence of a table's primary key to the maximum value. - def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: - unless pk and sequence - default_pk, default_sequence = pk_and_sequence_for(table) - - pk ||= default_pk - sequence ||= default_sequence - end - - if @logger && pk && !sequence - @logger.warn "#{table} has primary key #{pk} with no default sequence" - end - - if pk && sequence - quoted_sequence = quote_table_name(sequence) - - select_value <<-end_sql, 'Reset sequence' - SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) - end_sql - end - end - - # Returns a table's primary key and belonging sequence. - def pk_and_sequence_for(table) #:nodoc: - # First try looking for a sequence with a dependency on the - # given table's primary key. - result = exec_query(<<-end_sql, 'SCHEMA').rows.first - SELECT attr.attname, ns.nspname, seq.relname - FROM pg_class seq - INNER JOIN pg_depend dep ON seq.oid = dep.objid - INNER JOIN pg_attribute attr ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] - INNER JOIN pg_namespace ns ON seq.relnamespace = ns.oid - WHERE seq.relkind = 'S' - AND cons.contype = 'p' - AND dep.refobjid = '#{quote_table_name(table)}'::regclass - end_sql - - # [primary_key, sequence] - if result.second == 'public' then - sequence = result.last - else - sequence = result.second+'.'+result.last - end - - [result.first, sequence] - rescue - nil - end - - # Returns just a table's primary key - def primary_key(table) - row = exec_query(<<-end_sql, 'SCHEMA', [[nil, table]]).rows.first - SELECT DISTINCT(attr.attname) - FROM pg_attribute attr - INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] - WHERE cons.contype = 'p' - AND dep.refobjid = $1::regclass - end_sql - - row && row.first - end - - # Renames a table. - # - # 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)}" - end - - # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. - def add_column(table_name, column_name, type, options = {}) - clear_cache! - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - - execute add_column_sql - end - - # Changes the column of a table. - def change_column(table_name, column_name, type, options = {}) - clear_cache! - quoted_table_name = quote_table_name(table_name) - - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - - change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) - change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) - end - - # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) - clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" - end - - def change_column_null(table_name, column_name, null, default = nil) - clear_cache! - unless null || default.nil? - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") - end - execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") - end - - # Renames a column in a table. - def rename_column(table_name, column_name, new_column_name) - clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" - end - - def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_table_name(index_name)}" - end - - def rename_index(table_name, old_name, new_name) - execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" - end - - def index_name_length - 63 - end - - # Maps logical Rails types to PostgreSQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - return super unless type.to_s == 'integer' - return 'integer' unless limit - - case limit - when 1, 2; 'smallint' - when 3, 4; 'integer' - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") - end - end - - # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. - # - # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and - # requires that the ORDER BY include the distinct column. - # - # distinct("posts.id", "posts.created_at desc") - def distinct(columns, orders) #:nodoc: - return "DISTINCT #{columns}" if orders.empty? - - # Construct a clean list of column names from the ORDER BY clause, removing - # any ASC/DESC modifiers - order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*/i, '') } - order_columns.delete_if { |c| c.blank? } - order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } - - "DISTINCT #{columns}, #{order_columns * ', '}" - end - module Utils extend self @@ -1133,17 +519,26 @@ module ActiveRecord end end + def use_insert_returning? + @use_insert_returning + end + protected + # Returns the version of the connected PostgreSQL server. def postgresql_version @connection.server_version end + # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html + FOREIGN_KEY_VIOLATION = "23503" + UNIQUE_VIOLATION = "23505" + def translate_exception(exception, message) - case exception.message - when /duplicate key value violates unique constraint/ + case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE) + when UNIQUE_VIOLATION RecordNotUnique.new(message, exception) - when /violates foreign key constraint/ + when FOREIGN_KEY_VIOLATION InvalidForeignKey.new(message, exception) else super @@ -1151,6 +546,23 @@ module ActiveRecord end private + + def initialize_type_map + result = execute('SELECT oid, typname, typelem, typdelim FROM pg_type', 'SCHEMA') + leaves, nodes = result.partition { |row| row['typelem'] == '0' } + + # populate the leaf nodes + leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| + OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] + end + + # populate composite types + nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| + vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] + OID::TYPE_MAP[row['oid'].to_i] = vector + end + end + FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: def exec_no_cache(sql, binds) @@ -1173,7 +585,11 @@ module ActiveRecord # prepared statements whose return value may have changed is # FEATURE_NOT_SUPPORTED. Check here for more details: # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 - code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + begin + code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + rescue + raise e + end if FEATURE_NOT_SUPPORTED == code @statements.delete sql_key(sql) retry @@ -1225,7 +641,7 @@ module ActiveRecord if @config[:encoding] @connection.set_client_encoding(@config[:encoding]) end - self.client_min_messages = @config[:min_messages] if @config[:min_messages] + self.client_min_messages = @config[:min_messages] || 'warning' self.schema_search_path = @config[:schema_search_path] || @config[:schema_order] # Use standard-conforming strings if available so we don't have to do the E'...' dance. @@ -1242,8 +658,15 @@ module ActiveRecord # Returns the current ID of a table's sequence. def last_insert_id(sequence_name) #:nodoc: - r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) - Integer(r.rows.first.first) + Integer(last_insert_id_value(sequence_name)) + end + + def last_insert_id_value(sequence_name) + last_insert_id_result(sequence_name).rows.first.first + end + + def last_insert_id_result(sequence_name) #:nodoc: + exec_query("SELECT currval('#{sequence_name}')", 'SQL') end # Executes a SELECT query and returns the results, performing any data type @@ -1280,7 +703,7 @@ module ActiveRecord # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: exec_query(<<-end_sql, 'SCHEMA').rows - SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull + SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 962718da56..aad1f9a7ef 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,26 +1,17 @@ module ActiveRecord module ConnectionAdapters class SchemaCache - attr_reader :columns, :columns_hash, :primary_keys, :tables - attr_reader :connection + attr_reader :columns, :columns_hash, :primary_keys, :tables, :version + attr_accessor :connection def initialize(conn) @connection = conn - @tables = {} - @columns = Hash.new do |h, table_name| - h[table_name] = conn.columns(table_name) - end - - @columns_hash = Hash.new do |h, table_name| - h[table_name] = Hash[columns[table_name].map { |col| - [col.name, col] - }] - end - - @primary_keys = Hash.new do |h, table_name| - h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil - end + @columns = {} + @columns_hash = {} + @primary_keys = {} + @tables = {} + prepare_default_proc end # A cached lookup for table existence. @@ -30,12 +21,22 @@ module ActiveRecord @tables[name] = connection.table_exists?(name) end + # Add internal cache for table with +table_name+. + def add(table_name) + if table_exists?(table_name) + @primary_keys[table_name] + @columns[table_name] + @columns_hash[table_name] + end + end + # Clears out internal caches def clear! @columns.clear @columns_hash.clear @primary_keys.clear @tables.clear + @version = nil end # Clear out internal caches for table with +table_name+. @@ -45,6 +46,37 @@ module ActiveRecord @primary_keys.delete table_name @tables.delete table_name end + + def marshal_dump + # if we get current version during initialization, it happens stack over flow. + @version = ActiveRecord::Migrator.current_version + [@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val| + self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h } + end + end + + def marshal_load(array) + @version, @columns, @columns_hash, @primary_keys, @tables = array + prepare_default_proc + end + + private + + def prepare_default_proc + @columns.default_proc = Proc.new do |h, table_name| + h[table_name] = connection.columns(table_name) + end + + @columns_hash.default_proc = Proc.new do |h, table_name| + h[table_name] = Hash[columns[table_name].map { |col| + [col.name, col] + }] + end + + @primary_keys.default_proc = Proc.new do |h, table_name| + h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index ee5d10859c..4fe0013f0f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,6 +1,8 @@ -require 'active_record/connection_adapters/sqlite_adapter' +require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/statement_pool' +require 'arel/visitors/bind_visitor' -gem 'sqlite3', '~> 1.3.5' +gem 'sqlite3', '~> 1.3.6' require 'sqlite3' module ActiveRecord @@ -19,10 +21,6 @@ module ActiveRecord config[:database] = File.expand_path(config[:database], Rails.root) end - unless 'sqlite3' == config[:adapter] - raise ArgumentError, 'adapter name should be "sqlite3"' - end - db = SQLite3::Database.new( config[:database], :results_as_hash => true @@ -35,7 +33,183 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Adapter < SQLiteAdapter # :nodoc: + class SQLite3Column < Column #:nodoc: + class << self + def binary_to_string(value) + if value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) + end + value + end + end + end + + # The SQLite3 adapter works SQLite 3.6.16 or newer + # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). + # + # Options: + # + # * <tt>:database</tt> - Path to the database file. + class SQLite3Adapter < AbstractAdapter + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split('.').map { |v| v.to_i } + end + + def <=>(version_string) + @version <=> version_string.split('.').map { |v| v.to_i } + end + end + + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max) + super + @cache = Hash.new { |h,pid| h[pid] = {} } + end + + def each(&block); cache.each(&block); end + def key?(key); cache.key?(key); end + def [](key); cache[key]; end + def length; cache.length; end + + def []=(sql, key) + while @max <= cache.size + dealloc(cache.shift.last[:stmt]) + end + cache[sql] = key + end + + def clear + cache.values.each do |hash| + dealloc hash[:stmt] + end + cache.clear + end + + private + def cache + @cache[$$] + end + + def dealloc(stmt) + stmt.close unless stmt.closed? + end + end + + class BindSubstitution < Arel::Visitors::SQLite # :nodoc: + include Arel::Visitors::BindVisitor + end + + def initialize(connection, logger, config) + super(connection, logger) + @statements = StatementPool.new(@connection, + config.fetch(:statement_limit) { 1000 }) + @config = config + + if config.fetch(:prepared_statements) { true } + @visitor = Arel::Visitors::SQLite.new self + else + @visitor = BindSubstitution.new self + end + end + + def adapter_name #:nodoc: + 'SQLite' + end + + # Returns true + def supports_ddl_transactions? + true + end + + # Returns true if SQLite version is '3.6.8' or greater, false otherwise. + def supports_savepoints? + sqlite_version >= '3.6.8' + end + + # Returns true, since this connection adapter supports prepared statement + # caching. + def supports_statement_cache? + true + end + + # Returns true, since this connection adapter supports migrations. + def supports_migrations? #:nodoc: + true + end + + # Returns true. + def supports_primary_key? #:nodoc: + true + end + + def requires_reloading? + true + end + + # Returns true + def supports_add_column? + true + end + + # Disconnects from the database if already connected. Otherwise, this + # method does nothing. + def disconnect! + super + clear_cache! + @connection.close rescue nil + end + + # Clears the prepared statements cache. + def clear_cache! + @statements.clear + end + + # Returns true + def supports_count_distinct? #:nodoc: + true + end + + # Returns true + def supports_autoincrement? #:nodoc: + true + end + + def supports_index_sort_order? + true + end + + def native_database_types #:nodoc: + { + :primary_key => default_primary_key_type, + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "integer" }, + :float => { :name => "float" }, + :decimal => { :name => "decimal" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "boolean" } + } + end + + # Returns the current database encoding format as a string, eg: 'UTF-8' + def encoding + @connection.encoding.to_s + end + + # Returns true. + def supports_explain? + true + end + + # QUOTING ================================================== + def quote(value, column = nil) if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) s = column.class.string_to_binary(value).unpack("H*")[0] @@ -45,10 +219,390 @@ module ActiveRecord end end - # Returns the current database encoding format as a string, eg: 'UTF-8' - def encoding - @connection.encoding.to_s + def quote_string(s) #:nodoc: + @connection.class.quote(s) + end + + def quote_column_name(name) #:nodoc: + %Q("#{name.to_s.gsub('"', '""')}") + end + + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. + def quoted_date(value) #:nodoc: + if value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + + def type_cast(value, column) # :nodoc: + return value.to_f if BigDecimal === value + return super unless String === value + return super unless column && value + + value = super + if column.type == :string && value.encoding == Encoding::ASCII_8BIT + logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger + value.encode! 'utf-8' + end + value + end + + # DATABASE STATEMENTS ====================================== + + def explain(arel, binds = []) + sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + end + + class ExplainPrettyPrinter + # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles + # the output of the SQLite shell: + # + # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) + # 0|1|1|SCAN TABLE posts (~100000 rows) + # + def pp(result) # :nodoc: + result.rows.map do |row| + row.join('|') + end.join("\n") + "\n" + end + end + + def exec_query(sql, name = nil, binds = []) + log(sql, name, binds) do + + # Don't cache statements without bind values + if binds.empty? + stmt = @connection.prepare(sql) + cols = stmt.columns + records = stmt.to_a + stmt.close + stmt = records + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + cols = cache[:cols] ||= stmt.columns + stmt.reset! + stmt.bind_params binds.map { |col, val| + type_cast(val, col) + } + end + + ActiveRecord::Result.new(cols, stmt.to_a) + end + end + + def exec_delete(sql, name = 'SQL', binds = []) + exec_query(sql, name, binds) + @connection.changes end + alias :exec_update :exec_delete + + def last_inserted_id(result) + @connection.last_insert_row_id + end + + def execute(sql, name = nil) #:nodoc: + log(sql, name) { @connection.execute(sql) } + end + + def update_sql(sql, name = nil) #:nodoc: + super + @connection.changes + end + + def delete_sql(sql, name = nil) #:nodoc: + sql += " WHERE 1=1" unless sql =~ /WHERE/i + super sql, name + end + + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + super + id_value || @connection.last_insert_row_id + end + alias :create :insert_sql + + def select_rows(sql, name = nil) + exec_query(sql, name).rows + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + + def begin_db_transaction #:nodoc: + log('begin transaction',nil) { @connection.transaction } + end + + def commit_db_transaction #:nodoc: + log('commit transaction',nil) { @connection.commit } + end + + def rollback_db_transaction #:nodoc: + log('rollback transaction',nil) { @connection.rollback } + end + + # SCHEMA STATEMENTS ======================================== + + def tables(name = nil, table_name = nil) #:nodoc: + sql = <<-SQL + SELECT name + FROM sqlite_master + WHERE type = 'table' AND NOT name = 'sqlite_sequence' + SQL + sql << " AND name = #{quote_table_name(table_name)}" if table_name + + exec_query(sql, 'SCHEMA').map do |row| + row['name'] + end + end + + def table_exists?(table_name) + table_name && tables(nil, table_name).any? + end + + # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. + def columns(table_name) #:nodoc: + table_structure(table_name).map do |field| + case field["dflt_value"] + when /^null$/i + field["dflt_value"] = nil + when /^'(.*)'$/m + field["dflt_value"] = $1.gsub("''", "'") + when /^"(.*)"$/m + field["dflt_value"] = $1.gsub('""', '"') + end + + SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) + end + end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) #:nodoc: + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row| + IndexDefinition.new( + table_name, + row['name'], + row['unique'] != 0, + exec_query("PRAGMA index_info('#{row['name']}')", "Columns for index #{row['name']} on #{table_name}").map { |col| + col['name'] + }) + end + end + + def primary_key(table_name) #:nodoc: + column = table_structure(table_name).find { |field| + field['pk'] == 1 + } + column && column['name'] + end + + def remove_index!(table_name, index_name) #:nodoc: + exec_query "DROP INDEX #{quote_column_name(index_name)}" + end + + # Renames a table. + # + # Example: + # rename_table('octopuses', 'octopi') + def rename_table(name, new_name) + exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + end + + # See: http://www.sqlite.org/lang_altertable.html + # SQLite has an additional restriction on the ALTER TABLE statement + def valid_alter_table_options( type, options) + type.to_sym != :primary_key + end + + def add_column(table_name, column_name, type, options = {}) #:nodoc: + if supports_add_column? && valid_alter_table_options( type, options ) + super(table_name, column_name, type, options) + else + alter_table(table_name) do |definition| + definition.column(column_name, type, options) + end + end + end + + def remove_column(table_name, *column_names) #:nodoc: + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? + column_names.each do |column_name| + alter_table(table_name) do |definition| + definition.columns.delete(definition[column_name]) + end + end + end + alias :remove_columns :remove_column + + def change_column_default(table_name, column_name, default) #:nodoc: + alter_table(table_name) do |definition| + definition[column_name].default = default + end + end + + def change_column_null(table_name, column_name, null, default = nil) + unless null || default.nil? + exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + alter_table(table_name) do |definition| + definition[column_name].null = null + end + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + alter_table(table_name) do |definition| + include_default = options_include_default?(options) + definition[column_name].instance_eval do + self.type = type + self.limit = options[:limit] if options.include?(:limit) + self.default = options[:default] if include_default + self.null = options[:null] if options.include?(:null) + self.precision = options[:precision] if options.include?(:precision) + self.scale = options[:scale] if options.include?(:scale) + end + end + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + unless columns(table_name).detect{|c| c.name == column_name.to_s } + raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" + end + alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) + end + + def empty_insert_statement_value + "VALUES(NULL)" + end + + protected + def select(sql, name = nil, binds = []) #:nodoc: + exec_query(sql, name, binds) + end + + def table_structure(table_name) + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash + raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? + structure + end + + def alter_table(table_name, options = {}) #:nodoc: + altered_table_name = "altered_#{table_name}" + caller = lambda {|definition| yield definition if block_given?} + + transaction do + move_table(table_name, altered_table_name, + options.merge(:temporary => true)) + move_table(altered_table_name, table_name, &caller) + end + end + + def move_table(from, to, options = {}, &block) #:nodoc: + copy_table(from, to, options, &block) + drop_table(from) + end + + def copy_table(from, to, options = {}) #:nodoc: + from_primary_key = primary_key(from) + options[:primary_key] = from_primary_key if from_primary_key != 'id' + unless options[:primary_key] + options[:id] = columns(from).detect{|c| c.name == 'id'}.present? && from_primary_key == 'id' + end + create_table(to, options) do |definition| + @definition = definition + columns(from).each do |column| + column_name = options[:rename] ? + (options[:rename][column.name] || + options[:rename][column.name.to_sym] || + column.name) : column.name + + @definition.column(column_name, column.type, + :limit => column.limit, :default => column.default, + :precision => column.precision, :scale => column.scale, + :null => column.null) + end + @definition.primary_key(from_primary_key) if from_primary_key + yield @definition if block_given? + end + + copy_table_indexes(from, to, options[:rename] || {}) + copy_table_contents(from, to, + @definition.columns.map {|column| column.name}, + options[:rename] || {}) + end + + def copy_table_indexes(from, to, rename = {}) #:nodoc: + indexes(from).each do |index| + name = index.name + if to == "altered_#{from}" + name = "temp_#{name}" + elsif from == "altered_#{to}" + name = name[5..-1] + end + + to_column_names = columns(to).map { |c| c.name } + columns = index.columns.map {|c| rename[c] || c }.select do |column| + to_column_names.include?(column) + end + + unless columns.empty? + # index name can't be the same + opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } + opts[:unique] = true if index.unique + add_index(to, columns, opts) + end + end + end + + def copy_table_contents(from, to, columns, rename = {}) #:nodoc: + column_mappings = Hash[columns.map {|name| [name, name]}] + rename.each { |a| column_mappings[a.last] = a.first } + from_columns = columns(from).collect {|col| col.name} + columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + quoted_columns = columns.map { |col| quote_column_name(col) } * ',' + + quoted_to = quote_table_name(to) + exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| + sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" + sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' + sql << ')' + exec_query sql + end + end + + def sqlite_version + @sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)')) + end + + def default_primary_key_type + if supports_autoincrement? + 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' + else + 'INTEGER PRIMARY KEY NOT NULL' + end + end + + def translate_exception(exception, message) + case exception.message + when /column(s)? .* (is|are) not unique/ + RecordNotUnique.new(message, exception) + else + super + end + end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb deleted file mode 100644 index 55eca48efe..0000000000 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ /dev/null @@ -1,554 +0,0 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_record/connection_adapters/statement_pool' -require 'active_support/core_ext/string/encoding' - -module ActiveRecord - module ConnectionAdapters #:nodoc: - class SQLiteColumn < Column #:nodoc: - class << self - def binary_to_string(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value - end - end - end - - # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby - # drivers (available both as gems and from http://rubyforge.org/projects/sqlite-ruby/). - # - # Options: - # - # * <tt>:database</tt> - Path to the database file. - class SQLiteAdapter < AbstractAdapter - class Version - include Comparable - - def initialize(version_string) - @version = version_string.split('.').map { |v| v.to_i } - end - - def <=>(version_string) - @version <=> version_string.split('.').map { |v| v.to_i } - end - end - - class StatementPool < ConnectionAdapters::StatementPool - def initialize(connection, max) - super - @cache = Hash.new { |h,pid| h[pid] = {} } - end - - def each(&block); cache.each(&block); end - def key?(key); cache.key?(key); end - def [](key); cache[key]; end - def length; cache.length; end - - def []=(sql, key) - while @max <= cache.size - dealloc(cache.shift.last[:stmt]) - end - cache[sql] = key - end - - def clear - cache.values.each do |hash| - dealloc hash[:stmt] - end - cache.clear - end - - private - def cache - @cache[$$] - end - - def dealloc(stmt) - stmt.close unless stmt.closed? - end - end - - def initialize(connection, logger, config) - super(connection, logger) - @statements = StatementPool.new(@connection, - config.fetch(:statement_limit) { 1000 }) - @config = config - @visitor = Arel::Visitors::SQLite.new self - end - - def adapter_name #:nodoc: - 'SQLite' - end - - # Returns true if SQLite version is '2.0.0' or greater, false otherwise. - def supports_ddl_transactions? - sqlite_version >= '2.0.0' - end - - # Returns true if SQLite version is '3.6.8' or greater, false otherwise. - def supports_savepoints? - sqlite_version >= '3.6.8' - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # Returns true, since this connection adapter supports migrations. - def supports_migrations? #:nodoc: - true - end - - # Returns true. - def supports_primary_key? #:nodoc: - true - end - - # Returns true. - def supports_explain? - true - end - - def requires_reloading? - true - end - - # Returns true if SQLite version is '3.1.6' or greater, false otherwise. - def supports_add_column? - sqlite_version >= '3.1.6' - end - - # Disconnects from the database if already connected. Otherwise, this - # method does nothing. - def disconnect! - super - clear_cache! - @connection.close rescue nil - end - - # Clears the prepared statements cache. - def clear_cache! - @statements.clear - end - - # Returns true if SQLite version is '3.2.6' or greater, false otherwise. - def supports_count_distinct? #:nodoc: - sqlite_version >= '3.2.6' - end - - # Returns true if SQLite version is '3.1.0' or greater, false otherwise. - def supports_autoincrement? #:nodoc: - sqlite_version >= '3.1.0' - end - - def supports_index_sort_order? - sqlite_version >= '3.3.0' - end - - def native_database_types #:nodoc: - { - :primary_key => default_primary_key_type, - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "integer" }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "boolean" } - } - end - - - # QUOTING ================================================== - - def quote_string(s) #:nodoc: - @connection.class.quote(s) - end - - def quote_column_name(name) #:nodoc: - %Q("#{name.to_s.gsub('"', '""')}") - end - - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value - - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - @logger.error "Binary data inserted for `string` type on column `#{column.name}`" - value.encode! 'utf-8' - end - value - end - - # DATABASE STATEMENTS ====================================== - - def explain(arel, binds = []) - sql = "EXPLAIN QUERY PLAN #{to_sql(arel)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter - # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles - # the output of the SQLite shell: - # - # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) - # 0|1|1|SCAN TABLE posts (~100000 rows) - # - def pp(result) # :nodoc: - result.rows.map do |row| - row.join('|') - end.join("\n") + "\n" - end - end - - def exec_query(sql, name = nil, binds = []) - log(sql, name, binds) do - - # Don't cache statements without bind values - if binds.empty? - stmt = @connection.prepare(sql) - cols = stmt.columns - records = stmt.to_a - stmt.close - stmt = records - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - cols = cache[:cols] ||= stmt.columns - stmt.reset! - stmt.bind_params binds.map { |col, val| - type_cast(val, col) - } - end - - ActiveRecord::Result.new(cols, stmt.to_a) - end - end - - def exec_delete(sql, name = 'SQL', binds = []) - exec_query(sql, name, binds) - @connection.changes - end - alias :exec_update :exec_delete - - def last_inserted_id(result) - @connection.last_insert_row_id - end - - def execute(sql, name = nil) #:nodoc: - log(sql, name) { @connection.execute(sql) } - end - - def update_sql(sql, name = nil) #:nodoc: - super - @connection.changes - end - - def delete_sql(sql, name = nil) #:nodoc: - sql += " WHERE 1=1" unless sql =~ /WHERE/i - super sql, name - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super - id_value || @connection.last_insert_row_id - end - alias :create :insert_sql - - def select_rows(sql, name = nil) - exec_query(sql, name).rows - end - - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") - end - - def begin_db_transaction #:nodoc: - log('begin transaction',nil) { @connection.transaction } - end - - def commit_db_transaction #:nodoc: - log('commit transaction',nil) { @connection.commit } - end - - def rollback_db_transaction #:nodoc: - log('rollback transaction',nil) { @connection.rollback } - end - - # SCHEMA STATEMENTS ======================================== - - def tables(name = 'SCHEMA', table_name = nil) #:nodoc: - sql = <<-SQL - SELECT name - FROM sqlite_master - WHERE type = 'table' AND NOT name = 'sqlite_sequence' - SQL - sql << " AND name = #{quote_table_name(table_name)}" if table_name - - exec_query(sql, name).map do |row| - row['name'] - end - end - - def table_exists?(name) - name && tables('SCHEMA', name).any? - end - - # Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+. - def columns(table_name) #:nodoc: - table_structure(table_name).map do |field| - case field["dflt_value"] - when /^null$/i - field["dflt_value"] = nil - when /^'(.*)'$/ - field["dflt_value"] = $1.gsub(/''/, "'") - when /^"(.*)"$/ - field["dflt_value"] = $1.gsub(/""/, '"') - end - - SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) - end - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| - IndexDefinition.new( - table_name, - row['name'], - row['unique'] != 0, - exec_query("PRAGMA index_info('#{row['name']}')").map { |col| - col['name'] - }) - end - end - - def primary_key(table_name) #:nodoc: - column = table_structure(table_name).find { |field| - field['pk'] == 1 - } - column && column['name'] - end - - def remove_index!(table_name, index_name) #:nodoc: - exec_query "DROP INDEX #{quote_column_name(index_name)}" - end - - # Renames a table. - # - # Example: - # rename_table('octopuses', 'octopi') - def rename_table(name, new_name) - exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" - end - - # See: http://www.sqlite.org/lang_altertable.html - # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_options( type, options) - type.to_sym != :primary_key - end - - def add_column(table_name, column_name, type, options = {}) #:nodoc: - if supports_add_column? && valid_alter_table_options( type, options ) - super(table_name, column_name, type, options) - else - alter_table(table_name) do |definition| - definition.column(column_name, type, options) - end - end - end - - def remove_column(table_name, *column_names) #:nodoc: - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? - column_names.flatten.each do |column_name| - alter_table(table_name) do |definition| - definition.columns.delete(definition[column_name]) - end - end - end - alias :remove_columns :remove_column - - def change_column_default(table_name, column_name, default) #:nodoc: - alter_table(table_name) do |definition| - definition[column_name].default = default - end - end - - def change_column_null(table_name, column_name, null, default = nil) - unless null || default.nil? - exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") - end - alter_table(table_name) do |definition| - definition[column_name].null = null - end - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - alter_table(table_name) do |definition| - include_default = options_include_default?(options) - definition[column_name].instance_eval do - self.type = type - self.limit = options[:limit] if options.include?(:limit) - self.default = options[:default] if include_default - self.null = options[:null] if options.include?(:null) - self.precision = options[:precision] if options.include?(:precision) - self.scale = options[:scale] if options.include?(:scale) - end - end - end - - def rename_column(table_name, column_name, new_column_name) #:nodoc: - unless columns(table_name).detect{|c| c.name == column_name.to_s } - raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" - end - alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) - end - - def empty_insert_statement_value - "VALUES(NULL)" - end - - protected - def select(sql, name = nil, binds = []) #:nodoc: - exec_query(sql, name, binds) - end - - def table_structure(table_name) - structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash - raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? - structure - end - - def alter_table(table_name, options = {}) #:nodoc: - altered_table_name = "altered_#{table_name}" - caller = lambda {|definition| yield definition if block_given?} - - transaction do - move_table(table_name, altered_table_name, - options.merge(:temporary => true)) - move_table(altered_table_name, table_name, &caller) - end - end - - def move_table(from, to, options = {}, &block) #:nodoc: - copy_table(from, to, options, &block) - drop_table(from) - end - - def copy_table(from, to, options = {}) #:nodoc: - options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s)) - create_table(to, options) do |definition| - @definition = definition - columns(from).each do |column| - column_name = options[:rename] ? - (options[:rename][column.name] || - options[:rename][column.name.to_sym] || - column.name) : column.name - - @definition.column(column_name, column.type, - :limit => column.limit, :default => column.default, - :precision => column.precision, :scale => column.scale, - :null => column.null) - end - @definition.primary_key(primary_key(from)) if primary_key(from) - yield @definition if block_given? - end - - copy_table_indexes(from, to, options[:rename] || {}) - copy_table_contents(from, to, - @definition.columns.map {|column| column.name}, - options[:rename] || {}) - end - - def copy_table_indexes(from, to, rename = {}) #:nodoc: - indexes(from).each do |index| - name = index.name - if to == "altered_#{from}" - name = "temp_#{name}" - elsif from == "altered_#{to}" - name = name[5..-1] - end - - to_column_names = columns(to).map { |c| c.name } - columns = index.columns.map {|c| rename[c] || c }.select do |column| - to_column_names.include?(column) - end - - unless columns.empty? - # index name can't be the same - opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } - opts[:unique] = true if index.unique - add_index(to, columns, opts) - end - end - end - - def copy_table_contents(from, to, columns, rename = {}) #:nodoc: - column_mappings = Hash[columns.map {|name| [name, name]}] - rename.each { |a| column_mappings[a.last] = a.first } - from_columns = columns(from).collect {|col| col.name} - columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} - quoted_columns = columns.map { |col| quote_column_name(col) } * ',' - - quoted_to = quote_table_name(to) - exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| - sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' - sql << ')' - exec_query sql - end - end - - def sqlite_version - @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)')) - end - - def default_primary_key_type - if supports_autoincrement? - 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' - else - 'INTEGER PRIMARY KEY NOT NULL' - end - end - - def translate_exception(exception, message) - case exception.message - when /column(s)? .* (is|are) not unique/ - RecordNotUnique.new(message, exception) - else - super - end - end - - end - end -end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index d7746826a9..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 @@ -90,7 +89,11 @@ module ActiveRecord connection_handler.remove_connection(klass) end + def clear_cache! # :nodoc: + connection.schema_cache.clear! + end + delegate :clear_active_connections!, :clear_reloadable_connections!, - :clear_all_connections!, :verify_active_connections!, :to => :connection_handler + :clear_all_connections!, :to => :connection_handler end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index e87fba550b..aad21b8e37 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,87 +1,90 @@ -require 'active_support/concern' +require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/object/duplicable' require 'thread' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + ## + # :singleton-method: + # + # Accepts a logger conforming to the interface of Log4r which is then + # passed on to any new database connections made and which can be + # retrieved on both a class and instance level by calling +logger+. + mattr_accessor :logger, instance_accessor: false + + ## + # :singleton-method: + # Contains the database configuration - as is typically stored in config/database.yml - + # as a Hash. + # + # For example, the following database.yml... + # + # development: + # adapter: sqlite3 + # database: db/development.sqlite3 + # + # production: + # adapter: sqlite3 + # database: db/production.sqlite3 + # + # ...would result in ActiveRecord::Base.configurations to look like this: + # + # { + # 'development' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/development.sqlite3' + # }, + # 'production' => { + # 'adapter' => 'sqlite3', + # 'database' => 'db/production.sqlite3' + # } + # } + mattr_accessor :configurations, instance_accessor: false + self.configurations = {} + + ## + # :singleton-method: + # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling + # dates and times from the database. This is set to :utc by default. + mattr_accessor :default_timezone, instance_accessor: false + self.default_timezone = :utc + + ## + # :singleton-method: + # Specifies the format to use when dumping the database schema with Rails' + # Rakefile. If :sql, the schema is dumped as (potentially database- + # specific) SQL statements. If :ruby, the schema is dumped as an + # ActiveRecord::Schema file which can be loaded into any database that + # supports migrations. Use :ruby if you want to have different database + # adapters for, e.g., your development and test environments. + mattr_accessor :schema_format, instance_accessor: false + self.schema_format = :ruby + + ## + # :singleton-method: + # Specify whether or not to use timestamps for migration versions + mattr_accessor :timestamped_migrations, instance_accessor: false + self.timestamped_migrations = true + + mattr_accessor :connection_handler, instance_accessor: false + self.connection_handler = ConnectionAdapters::ConnectionHandler.new + + mattr_accessor :dependent_restrict_raises, instance_accessor: false + self.dependent_restrict_raises = true + end + module Core extend ActiveSupport::Concern included do ## # :singleton-method: - # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, - # which is then passed on to any new database connections made and which can be retrieved on both - # a class and instance level by calling +logger+. - config_attribute :logger, :global => true - - ## - # :singleton-method: - # Contains the database configuration - as is typically stored in config/database.yml - - # as a Hash. - # - # For example, the following database.yml... - # - # development: - # adapter: sqlite3 - # database: db/development.sqlite3 - # - # production: - # adapter: sqlite3 - # database: db/production.sqlite3 - # - # ...would result in ActiveRecord::Base.configurations to look like this: - # - # { - # 'development' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/development.sqlite3' - # }, - # 'production' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/production.sqlite3' - # } - # } - config_attribute :configurations, :global => true - self.configurations = {} - - ## - # :singleton-method: - # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling - # dates and times from the database. This is set to :utc by default. - config_attribute :default_timezone, :global => true - self.default_timezone = :utc - - ## - # :singleton-method: - # Specifies the format to use when dumping the database schema with Rails' - # Rakefile. If :sql, the schema is dumped as (potentially database- - # specific) SQL statements. If :ruby, the schema is dumped as an - # ActiveRecord::Schema file which can be loaded into any database that - # supports migrations. Use :ruby if you want to have different database - # adapters for, e.g., your development and test environments. - config_attribute :schema_format, :global => true - self.schema_format = :ruby - - ## - # :singleton-method: - # Specify whether or not to use timestamps for migration versions - config_attribute :timestamped_migrations, :global => true - self.timestamped_migrations = true - - ## - # :singleton-method: # The connection handler config_attribute :connection_handler - self.connection_handler = ConnectionAdapters::ConnectionHandler.new - ## - # :singleton-method: - # Specifies wether or not has_many or has_one association option - # :dependent => :restrict raises an exception. If set to true, the - # ActiveRecord::DeleteRestrictionError exception will be raised - # along with a DEPRECATION WARNING. If set to false, an error would - # be added to the model instead. - config_attribute :dependent_restrict_raises, :global => true - self.dependent_restrict_raises = true + %w(logger configurations default_timezone schema_format timestamped_migrations).each do |name| + config_attribute name, global: true + end end module ClassMethods @@ -125,23 +128,29 @@ module ActiveRecord object.is_a?(self) end + # Returns an instance of <tt>Arel::Table</tt> loaded with the current table name. + # + # class Post < ActiveRecord::Base + # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0)) + # end def arel_table @arel_table ||= Arel::Table.new(table_name, arel_engine) end + # Returns the Arel engine. def arel_engine - @arel_engine ||= connection_handler.connection_pools[name] ? self : active_record_super.arel_engine + @arel_engine ||= connection_handler.retrieve_connection_pool(self) ? self : active_record_super.arel_engine end private def relation #:nodoc: - @relation ||= Relation.new(self, arel_table) + relation = Relation.new(self, arel_table) if finder_needs_type_condition? - @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) else - @relation + relation end end end @@ -164,7 +173,11 @@ 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.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 @@ -175,7 +188,7 @@ module ActiveRecord assign_attributes(attributes, options) if attributes yield self if block_given? - run_callbacks :initialize + run_callbacks :initialize unless _initialize_callbacks.empty? end # Initialize an empty model object from +coder+. +coder+ must contain @@ -190,6 +203,7 @@ module ActiveRecord # post.title # => 'hello world' def init_with(coder) @attributes = self.class.initialize_attributes(coder['attributes']) + @columns_hash = self.class.column_types.merge(coder['column_types'] || {}) init_internals @@ -201,15 +215,36 @@ module ActiveRecord self end + ## + # :method: clone + # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied. + # That means that modifying attributes of the clone will modify the original, since they will both point to the + # same attributes hash. If you need a copy of your attributes hash, please use the #dup method. + # + # user = User.first + # new_user = user.clone + # user.name # => "Bob" + # new_user.name = "Joe" + # user.name # => "Joe" + # + # user.object_id == new_user.object_id # => false + # user.name.object_id == new_user.name.object_id # => true + # + # user.name.object_id == user.dup.name.object_id # => false + + ## + # :method: dup # Duped objects have no id assigned and are treated as new records. Note # that this is a "shallow" copy as it copies the object's attributes # only, not its associations. The extent of a "deep" copy is application # specific and is therefore left to the application to implement according # to its need. # The dup method does not preserve the timestamps (created|updated)_(at|on). - def initialize_dup(other) + + ## + def initialize_dup(other) # :nodoc: cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - self.class.initialize_attributes(cloned_attributes) + self.class.initialize_attributes(cloned_attributes, :serialized => false) cloned_attributes.delete(self.class.primary_key) @@ -220,7 +255,7 @@ module ActiveRecord @changed_attributes = {} self.class.column_defaults.each do |attr, orig_value| - @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) + @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) end @aggregation_cache = {} @@ -245,7 +280,7 @@ module ActiveRecord # end # coder = {} # Post.new.encode_with(coder) - # coder # => { 'id' => nil, ... } + # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) coder['attributes'] = attributes end @@ -324,6 +359,11 @@ module ActiveRecord "#<#{self.class} #{inspection}>" end + # Returns a hash of the given methods with their names as keys and returned values as values. + def slice(*methods) + Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access + end + private # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements @@ -333,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 @@ -343,16 +383,17 @@ module ActiveRecord @attributes[pk] = nil unless @attributes.key?(pk) - @relation = nil - @aggregation_cache = {} - @association_cache = {} - @attributes_cache = {} - @previously_changed = {} - @changed_attributes = {} - @readonly = false - @destroyed = false - @marked_for_destruction = false - @new_record = true + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} + @previously_changed = {} + @changed_attributes = {} + @readonly = false + @destroyed = false + @marked_for_destruction = false + @new_record = true + @mass_assignment_options = nil + @_start_transaction_state = {} end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 224f5276eb..c877079b25 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -1,113 +1,115 @@ module ActiveRecord # = Active Record Counter Cache module CounterCache - # Resets one or more counter caches to their correct value using an SQL - # count query. This is useful when adding new counter caches, or if the - # counter has been corrupted or modified directly by SQL. - # - # ==== Parameters - # - # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more counter names to reset - # - # ==== Examples - # - # # For Post with id #1 records reset the comments_count - # Post.reset_counters(1, :comments) - def reset_counters(id, *counters) - object = find(id) - counters.each do |association| - has_many_association = reflect_on_association(association.to_sym) + extend ActiveSupport::Concern - foreign_key = has_many_association.foreign_key.to_s - child_class = has_many_association.klass - belongs_to = child_class.reflect_on_all_associations(:belongs_to) - reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key } - counter_name = reflection.counter_cache_column + module ClassMethods + # Resets one or more counter caches to their correct value using an SQL + # count query. This is useful when adding new counter caches, or if the + # counter has been corrupted or modified directly by SQL. + # + # ==== Parameters + # + # * +id+ - The id of the object you wish to reset a counter on. + # * +counters+ - One or more counter names to reset + # + # ==== Examples + # + # # For Post with id #1 records reset the comments_count + # Post.reset_counters(1, :comments) + def reset_counters(id, *counters) + object = find(id) + counters.each do |association| + has_many_association = reflect_on_association(association.to_sym) - stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(association).count - }) - connection.update stmt - end - return true - end + foreign_key = has_many_association.foreign_key.to_s + child_class = has_many_association.klass + belongs_to = child_class.reflect_on_all_associations(:belongs_to) + reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } + counter_name = reflection.counter_cache_column - # A generic "counter updater" implementation, intended primarily to be - # used by increment_counter and decrement_counter, but which may also - # be useful on its own. It simply does a direct SQL update for the record - # with the given ID, altering the given hash of counters by the amount - # given by the corresponding value: - # - # ==== Parameters - # - # * +id+ - The id of the object you wish to update a counter on or an Array of ids. - # * +counters+ - An Array of Hashes containing the names of the fields - # to update as keys and the amount to update the field by as values. - # - # ==== Examples - # - # # For the Post with id of 5, decrement the comment_count by 1, and - # # increment the action_count by 1 - # Post.update_counters 5, :comment_count => -1, :action_count => 1 - # # Executes the following SQL: - # # UPDATE posts - # # SET comment_count = COALESCE(comment_count, 0) - 1, - # # action_count = COALESCE(action_count, 0) + 1 - # # WHERE id = 5 - # - # # For the Posts with id of 10 and 15, increment the comment_count by 1 - # Post.update_counters [10, 15], :comment_count => 1 - # # Executes the following SQL: - # # UPDATE posts - # # SET comment_count = COALESCE(comment_count, 0) + 1 - # # WHERE id IN (10, 15) - def update_counters(id, counters) - updates = counters.map do |counter_name, value| - operator = value < 0 ? '-' : '+' - quoted_column = connection.quote_column_name(counter_name) - "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ + arel_table[counter_name] => object.send(association).count + }) + connection.update stmt + end + return true end - IdentityMap.remove_by_id(symbolized_base_class, id) if IdentityMap.enabled? + # A generic "counter updater" implementation, intended primarily to be + # used by increment_counter and decrement_counter, but which may also + # be useful on its own. It simply does a direct SQL update for the record + # with the given ID, altering the given hash of counters by the amount + # given by the corresponding value: + # + # ==== Parameters + # + # * +id+ - The id of the object you wish to update a counter on or an Array of ids. + # * +counters+ - An Array of Hashes containing the names of the fields + # to update as keys and the amount to update the field by as values. + # + # ==== Examples + # + # # For the Post with id of 5, decrement the comment_count by 1, and + # # increment the action_count by 1 + # Post.update_counters 5, :comment_count => -1, :action_count => 1 + # # Executes the following SQL: + # # UPDATE posts + # # SET comment_count = COALESCE(comment_count, 0) - 1, + # # action_count = COALESCE(action_count, 0) + 1 + # # WHERE id = 5 + # + # # For the Posts with id of 10 and 15, increment the comment_count by 1 + # Post.update_counters [10, 15], :comment_count => 1 + # # Executes the following SQL: + # # UPDATE posts + # # SET comment_count = COALESCE(comment_count, 0) + 1 + # # WHERE id IN (10, 15) + def update_counters(id, counters) + updates = counters.map do |counter_name, value| + operator = value < 0 ? '-' : '+' + quoted_column = connection.quote_column_name(counter_name) + "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + end - update_all(updates.join(', '), primary_key => id ) - end + where(primary_key => id).update_all updates.join(', ') + end - # Increment a number field by one, usually representing a count. - # - # This is used for caching aggregate values, so that they don't need to be computed every time. - # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is - # shown it would have to run an SQL query to find how many posts and comments there are. - # - # ==== Parameters - # - # * +counter_name+ - The name of the field that should be incremented. - # * +id+ - The id of the object that should be incremented. - # - # ==== Examples - # - # # Increment the post_count column for the record with an id of 5 - # DiscussionBoard.increment_counter(:post_count, 5) - def increment_counter(counter_name, id) - update_counters(id, counter_name => 1) - end + # Increment a number field by one, usually representing a count. + # + # This is used for caching aggregate values, so that they don't need to be computed every time. + # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is + # shown it would have to run an SQL query to find how many posts and comments there are. + # + # ==== Parameters + # + # * +counter_name+ - The name of the field that should be incremented. + # * +id+ - The id of the object that should be incremented. + # + # ==== Examples + # + # # Increment the post_count column for the record with an id of 5 + # DiscussionBoard.increment_counter(:post_count, 5) + def increment_counter(counter_name, id) + update_counters(id, counter_name => 1) + end - # Decrement a number field by one, usually representing a count. - # - # This works the same as increment_counter but reduces the column value by 1 instead of increasing it. - # - # ==== Parameters - # - # * +counter_name+ - The name of the field that should be decremented. - # * +id+ - The id of the object that should be decremented. - # - # ==== Examples - # - # # Decrement the post_count column for the record with an id of 5 - # DiscussionBoard.decrement_counter(:post_count, 5) - def decrement_counter(counter_name, id) - update_counters(id, counter_name => -1) + # Decrement a number field by one, usually representing a count. + # + # This works the same as increment_counter but reduces the column value by 1 instead of increasing it. + # + # ==== Parameters + # + # * +counter_name+ - The name of the field that should be decremented. + # * +id+ - The id of the object that should be decremented. + # + # ==== Examples + # + # # Decrement the post_count column for the record with an id of 5 + # DiscussionBoard.decrement_counter(:post_count, 5) + def decrement_counter(counter_name, id) + update_counters(id, counter_name => -1) + end end end end diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb deleted file mode 100644 index 38dbbef5fc..0000000000 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ /dev/null @@ -1,84 +0,0 @@ -module ActiveRecord - - # = Active Record Dynamic Finder Match - # - # Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info - # - class DynamicFinderMatch - def self.match(method) - method = method.to_s - klass = [FindBy, FindByBang, FindOrInitializeCreateBy].find do |_klass| - _klass.matches?(method) - end - klass.new(method) if klass - end - - def self.matches?(method) - method =~ self::METHOD_PATTERN - end - - def initialize(method) - @finder = :first - @instantiator = nil - match_data = method.match(self.class::METHOD_PATTERN) - @attribute_names = match_data[-1].split("_and_") - initialize_from_match_data(match_data) - end - - attr_reader :finder, :attribute_names, :instantiator - - def finder? - @finder && !@instantiator - end - - def creator? - @finder == :first && @instantiator == :create - end - - def instantiator? - @instantiator - end - - def bang? - false - end - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - - private - - def initialize_from_match_data(match_data) - end - end - - class FindBy < DynamicFinderMatch - METHOD_PATTERN = /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @finder = :last if match_data[1] == 'last_' - @finder = :all if match_data[1] == 'all_' - end - end - - class FindByBang < DynamicFinderMatch - METHOD_PATTERN = /^find_by_([_a-zA-Z]\w*)\!$/ - - def bang? - true - end - end - - class FindOrInitializeCreateBy < DynamicFinderMatch - METHOD_PATTERN = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @instantiator = match_data[1] == 'initialize' ? :new : :create - end - - def valid_arguments?(arguments) - arguments.size == 1 && arguments.first.is_a?(Hash) || super - end - end -end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 60ce3dd4f1..3bac31c6aa 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,79 +1,131 @@ module ActiveRecord - module DynamicMatchers - def respond_to?(method_id, include_private = false) - if match = DynamicFinderMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - elsif match = DynamicScopeMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - end + 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 activerecord-deprecated_finders + # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5), + # then we can remove the indirection. - super + def respond_to?(name, include_private = false) + match = Method.match(self, name) + match && match.valid? || super end private - # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and - # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders - # section at the top of this file for more detailed information. - # - # It's even possible to use all the additional parameters to +find+. For example, the - # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. - # - # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it - # is first invoked, so that future attempts to use it do not run through method_missing. - def method_missing(method_id, *arguments, &block) - if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id)) - attribute_names = match.attribute_names - super unless all_attributes_exists?(attribute_names) - unless match.valid_arguments?(arguments) - method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'" - backtrace = [method_trace] + caller - raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace - end - if match.respond_to?(:scope?) && match.scope? - self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) - attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)] - # - scoped(:conditions => attributes) # scoped(:conditions => attributes) - end # end - METHOD - send(method_id, *arguments) - elsif match.finder? - options = arguments.extract_options! - relation = options.any? ? scoped(options) : scoped - relation.send :find_by_attributes, match, attribute_names, *arguments, &block - elsif match.instantiator? - scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block - end + def method_missing(name, *arguments, &block) + match = Method.match(self, name) + + if match && match.valid? + match.define + send(name, *arguments, &block) else super end end - # Similar in purpose to +expand_hash_conditions_for_aggregates+. - def expand_attribute_names_for_aggregates(attribute_names) - attribute_names.map { |attribute_name| - unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? - aggregate_mapping(aggregation).map do |field_attr, _| - field_attr.to_sym - end - else - attribute_name.to_sym + class Method + @matchers = [] + + class << self + attr_reader :matchers + + def match(model, name) + klass = matchers.find { |k| name =~ k.pattern } + klass.new(model, name) if klass + end + + def pattern + /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/ + end + + def prefix + raise NotImplementedError end - }.flatten + + def suffix + '' + end + end + + attr_reader :model, :name, :attribute_names + + def initialize(model, name) + @model = model + @name = name.to_s + @attribute_names = @name.match(self.class.pattern)[1].split('_and_') + @attribute_names.map! { |n| @model.attribute_aliases[n] || n } + end + + def valid? + attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } + end + + def define + model.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def self.#{name}(#{signature}) + #{body} + end + CODE + end + + def body + raise NotImplementedError + end end - def all_attributes_exists?(attribute_names) - (expand_attribute_names_for_aggregates(attribute_names) - - column_methods_hash.keys).empty? + module Finder + # Extended in activerecord-deprecated_finders + def body + result + end + + # Extended in activerecord-deprecated_finders + def result + "#{finder}(#{attributes_hash})" + end + + # Extended in activerecord-deprecated_finders + def signature + attribute_names.join(', ') + end + + def attributes_hash + "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}" + end + + def finder + raise NotImplementedError + end end - def aggregate_mapping(reflection) - mapping = reflection.options[:mapping] || [reflection.name, reflection.name] - mapping.first.is_a?(Array) ? mapping : [mapping] + class FindBy < Method + Method.matchers << self + include Finder + + def self.prefix + "find_by" + end + + def finder + "find_by" + end end + class FindByBang < Method + Method.matchers << self + include Finder + + def self.prefix + "find_by" + end + + def self.suffix + "!" + end + def finder + "find_by!" + end + end end end diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb deleted file mode 100644 index a502155aac..0000000000 --- a/activerecord/lib/active_record/dynamic_scope_match.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActiveRecord - - # = Active Record Dynamic Scope Match - # - # Provides dynamic attribute-based scopes such as <tt>scoped_by_price(4.99)</tt> - # if, for example, the <tt>Product</tt> has an attribute with that name. You can - # chain more <tt>scoped_by_* </tt> methods after the other. It acts like a named - # scope except that it's dynamic. - class DynamicScopeMatch - def self.match(method) - return unless method.to_s =~ /^scoped_by_([_a-zA-Z]\w*)$/ - new(true, $1 && $1.split('_and_')) - end - - def initialize(scope, attribute_names) - @scope = scope - @attribute_names = attribute_names - end - - attr_reader :scope, :attribute_names - alias :scope? :scope - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - end -end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index fc80f3081e..5f157fde6d 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -53,6 +53,10 @@ module ActiveRecord class RecordNotSaved < ActiveRecordError end + # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false. + class RecordNotDestroyed < ActiveRecordError + end + # Raised when SQL statement cannot be executed by the database (for example, it's often the case for # MySQL when Ruby driver used is too old). class StatementInvalid < ActiveRecordError @@ -102,13 +106,11 @@ module ActiveRecord attr_reader :record, :attempted_action def initialize(record, attempted_action) + super("Attempted to #{attempted_action} a stale object: #{record.class.name}") @record = record @attempted_action = attempted_action end - def message - "Attempted to #{attempted_action} a stale object: #{record.class.name}" - end end # Raised when association is being configured improperly or @@ -164,9 +166,9 @@ module ActiveRecord class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute def initialize(message, exception, attribute) + super(message) @exception = exception @attribute = attribute - @message = message end end @@ -185,11 +187,12 @@ module ActiveRecord attr_reader :model def initialize(model) + super("Unknown primary key for table #{model.table_name} in model #{model}.") @model = model end - def message - "Unknown primary key for table #{model.table_name} in model #{model}." - end + end + + class ImmutableRelation < ActiveRecordError end end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 01cacf6153..9e0390bed1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,12 +1,12 @@ -require 'active_support/core_ext/class/attribute' +require 'active_support/lazy_load_hooks' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false + end + module Explain - def self.extended(base) - # If a query takes longer than these many seconds we log its query plan - # automatically. nil disables this feature. - base.config_attribute :auto_explain_threshold_in_seconds, :global => true - end + delegate :auto_explain_threshold_in_seconds, :auto_explain_threshold_in_seconds=, to: 'ActiveRecord::Model' # If auto explain is enabled, this method triggers EXPLAIN logging for the # queries triggered by the block if it takes more than the threshold as a @@ -52,7 +52,7 @@ module ActiveRecord # Makes the adapter execute EXPLAIN for the tuples of queries and bindings. # Returns a formatted string ready to be logged. def exec_explain(queries) # :nodoc: - queries && queries.map do |sql, bind| + str = queries && queries.map do |sql, bind| [].tap do |msg| msg << "EXPLAIN for: #{sql}" unless bind.empty? @@ -62,6 +62,12 @@ module ActiveRecord msg << connection.explain(sql, bind) end.join("\n") end.join("\n") + + # Overriding inspect to be more human readable, specially in the console. + def str.inspect + self + end + str end # Silences automatic EXPLAIN logging for the duration of the block. @@ -70,7 +76,7 @@ module ActiveRecord # the threshold is set to 0. # # As the name of the method suggests this only applies to automatic - # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run. + # EXPLAINs, manual calls to <tt>ActiveRecord::Relation#explain</tt> run. def silence_auto_explain current = Thread.current original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 1f8c4fc203..d5ba343b4c 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -2,9 +2,12 @@ require 'active_support/notifications' module ActiveRecord class ExplainSubscriber # :nodoc: - def call(*args) + def start(name, id, payload) + # unused + end + + def finish(name, id, payload) if queries = Thread.current[:available_queries_for_explain] - payload = args.last queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload) end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b82d5b5621..b1db5f6f9f 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -2,18 +2,13 @@ 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' -if defined? ActiveRecord +module ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: end -else - class FixtureClassNotFound < StandardError #:nodoc: - end -end -module ActiveRecord # \Fixtures are a way of organizing data that you want to test against; in short, sample data. # # They are stored in YAML files, one file per model, which are placed in the directory @@ -87,7 +82,7 @@ module ActiveRecord # end # # test "find_alt_method_2" do - # assert_equal "Ruby on Rails", @rubyonrails.news + # assert_equal "Ruby on Rails", @rubyonrails.name # end # # In order to use these methods to access fixtured data within your testcases, you must specify one of the @@ -376,19 +371,23 @@ module ActiveRecord # # Any fixture labeled "DEFAULTS" is safely ignored. class Fixtures + #-- + # NOTE: an instance of Fixtures can be called fixture_set, it is normally stored in a single YAML file and possibly in a folder with the same name. + #++ MAX_ID = 2 ** 30 - 1 @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.default_fixture_model_name(fixture_name) # :nodoc: + def self.default_fixture_model_name(fixture_set_name) # :nodoc: ActiveRecord::Base.pluralize_table_names ? - fixture_name.singularize.camelize : - fixture_name.camelize + fixture_set_name.singularize.camelize : + fixture_set_name.camelize end - def self.default_fixture_table_name(fixture_name) # :nodoc: - "#{ActiveRecord::Base.table_name_prefix}"\ - "#{fixture_name.tr('/', '_')}#{ActiveRecord::Base.table_name_suffix}".to_sym + def self.default_fixture_table_name(fixture_set_name) # :nodoc: + "#{ ActiveRecord::Base.table_name_prefix }"\ + "#{ fixture_set_name.tr('/', '_') }"\ + "#{ ActiveRecord::Base.table_name_suffix }".to_sym end def self.reset_cache @@ -415,11 +414,11 @@ module ActiveRecord cache_for_connection(connection).update(fixtures_map) end - def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true) + def self.instantiate_fixtures(object, fixture_set, load_instances = true) if load_instances - fixtures.each do |name, fixture| + fixture_set.each do |fixture_name, fixture| begin - object.instance_variable_set "@#{name}", fixture.find + object.instance_variable_set "@#{fixture_name}", fixture.find rescue FixtureClassNotFound nil end @@ -428,61 +427,59 @@ module ActiveRecord end def self.instantiate_all_loaded_fixtures(object, load_instances = true) - all_loaded_fixtures.each do |table_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances) + all_loaded_fixtures.each_value do |fixture_set| + instantiate_fixtures(object, fixture_set, load_instances) end end cattr_accessor :all_loaded_fixtures self.all_loaded_fixtures = {} - def self.create_fixtures(fixtures_directory, table_names, class_names = {}) - table_names = Array(table_names).map(&:to_s) + def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}) + fixture_set_names = Array(fixture_set_names).map(&:to_s) class_names = class_names.stringify_keys # FIXME: Apparently JK uses this. connection = block_given? ? yield : ActiveRecord::Base.connection - files_to_read = table_names.reject { |table_name| - fixture_is_cached?(connection, table_name) + files_to_read = fixture_set_names.reject { |fs_name| + fixture_is_cached?(connection, fs_name) } unless files_to_read.empty? connection.disable_referential_integrity do fixtures_map = {} - fixture_files = files_to_read.map do |path| - fixture_name = path - - fixtures_map[fixture_name] = new( # ActiveRecord::Fixtures.new + fixture_sets = files_to_read.map do |fs_name| + fixtures_map[fs_name] = new( # ActiveRecord::Fixtures.new connection, - fixture_name, - class_names[fixture_name.to_s] || default_fixture_model_name(fixture_name), - ::File.join(fixtures_directory, path)) + fs_name, + class_names[fs_name] || default_fixture_model_name(fs_name), + ::File.join(fixtures_directory, fs_name)) end all_loaded_fixtures.update(fixtures_map) connection.transaction(:requires_new => true) do - fixture_files.each do |ff| - conn = ff.model_class.respond_to?(:connection) ? ff.model_class.connection : connection - table_rows = ff.table_rows + fixture_sets.each do |fs| + conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection + table_rows = fs.table_rows table_rows.keys.each do |table| conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' end - table_rows.each do |table_name,rows| + table_rows.each do |fixture_set_name, rows| rows.each do |row| - conn.insert_fixture(row, table_name) + conn.insert_fixture(row, fixture_set_name) end end end # Cap primary key sequences to max(pk). if connection.respond_to?(:reset_pk_sequence!) - fixture_files.each do |ff| - connection.reset_pk_sequence!(ff.table_name) + fixture_sets.each do |fs| + connection.reset_pk_sequence!(fs.table_name) end end end @@ -490,7 +487,7 @@ module ActiveRecord cache_fixtures(connection, fixtures_map) end end - cached_fixtures(connection, table_names) + cached_fixtures(connection, fixture_set_names) end # Returns a consistent, platform-independent identifier for +label+. @@ -501,26 +498,23 @@ module ActiveRecord attr_reader :table_name, :name, :fixtures, :model_class - def initialize(connection, fixture_name, class_name, fixture_path) - @connection = connection - @fixture_path = fixture_path - @name = fixture_name - @class_name = class_name + def initialize(connection, name, class_name, path) + @fixtures = {} # Ordered hash + @name = name + @path = path - @fixtures = {} - - # Should be an AR::Base type class - if class_name.is_a?(Class) + if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name else @model_class = class_name.constantize rescue nil end - @connection = model_class.connection if model_class && model_class.respond_to?(:connection) + @connection = ( model_class.respond_to?(:connection) ? + model_class.connection : connection ) @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : - self.class.default_fixture_table_name(fixture_name) ) + self.class.default_fixture_table_name(name) ) read_fixture_files end @@ -559,8 +553,8 @@ module ActiveRecord if model_class && model_class < ActiveRecord::Model # fill in timestamp columns if they aren't specified and the model is set to record_timestamps if model_class.record_timestamps - timestamp_column_names.each do |name| - row[name] = now unless row.key?(name) + timestamp_column_names.each do |c_name| + row[c_name] = now unless row.key?(c_name) end end @@ -599,7 +593,7 @@ module ActiveRecord when :has_and_belongs_to_many if (targets = row.delete(association.name.to_s)) targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - table_name = association.options[:join_table] + table_name = association.join_table rows[table_name].concat targets.map { |target| { association.foreign_key => row[primary_key_name], association.association_foreign_key => ActiveRecord::Fixtures.identify(target) } @@ -638,26 +632,23 @@ module ActiveRecord end def read_fixture_files - yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f| + yaml_files = Dir["#{@path}/**/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path] yaml_files.each do |file| Fixtures::File.open(file) do |fh| - fh.each do |name, row| - fixtures[name] = ActiveRecord::Fixture.new(row, model_class) + fh.each do |fixture_name, row| + fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end end end end def yaml_file_path - "#{@fixture_path}.yml" + "#{@path}.yml" end - def yaml_fixtures_key(path) - ::File.basename(@fixture_path).split(".").first - end end class Fixture #:nodoc: @@ -712,7 +703,7 @@ module ActiveRecord class_attribute :fixture_table_names class_attribute :fixture_class_names class_attribute :use_transactional_fixtures - class_attribute :use_instantiated_fixtures # true, false, or :no_instances + class_attribute :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures self.fixture_table_names = [] @@ -720,9 +711,8 @@ module ActiveRecord self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false - self.fixture_class_names = Hash.new do |h, fixture_name| - fixture_name = fixture_name.to_s - h[fixture_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_name) + self.fixture_class_names = Hash.new do |h, fixture_set_name| + h[fixture_set_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_set_name) end end @@ -746,18 +736,18 @@ module ActiveRecord self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) end - def fixtures(*fixture_names) - if fixture_names.first == :all - fixture_names = Dir["#{fixture_path}/**/*.yml"].map { |f| + def fixtures(*fixture_set_names) + if fixture_set_names.first == :all + fixture_set_names = Dir["#{fixture_path}/**/*.yml"].map { |f| File.basename f, '.yml' } else - fixture_names = fixture_names.flatten.map { |n| n.to_s } + fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } end - self.fixture_table_names |= fixture_names - require_fixture_classes(fixture_names) - setup_fixture_accessors(fixture_names) + self.fixture_table_names |= fixture_set_names + require_fixture_classes(fixture_set_names) + setup_fixture_accessors(fixture_set_names) end def try_to_load_dependency(file_name) @@ -772,35 +762,39 @@ module ActiveRecord end end - def require_fixture_classes(fixture_names = nil) - (fixture_names || fixture_table_names).each do |fixture_name| - file_name = fixture_name.to_s + def require_fixture_classes(fixture_set_names = nil) + if fixture_set_names + fixture_set_names = fixture_set_names.map { |n| n.to_s } + else + fixture_set_names = fixture_table_names + end + + fixture_set_names.each do |file_name| file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names try_to_load_dependency(file_name) end end - def setup_fixture_accessors(fixture_names = nil) - fixture_names = Array(fixture_names || fixture_table_names) + def setup_fixture_accessors(fixture_set_names = nil) + fixture_set_names = Array(fixture_set_names || fixture_table_names) methods = Module.new do - fixture_names.each do |fixture_name| - fixture_name = fixture_name.to_s - accessor_name = fixture_name.tr('/', '_').to_sym + fixture_set_names.each do |fs_name| + fs_name = fs_name.to_s + accessor_name = fs_name.tr('/', '_').to_sym - define_method(accessor_name) do |*fixtures| - force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload + define_method(accessor_name) do |*fixture_names| + force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload - @fixture_cache[fixture_name] ||= {} + @fixture_cache[fs_name] ||= {} - instances = fixtures.map do |fixture| - @fixture_cache[fixture_name].delete(fixture) if force_reload + instances = fixture_names.map do |f_name| + f_name = f_name.to_s + @fixture_cache[fs_name].delete(f_name) if force_reload - if @loaded_fixtures[fixture_name][fixture.to_s] - ActiveRecord::IdentityMap.without do - @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find - end + if @loaded_fixtures[fs_name][f_name] + @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find else - raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'" + raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'" end end @@ -829,7 +823,7 @@ module ActiveRecord end def setup_fixtures - return unless !ActiveRecord::Base.configurations.blank? + return if ActiveRecord::Base.configurations.blank? if pre_loaded_fixtures && !use_transactional_fixtures raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' @@ -885,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 @@ -907,8 +901,8 @@ module ActiveRecord ActiveRecord::Fixtures.instantiate_all_loaded_fixtures(self, load_instances?) else raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? - @loaded_fixtures.each do |fixture_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?) + @loaded_fixtures.each_value do |fixture_set| + ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_set, load_instances?) end end end diff --git a/activerecord/lib/active_record/fixtures/file.rb b/activerecord/lib/active_record/fixtures/file.rb index 6547791144..a9cabf5a7b 100644 --- a/activerecord/lib/active_record/fixtures/file.rb +++ b/activerecord/lib/active_record/fixtures/file.rb @@ -24,37 +24,33 @@ module ActiveRecord rows.each(&block) end - RESCUE_ERRORS = [ ArgumentError ] # :nodoc: + RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] # :nodoc: private - if defined?(Psych) && defined?(Psych::SyntaxError) - RESCUE_ERRORS << Psych::SyntaxError - end - - def rows - return @rows if @rows + def rows + return @rows if @rows + + begin + data = YAML.load(render(IO.read(@file))) + rescue *RESCUE_ERRORS => error + raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace + end + @rows = data ? validate(data).to_a : [] + end - begin - data = YAML.load(render(IO.read(@file))) - rescue *RESCUE_ERRORS => error - raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace + def render(content) + ERB.new(content).result end - @rows = data ? validate(data).to_a : [] - end - def render(content) - ERB.new(content).result - end + # Validate our unmarshalled data. + def validate(data) + unless Hash === data || YAML::Omap === data + raise Fixture::FormatError, 'fixture is not a hash' + end - # Validate our unmarshalled data. - def validate(data) - unless Hash === data || YAML::Omap === data - raise Fixture::FormatError, 'fixture is not a hash' + raise Fixture::FormatError unless data.all? { |name, row| Hash === row } + data end - - raise Fixture::FormatError unless data.all? { |name, row| Hash === row } - data - end end end end diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb deleted file mode 100644 index d9777bb2f6..0000000000 --- a/activerecord/lib/active_record/identity_map.rb +++ /dev/null @@ -1,144 +0,0 @@ -module ActiveRecord - # = Active Record Identity Map - # - # Ensures that each object gets loaded only once by keeping every loaded - # object in a map. Looks up objects using the map when referring to them. - # - # More information on Identity Map pattern: - # http://www.martinfowler.com/eaaCatalog/identityMap.html - # - # == Configuration - # - # In order to enable IdentityMap, set <tt>config.active_record.identity_map = true</tt> - # in your <tt>config/application.rb</tt> file. - # - # IdentityMap is disabled by default and still in development (i.e. use it with care). - # - # == Associations - # - # Active Record Identity Map does not track associations yet. For example: - # - # comment = @post.comments.first - # comment.post = nil - # @post.comments.include?(comment) #=> true - # - # Ideally, the example above would return false, removing the comment object from the - # post association when the association is nullified. This may cause side effects, as - # in the situation below, if Identity Map is enabled: - # - # Post.has_many :comments, :dependent => :destroy - # - # comment = @post.comments.first - # comment.post = nil - # comment.save - # Post.destroy(@post.id) - # - # Without using Identity Map, the code above will destroy the @post object leaving - # the comment object intact. However, once we enable Identity Map, the post loaded - # by Post.destroy is exactly the same object as the object @post. As the object @post - # still has the comment object in @post.comments, once Identity Map is enabled, the - # comment object will be accidently removed. - # - # This inconsistency is meant to be fixed in future Rails releases. - # - module IdentityMap - - class << self - def enabled=(flag) - Thread.current[:identity_map_enabled] = flag - end - - def enabled - Thread.current[:identity_map_enabled] - end - alias enabled? enabled - - def repository - Thread.current[:identity_map] ||= Hash.new { |h,k| h[k] = {} } - end - - def use - old, self.enabled = enabled, true - - yield if block_given? - ensure - self.enabled = old - clear - end - - def without - old, self.enabled = enabled, false - - yield if block_given? - ensure - self.enabled = old - end - - def get(klass, primary_key) - record = repository[klass.symbolized_sti_name][primary_key] - - if record.is_a?(klass) - ActiveSupport::Notifications.instrument("identity.active_record", - :line => "From Identity Map (id: #{primary_key})", - :name => "#{klass} Loaded", - :connection_id => object_id) - - record - else - nil - end - end - - def add(record) - repository[record.class.symbolized_sti_name][record.id] = record - end - - def remove(record) - repository[record.class.symbolized_sti_name].delete(record.id) - end - - def remove_by_id(symbolized_sti_name, id) - repository[symbolized_sti_name].delete(id) - end - - def clear - repository.clear - end - end - - # Reinitialize an Identity Map model object from +coder+. - # +coder+ must contain the attributes necessary for initializing an empty - # model object. - def reinit_with(coder) - @attributes_cache = {} - dirty = @changed_attributes.keys - attributes = self.class.initialize_attributes(coder['attributes'].except(*dirty)) - @attributes.update(attributes) - @changed_attributes.update(coder['attributes'].slice(*dirty)) - @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]} - - run_callbacks :find - - self - end - - class Middleware - def initialize(app) - @app = app - end - - def call(env) - enabled = IdentityMap.enabled - IdentityMap.enabled = true - - response = @app.call(env) - response[2] = Rack::BodyProxy.new(response[2]) do - IdentityMap.enabled = enabled - IdentityMap.clear - end - - response - end - end - end -end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index eaa7deac5a..04fff99a6e 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,13 +1,16 @@ -require 'active_support/concern' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + # Determine whether to store the full constant name including namespace when using STI + mattr_accessor :store_full_sti_class, instance_accessor: false + self.store_full_sti_class = true + end + module Inheritance extend ActiveSupport::Concern included do - # Determine whether to store the full constant name including namespace when using STI config_attribute :store_full_sti_class - self.store_full_sti_class = true end module ClassMethods @@ -37,17 +40,43 @@ 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>). + # If you are using inheritance with ActiveRecord and don't want child classes + # to utilize the implied STI table name of the parent class, this will need to be true. + # For example, given the following: + # + # class SuperClass < ActiveRecord::Base + # self.abstract_class = true + # end + # class Child < SuperClass + # self.table_name = 'the_table_i_really_want' + # end + # + # + # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt> + # attr_accessor :abstract_class # Returns whether this class is an abstract class or not. @@ -62,25 +91,10 @@ module ActiveRecord # Finder methods must instantiate through this method to work with the # single-table inheritance model that makes it possible to create # objects of different types from the same table. - def instantiate(record) - sti_class = find_sti_class(record[inheritance_column]) - record_id = sti_class.primary_key && record[sti_class.primary_key] - - if ActiveRecord::IdentityMap.enabled? && record_id - if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? - record_id = record_id.to_i - end - if instance = IdentityMap.get(sti_class, record_id) - instance.reinit_with('attributes' => record) - else - instance = sti_class.allocate.init_with('attributes' => record) - IdentityMap.add(instance) - end - else - instance = sti_class.allocate.init_with('attributes' => record) - end - - instance + def instantiate(record, column_types = {}) + sti_class = find_sti_class(record[inheritance_column]) + column_types = sti_class.decorate_columns(column_types) + sti_class.allocate.init_with('attributes' => record, 'column_types' => column_types) end # For internal use. @@ -93,21 +107,6 @@ module ActiveRecord protected - # Returns the class descending directly from ActiveRecord::Base or an - # abstract class, if any, in the inheritance hierarchy. - def class_of_active_record_descendant(klass) - unless klass < Model - raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" - end - - sup = klass.active_record_super - if [Base, Model].include?(klass) || [Base, Model].include?(sup) || sup.abstract_class? - klass - else - class_of_active_record_descendant(sup) - end - end - # Returns the class type of the record using the current module as a prefix. So descendants of # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. def compute_type(type_name) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 2c42f4cca5..23c272ef12 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -39,7 +39,7 @@ module ActiveRecord when new_record? "#{self.class.model_name.cache_key}/new" when timestamp = self[:updated_at] - timestamp = timestamp.utc.to_s(:number) + timestamp = timestamp.utc.to_s(:nsec) "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" else "#{self.class.model_name.cache_key}/#{id}" diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 4d73cdd37a..e96ed00f9c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -1,4 +1,9 @@ module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :lock_optimistically, instance_accessor: false + self.lock_optimistically = true + end + module Locking # == What is Optimistic Locking # @@ -51,8 +56,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :lock_optimistically, :global => true - self.lock_optimistically = true + config_attribute :lock_optimistically end def locking_enabled? #:nodoc: @@ -82,9 +86,9 @@ 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_values(false, false, attribute_names)) + ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names)) affected_rows = connection.update stmt @@ -101,24 +105,29 @@ module ActiveRecord end end - def destroy #:nodoc: - return super unless locking_enabled? + def destroy_row + affected_rows = super - if persisted? - table = self.class.arel_table - lock_col = self.class.locking_column - predicate = table[self.class.primary_key].eq(id). - and(table[lock_col].eq(send(lock_col).to_i)) + if locking_enabled? && affected_rows != 1 + raise ActiveRecord::StaleObjectError.new(self, "destroy") + end - affected_rows = self.class.unscoped.where(predicate).delete_all + affected_rows + end - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "destroy") - end + def relation_for_destroy + relation = super + + if locking_enabled? + column_name = self.class.locking_column + column = self.class.columns_hash[column_name] + substitute = connection.substitute_at(column, relation.bind_values.length) + + relation = relation.where(self.class.arel_table[column_name].eq(substitute)) + relation.bind_values << [column, self[column_name].to_i] end - @destroyed = true - freeze + relation end module ClassMethods @@ -133,8 +142,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - @original_locking_column = @locking_column if defined?(@locking_column) - @locking_column = value.to_s + @locking_column = value.to_s end # The version column used for optimistic locking. Defaults to +lock_version+. @@ -160,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) #:nodoc: - if attributes.key?(locking_column) && lock_optimistically - attributes[locking_column] ||= 0 - end + def column_defaults + @column_defaults ||= begin + defaults = super - attributes + if defaults.key?(locking_column) && lock_optimistically + defaults[locking_column] ||= 0 + end + + defaults + end end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 2a9139749d..c1d57855a9 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,7 +1,4 @@ -require "active_support/core_ext/module/delegation" require "active_support/core_ext/class/attribute_accessors" -require 'active_support/deprecation' -require 'active_record/schema_migration' require 'set' module ActiveRecord @@ -33,6 +30,12 @@ module ActiveRecord end end + class PendingMigrationError < ActiveRecordError#:nodoc: + def initialize + super("Migrations are pending run 'rake db:migrate RAILS_ENV=#{ENV['RAILS_ENV']}' to resolve the issue") + end + end + # = Active Record Migrations # # Migrations can manage the evolution of a schema used by several physical @@ -47,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 @@ -327,10 +330,28 @@ module ActiveRecord class Migration autoload :CommandRecorder, 'active_record/migration/command_recorder' + + # This class is used to verify that all migrations have been run before + # loading a web page if config.active_record.migration_error is set to :page_load + class CheckPending + def initialize(app) + @app = app + end + + def call(env) + ActiveRecord::Migration.check_pending! + @app.call(env) + end + end + class << self attr_accessor :delegate # :nodoc: end + def self.check_pending! + raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration? + end + def self.method_missing(name, *args, &block) # :nodoc: (delegate || superclass.delegate).send(name, *args, &block) end @@ -606,6 +627,14 @@ module ActiveRecord end end + def needs_migration? + current_version < last_version + end + + def last_version + migrations(migrations_paths).last.try(:version)||0 + end + def proper_table_name(name) # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 96b62fdd61..95f4360578 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -51,13 +51,15 @@ module ActiveRecord super || delegate.respond_to?(*args) end - [:create_table, :create_join_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method| + [:create_table, :create_join_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default, :add_reference, :remove_reference].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args) # def create_table(*args) record(:"#{method}", args) # record(:create_table, args) end # end EOV end + alias :add_belongs_to :add_reference + alias :remove_belongs_to :remove_reference private @@ -102,6 +104,16 @@ module ActiveRecord [:remove_timestamps, args] end + def invert_add_reference(args) + [:remove_reference, args] + end + alias :invert_add_belongs_to :invert_add_reference + + def invert_remove_reference(args) + [:add_reference, args] + end + alias :invert_remove_belongs_to :invert_remove_reference + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) @delegate.send(method, *args, &block) diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb index 01a580781b..e880ae97bb 100644 --- a/activerecord/lib/active_record/migration/join_table.rb +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -4,13 +4,11 @@ module ActiveRecord private def find_join_table_name(table_1, table_2, options = {}) - options.delete(:table_name) { join_table_name(table_1, table_2) } + options.delete(:table_name) || join_table_name(table_1, table_2) end def join_table_name(table_1, table_2) - tables_names = [table_1, table_2].map(&:to_s).sort - - tables_names.join("_").to_sym + [table_1, table_2].sort.join("_").to_sym end end end diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb index 86de5ab2fa..57553c29eb 100644 --- a/activerecord/lib/active_record/model.rb +++ b/activerecord/lib/active_record/model.rb @@ -1,6 +1,31 @@ -require 'active_support/deprecation' +require 'active_support/core_ext/module/attribute_accessors' module ActiveRecord + module Configuration # :nodoc: + # This just abstracts out how we define configuration options in AR. Essentially we + # have mattr_accessors on the ActiveRecord:Model constant that define global defaults. + # Classes that then use AR get class_attributes defined, which means that when they + # are assigned the default will be overridden for that class and subclasses. (Except + # when options[:global] == true, in which case there is one global value always.) + def config_attribute(name, options = {}) + if options[:global] + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def self.#{name}; ActiveRecord::Model.#{name}; end + def #{name}; ActiveRecord::Model.#{name}; end + def self.#{name}=(val); ActiveRecord::Model.#{name} = val; end + CODE + else + options[:instance_writer] ||= false + class_attribute name, options + + singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + 1 + remove_method :#{name} + def #{name}; ActiveRecord::Model.#{name}; end + CODE + end + end + end + # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record persistence. # This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>. Example: # @@ -9,41 +34,35 @@ module ActiveRecord # end # module Model - module ClassMethods #:nodoc: - include ActiveSupport::Callbacks::ClassMethods - include ActiveModel::Naming - include QueryCache::ClassMethods - include ActiveSupport::Benchmarkable - include ActiveSupport::DescendantsTracker - - include Querying - include Translation - include DynamicMatchers - include CounterCache - include Explain - include ConnectionHandling - end + extend ActiveSupport::Concern + extend ConnectionHandling + extend ActiveModel::Observing::ClassMethods - def self.included(base) - return if base.singleton_class < ClassMethods + # This allows us to detect an ActiveRecord::Model while it's in the process of being included. + module Tag; end + def self.append_features(base) base.class_eval do - extend ClassMethods - Callbacks::Register.setup(self) - initialize_generated_modules unless self == Base + include Tag + extend Configuration end + + super end - extend ActiveModel::Configuration - extend ActiveModel::Callbacks - extend ActiveModel::MassAssignmentSecurity::ClassMethods - extend ActiveModel::AttributeMethods::ClassMethods - extend Callbacks::Register - extend Explain - extend ConnectionHandling + included do + extend ActiveModel::Naming + extend ActiveSupport::Benchmarkable + extend ActiveSupport::DescendantsTracker + + extend QueryCache::ClassMethods + extend Querying + extend Translation + extend DynamicMatchers + extend Explain + extend ConnectionHandling - def self.extend(*modules) - ClassMethods.send(:include, *modules) + initialize_generated_modules unless self == Base end include Persistence @@ -52,18 +71,26 @@ module ActiveRecord include Inheritance include Scoping include Sanitization - include Integration include AttributeAssignment include ActiveModel::Conversion + include Integration include Validations - include Locking::Optimistic, Locking::Pessimistic + include CounterCache + include Locking::Optimistic + include Locking::Pessimistic include AttributeMethods - include Callbacks, ActiveModel::Observing, Timestamp + include Callbacks + include ActiveModel::Observing + include Timestamp include Associations - include IdentityMap include ActiveModel::SecurePassword - include AutosaveAssociation, NestedAttributes - include Aggregations, Transactions, Reflection, Serialization, Store + include AutosaveAssociation + include NestedAttributes + include Aggregations + include Transactions + include Reflection + include Serialization + include Store include Core class << self @@ -74,36 +101,60 @@ module ActiveRecord def abstract_class? false end - + + # Defines the name of the table column which will store the class name on single-table + # inheritance situations. def inheritance_column 'type' end end - module DeprecationProxy #:nodoc: - class << self - instance_methods.each { |m| undef_method m unless m =~ /^__|^object_id$|^instance_eval$/ } - - def method_missing(name, *args, &block) - if Model.respond_to?(name) - Model.send(name, *args, &block) - else - ActiveSupport::Deprecation.warn( - "The object passed to the active_record load hook was previously ActiveRecord::Base " \ - "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ - "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ - "a module rather than a class. (Model is included in Base, so anything added to Model " \ - "will be available on Base as well.)" - ) - Base.send(name, *args, &block) - end + class DeprecationProxy < BasicObject #:nodoc: + def initialize(model = Model, base = Base) + @model = model + @base = base + end + + def method_missing(name, *args, &block) + if @model.respond_to?(name, true) + @model.send(name, *args, &block) + else + ::ActiveSupport::Deprecation.warn( + "The object passed to the active_record load hook was previously ActiveRecord::Base " \ + "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \ + "is only defined on ActiveRecord::Base. Please change your code so that it works with " \ + "a module rather than a class. (Model is included in Base, so anything added to Model " \ + "will be available on Base as well.)" + ) + @base.send(name, *args, &block) end + end + + alias send method_missing - alias send method_missing + def extend(*mods) + ::ActiveSupport::Deprecation.warn( + "The object passed to the active_record load hook was previously ActiveRecord::Base " \ + "(a Class). Now it is ActiveRecord::Model (a Module). You have called `extend' which " \ + "would add singleton methods to Model. This is presumably not what you want, since the " \ + "methods would not be inherited down to Base. Rather than using extend, please use " \ + "ActiveSupport::Concern + include, which will ensure that your class methods are " \ + "inherited." + ) + @base.extend(*mods) end end end + # This hook is where config accessors on Model get defined. + # + # We don't want to just open the Model module and add stuff to it in other files, because + # that would cause Model to load, which causes all sorts of loading order issues. + # + # We need this hook rather than just using the :active_record one, because users of the + # :active_record hook may need to use config options. + ActiveSupport.run_load_hooks(:active_record_config, Model) + # Load Base at this point, because the active_record load hook is run in that file. Base end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 61f82af0c3..99de16cd33 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,7 +1,18 @@ -require 'active_support/concern' -require 'active_support/core_ext/class/attribute_accessors' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :primary_key_prefix_type, instance_accessor: false + + mattr_accessor :table_name_prefix, instance_accessor: false + self.table_name_prefix = "" + + mattr_accessor :table_name_suffix, instance_accessor: false + self.table_name_suffix = "" + + mattr_accessor :pluralize_table_names, instance_accessor: false + self.pluralize_table_names = true + end + module ModelSchema extend ActiveSupport::Concern @@ -13,7 +24,7 @@ module ActiveRecord # the Product class will look for "productid" instead of "id" as the primary column. If the # latter is specified, the Product class will look for "product_id" instead of "id". Remember # that this is a global setting for all Active Records. - config_attribute :primary_key_prefix_type, :global => true + config_attribute :primary_key_prefix_type, global: true ## # :singleton-method: @@ -26,14 +37,12 @@ module ActiveRecord # a namespace by defining a singleton method in the parent module called table_name_prefix which # returns your chosen prefix. config_attribute :table_name_prefix - self.table_name_prefix = "" ## # :singleton-method: # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", # "people_basecamp"). By default, the suffix is the empty string. config_attribute :table_name_suffix - self.table_name_suffix = "" ## # :singleton-method: @@ -41,7 +50,6 @@ module ActiveRecord # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. # See table_name for the full rules on table/class naming. This is true, by default. config_attribute :pluralize_table_names - self.pluralize_table_names = true end module ClassMethods @@ -114,11 +122,18 @@ module ActiveRecord # You can also just define your own <tt>self.table_name</tt> method; see # the documentation for ActiveRecord::Base#table_name. def table_name=(value) - @original_table_name = @table_name if defined?(@table_name) - @table_name = value && value.to_s - @quoted_table_name = nil - @arel_table = nil - @relation = Relation.new(self, arel_table) + value = value && value.to_s + + if defined?(@table_name) + return if value == @table_name + reset_column_information if connected? + end + + @table_name = value + @quoted_table_name = nil + @arel_table = nil + @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name + @relation = Relation.new(self, arel_table) end # Returns a quoted version of the table name, used to construct SQL statements. @@ -128,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 @@ -152,8 +163,8 @@ module ActiveRecord # Sets the value of inheritance_column def inheritance_column=(value) - @original_inheritance_column = inheritance_column - @inheritance_column = value.to_s + @inheritance_column = value.to_s + @explicit_inheritance_column = true end def sequence_name @@ -165,7 +176,8 @@ module ActiveRecord end def reset_sequence_name #:nodoc: - self.sequence_name = connection.default_sequence_name(table_name, primary_key) + @explicit_sequence_name = false + @sequence_name = connection.default_sequence_name(table_name, primary_key) end # Sets the name of the sequence to use when generating ids to the given @@ -183,8 +195,8 @@ module ActiveRecord # self.sequence_name = "projectseq" # default would have been "project_seq" # end def sequence_name=(value) - @original_sequence_name = @sequence_name if defined?(@sequence_name) @sequence_name = value.to_s + @explicit_sequence_name = true end # Indicates whether the table associated with this class exists @@ -206,6 +218,26 @@ module ActiveRecord @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] end + def column_types # :nodoc: + @column_types ||= decorate_columns(columns_hash.dup) + end + + def decorate_columns(columns_hash) # :nodoc: + return if columns_hash.empty? + + serialized_attributes.each_key do |key| + columns_hash[key] = AttributeMethods::Serialization::Type.new(columns_hash[key]) + end + + columns_hash.each do |name, col| + if create_time_zone_conversion_attribute?(name, col) + columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(col) + end + end + + columns_hash + end + # Returns a hash where the keys are column names and the values are # default values when instantiating the AR object for this table. def column_defaults @@ -227,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 @@ -268,13 +299,23 @@ module ActiveRecord undefine_attribute_methods connection.schema_cache.clear_table_cache!(table_name) if table_exists? - @column_names = @content_columns = @column_defaults = @columns = @columns_hash = nil - @dynamic_methods_hash = @inheritance_column = nil - @arel_engine = @relation = nil + @arel_engine = nil + @column_defaults = nil + @column_names = nil + @columns = nil + @columns_hash = nil + @column_types = nil + @content_columns = nil + @dynamic_methods_hash = nil + @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column + @relation = nil end - def clear_cache! # :nodoc: - connection.schema_cache.clear! + # This is a hook for use by modules that need to do extra stuff to + # attributes when they are initialized. (e.g. attribute + # serialization) + def initialize_attributes(attributes, options = {}) #:nodoc: + attributes end private @@ -282,8 +323,7 @@ module ActiveRecord # Guesses the table name, but does not decorate it with prefix and suffix information. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore - table_name = table_name.pluralize if pluralize_table_names - table_name + pluralize_table_names ? table_name.pluralize : table_name end # Computes and returns a table name according to default conventions. diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 9e21039c4f..f91e41b535 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -1,10 +1,13 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/try' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/class/attribute' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :nested_attributes_options, instance_accessor: false + self.nested_attributes_options = {} + end + module NestedAttributes #:nodoc: class TooManyRecords < ActiveRecordError end @@ -13,16 +16,15 @@ module ActiveRecord included do config_attribute :nested_attributes_options - self.nested_attributes_options = {} end # = Active Record Nested Attributes # # Nested attributes allow you to save attributes on associated records - # through the parent. By default nested attribute updating is turned off, - # you can enable it using the accepts_nested_attributes_for class method. - # When you enable nested attributes an attribute writer is defined on - # the model. + # through the parent. By default nested attribute updating is turned off + # and you can enable it using the accepts_nested_attributes_for class + # method. When you enable nested attributes an attribute writer is + # defined on the model. # # The attribute writer is named after the association, which means that # in the following example, two new methods are added to your model: @@ -243,15 +245,24 @@ 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. # [:update_only] - # Allows you to specify that an existing record may only be updated. - # A new record may only be created when there is no existing record. - # This option only works for one-to-one associations and is ignored for - # collection associations. This option is off by default. + # For a one-to-one association, this option allows you to specify how + # nested attributes are to be used when an associated record already + # exists. In general, an existing record may either be updated with the + # new set of attribute values or be replaced by a wholly new record + # containing those values. By default the :update_only option is +false+ + # and the nested attributes are used to update the existing record only + # if they include the record's <tt>:id</tt> value. Otherwise a new + # record will be instantiated and used to replace the existing one. + # However if the :update_only option is +true+, the nested attributes + # are used to update the record's attributes always, regardless of + # whether the <tt>:id</tt> is present. The option is ignored for collection + # associations. # # Examples: # # creates avatar_attributes= @@ -280,7 +291,7 @@ module ActiveRecord # def pirate_attributes=(attributes) # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options) # end - class_eval <<-eoruby, __FILE__, __LINE__ + 1 + generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 if method_defined?(:#{association_name}_attributes=) remove_method(:#{association_name}_attributes=) end @@ -312,10 +323,13 @@ module ActiveRecord # Assigns the given attributes to the association. # - # If update_only is false and the given attributes include an <tt>:id</tt> - # that matches the existing record's id, then the existing record will be - # modified. If update_only is true, a new record is only created when no - # object exists. Otherwise a new record will be built. + # If an associated record does not yet exist, one will be instantiated. If + # an associated record already exists, the method's behavior depends on + # the value of the update_only option. If update_only is +false+ and the + # given attributes include an <tt>:id</tt> that matches the existing record's + # id, then the existing record will be modified. If no <tt>:id</tt> is provided + # it will be replaced with a new record. If update_only is +true+ the existing + # record will be modified regardless of whether an <tt>:id</tt> is provided. # # If the given attributes include a matching <tt>:id</tt> attribute, or # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, @@ -336,7 +350,7 @@ module ActiveRecord if respond_to?(method) send(method, attributes.except(*unassignable_keys(assignment_opts)), assignment_opts) else - raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?" + raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" end end end @@ -358,7 +372,7 @@ module ActiveRecord # }) # # Will update the name of the Person with ID 1, build a new associated - # person with the name `John', and mark the associated Person with ID 2 + # person with the name 'John', and mark the associated Person with ID 2 # for destruction. # # Also accepts an Array of attribute hashes: @@ -375,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 @@ -394,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 60c37ac2b7..4c1c91e3df 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -1,10 +1,61 @@ # -*- coding: utf-8 -*- module ActiveRecord - # = Active Record Null Relation - class NullRelation < Relation + module NullRelation # :nodoc: def exec_queries @records = [] end + + def pluck(_column_name) + [] + end + + def delete_all(_conditions = nil) + 0 + end + + def update_all(_updates, _conditions = nil, _options = {}) + 0 + end + + def delete(_id_or_array) + 0 + end + + def size + 0 + end + + def empty? + true + end + + def any? + false + end + + def many? + false + end + + def to_sql + @to_sql ||= "" + end + + def where_values_hash + {} + end + + def count + 0 + end + + def calculate(_operation, _column_name, _options = {}) + nil + end + + def exists?(_id = false) + false + end end -end
\ No newline at end of file +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 09ee2ba61d..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 @@ -115,36 +113,37 @@ module ActiveRecord # callbacks, Observer methods, or any <tt>:dependent</tt> association # options, use <tt>#destroy</tt>. def delete - if persisted? - self.class.delete(id) - IdentityMap.remove(self) if IdentityMap.enabled? - end + self.class.delete(id) if persisted? @destroyed = true freeze end # Deletes the record in the database and freezes this instance to reflect # that no changes should be made (since they can't be persisted). + # + # There's a series of callbacks associated with <tt>destroy</tt>. If + # the <tt>before_destroy</tt> callback return +false+ the action is cancelled + # and <tt>destroy</tt> returns +false+. See + # ActiveRecord::Callbacks for further details. def destroy + raise ReadOnlyRecord if readonly? destroy_associations - - if persisted? - IdentityMap.remove(self) if IdentityMap.enabled? - pk = self.class.primary_key - column = self.class.columns_hash[pk] - substitute = connection.substitute_at(column, 0) - - relation = self.class.unscoped.where( - self.class.arel_table[pk].eq(substitute)) - - relation.bind_values = [[column, id]] - relation.delete_all - end - + destroy_row if persisted? @destroyed = true freeze end + # Deletes the record in the database and freezes this instance to reflect + # that no changes should be made (since they can't be persisted). + # + # There's a series of callbacks associated with <tt>destroy!</tt>. If + # the <tt>before_destroy</tt> callback return +false+ the action is cancelled + # and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See + # ActiveRecord::Callbacks for further details. + def destroy! + destroy || raise(ActiveRecord::RecordNotDestroyed) + end + # Returns an instance of the specified +klass+ with the attributes of the # current record. This is mostly useful in relation to single-table # inheritance structures where you want a subclass to appear as the @@ -162,7 +161,7 @@ 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 @@ -176,27 +175,11 @@ module ActiveRecord # def update_attribute(name, value) name = name.to_s - raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) + verify_readonly_attribute(name) send("#{name}=", value) save(:validate => false) end - # Updates a single attribute of an object, without calling save. - # - # * Validation is skipped. - # * Callbacks are skipped. - # * updated_at/updated_on column is not updated if that column is available. - # - # Raises an +ActiveRecordError+ when called on new objects, or when the +name+ - # attribute is marked as readonly. - def update_column(name, value) - name = name.to_s - raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) - raise ActiveRecordError, "can not update on a new record object" unless persisted? - raw_write_attribute(name, value) - self.class.update_all({ name => value }, self.class.primary_key => id) == 1 - end - # Updates the attributes of the model from the passed-in hash and saves the # record, all wrapped in a transaction. If the object is invalid, the saving # will fail and false will be returned. @@ -225,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+. @@ -284,10 +301,15 @@ module ActiveRecord clear_aggregation_cache clear_association_cache - IdentityMap.without do - fresh_object = self.class.unscoped { self.class.find(id, options) } - @attributes.update(fresh_object.instance_variable_get('@attributes')) - end + fresh_object = + if options && options[:lock] + self.class.unscoped { self.class.lock.find(id) } + else + self.class.unscoped { self.class.find(id) } + end + + @attributes.update(fresh_object.instance_variable_get('@attributes')) + @columns_hash = fresh_object.instance_variable_get('@columns_hash') @attributes_cache = {} self @@ -322,14 +344,15 @@ module ActiveRecord changes = {} attributes.each do |column| - changes[column.to_s] = write_attribute(column.to_s, current_time) + column = column.to_s + changes[column] = write_attribute(column, current_time) end changes[self.class.locking_column] = increment_lock if locking_enabled? @changed_attributes.except!(*changes.keys) primary_key = self.class.primary_key - self.class.unscoped.update_all(changes, { primary_key => self[primary_key] }) == 1 + self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 end end @@ -339,6 +362,22 @@ module ActiveRecord def destroy_associations end + def destroy_row + relation_for_destroy.delete_all + end + + def relation_for_destroy + pk = self.class.primary_key + column = self.class.columns_hash[pk] + substitute = connection.substitute_at(column, 0) + + relation = self.class.unscoped.where( + self.class.arel_table[pk].eq(substitute)) + + relation.bind_values = [[column, id]] + relation + end + def create_or_update raise ReadOnlyRecord if readonly? result = new_record? ? create : update @@ -348,7 +387,7 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def update(attribute_names = @attributes.keys) - attributes_with_values = arel_attributes_values(false, false, attribute_names) + attributes_with_values = arel_attributes_with_values_for_update(attribute_names) return 0 if attributes_with_values.empty? klass = self.class stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values) @@ -358,15 +397,17 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. def create - attributes_values = arel_attributes_values(!id.nil?) + attributes_values = arel_attributes_with_values_for_create(!id.nil?) new_id = self.class.unscoped.insert attributes_values - self.id ||= new_id if self.class.primary_key - IdentityMap.add(self) if IdentityMap.enabled? @new_record = false id end + + def verify_readonly_attribute(name) + raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) + end end end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 9701898415..2bd8ecda20 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Query Cache @@ -34,16 +33,22 @@ module ActiveRecord response = @app.call(env) response[2] = Rack::BodyProxy.new(response[2]) do - ActiveRecord::Base.connection_id = connection_id - ActiveRecord::Base.connection.clear_query_cache - ActiveRecord::Base.connection.disable_query_cache! unless enabled + restore_query_cache_settings(connection_id, enabled) end response rescue Exception => e + restore_query_cache_settings(connection_id, enabled) + raise e + end + + private + + def restore_query_cache_settings(connection_id, enabled) + ActiveRecord::Base.connection_id = connection_id ActiveRecord::Base.connection.clear_query_cache ActiveRecord::Base.connection.disable_query_cache! unless enabled - raise e end + end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 5945b05190..13e09eda53 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,15 +1,15 @@ -require 'active_support/core_ext/module/delegation' module ActiveRecord module Querying - delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped - delegate :first_or_create, :first_or_create!, :first_or_initialize, :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, :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 @@ -36,7 +36,15 @@ module ActiveRecord def find_by_sql(sql, binds = []) logging_query_plan do result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) - result_set.map { |record| instantiate(record) } + column_types = {} + + if result_set.respond_to? :column_types + column_types = result_set.column_types + else + ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" + end + + result_set.map { |record| instantiate(record, column_types) } end end @@ -52,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 058dd58efb..ecf8547e67 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -29,7 +29,13 @@ 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" end @@ -38,10 +44,15 @@ module ActiveRecord # first time. Also, make it output to STDERR. console do |app| require "active_record/railties/console_sandbox" if app.sandbox? + require "active_record/base" console = ActiveSupport::Logger.new(STDERR) Rails.logger.extend ActiveSupport::Logger.broadcast console end + runner do |app| + require "active_record/base" + end + initializer "active_record.initialize_timezone" do ActiveSupport.on_load(:active_record) do self.time_zone_aware_attributes = true @@ -53,16 +64,34 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end - initializer "active_record.identity_map" do |app| - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::IdentityMap::Middleware" if config.active_record.delete(:identity_map) + initializer "active_record.migration_error" do |app| + if config.active_record.delete(:migration_error) == :page_load + config.app_middleware.insert_after "::ActionDispatch::Callbacks", + "ActiveRecord::Migration::CheckPending" + end + end + + initializer "active_record.check_schema_cache_dump" do |app| + if config.active_record.delete(:use_schema_cache_dump) + config.after_initialize do |app| + ActiveSupport.on_load(:active_record) do + filename = File.join(app.config.paths["db"].first, "schema_cache.dump") + + if File.file?(filename) + cache = Marshal.load File.binread filename + if cache.version == ActiveRecord::Migrator.current_version + ActiveRecord::Model.connection.schema_cache = cache + else + warn "schema_cache.dump is expired. Current version is #{ActiveRecord::Migrator.current_version}, but cache version is #{cache.version}." + end + end + end + end + end end initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - if app.config.active_record.delete(:whitelist_attributes) - attr_accessible(nil) - end app.config.active_record.each do |k,v| send "#{k}=", v end @@ -73,7 +102,9 @@ module ActiveRecord # and then establishes the connection. initializer "active_record.initialize_database" do |app| ActiveSupport.on_load(:active_record) do - self.configurations = app.config.database_configuration + unless ENV['DATABASE_URL'] + self.configurations = app.config.database_configuration + end establish_connection end end @@ -87,18 +118,12 @@ module ActiveRecord end initializer "active_record.set_reloader_hooks" do |app| - hook = lambda do - ActiveRecord::Base.clear_reloadable_connections! - ActiveRecord::Base.clear_cache! - end + hook = app.config.reload_classes_only_on_change ? :to_prepare : :to_cleanup - if app.config.reload_classes_only_on_change - ActiveSupport.on_load(:active_record) do - ActionDispatch::Reloader.to_prepare(&hook) - end - else - ActiveSupport.on_load(:active_record) do - ActionDispatch::Reloader.to_cleanup(&hook) + ActiveSupport.on_load(:active_record) do + ActionDispatch::Reloader.send(hook) do + ActiveRecord::Model.clear_reloadable_connections! + ActiveRecord::Model.clear_cache! end end end @@ -107,14 +132,15 @@ module ActiveRecord config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"] end - config.after_initialize do + config.after_initialize do |app| ActiveSupport.on_load(:active_record) do - ActiveRecord::Base.instantiate_observers + ActiveRecord::Model.instantiate_observers ActionDispatch::Reloader.to_prepare do - ActiveRecord::Base.instantiate_observers + ActiveRecord::Model.instantiate_observers end end + end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 822b51e838..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 @@ -14,139 +13,28 @@ db_namespace = namespace :db do end namespace :create do - # desc 'Create all the local databases defined in config/database.yml' task :all => :load_config do - ActiveRecord::Base.configurations.each_value do |config| - # Skip entries that don't have a database key, such as the first entry here: - # - # defaults: &defaults - # adapter: mysql - # username: root - # password: - # host: localhost - # - # development: - # database: blog_development - # *defaults - next unless config['database'] - # Only connect to local databases - local_database?(config) { create_database(config) } - end + ActiveRecord::Tasks::DatabaseTasks.create_all end end desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' - task :create => :load_config do - configs_for_environment.each { |config| create_database(config) } - ActiveRecord::Base.establish_connection(configs_for_environment.first) - end - - def mysql_creation_options(config) - @charset = ENV['CHARSET'] || 'utf8' - @collation = ENV['COLLATION'] || 'utf8_unicode_ci' - {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)} - end - - def create_database(config) - begin - if config['adapter'] =~ /sqlite/ - if File.exist?(config['database']) - $stderr.puts "#{config['database']} already exists" - else - begin - # Create the SQLite database - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection - rescue Exception => e - $stderr.puts e, *(e.backtrace) - $stderr.puts "Couldn't create database for #{config.inspect}" - end - end - return # Skip the else clause of begin/rescue - else - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection - end - rescue - case config['adapter'] - when /mysql/ - if config['adapter'] =~ /jdbc/ - #FIXME After Jdbcmysql gives this class - require 'active_record/railties/jdbcmysql_error' - error_class = ArJdbcMySQL::Error - else - error_class = config['adapter'] =~ /mysql2/ ? Mysql2::Error : Mysql::Error - end - access_denied_error = 1045 - begin - ActiveRecord::Base.establish_connection(config.merge('database' => nil)) - ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config)) - ActiveRecord::Base.establish_connection(config) - rescue error_class => sqlerr - if sqlerr.errno == access_denied_error - print "#{sqlerr.error}. \nPlease provide the root password for your mysql installation\n>" - root_password = $stdin.gets.strip - grant_statement = "GRANT ALL PRIVILEGES ON #{config['database']}.* " \ - "TO '#{config['username']}'@'localhost' " \ - "IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;" - ActiveRecord::Base.establish_connection(config.merge( - 'database' => nil, 'username' => 'root', 'password' => root_password)) - ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config)) - ActiveRecord::Base.connection.execute grant_statement - ActiveRecord::Base.establish_connection(config) - else - $stderr.puts sqlerr.error - $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['charset'] || @charset}, collation: #{config['collation'] || @collation}" - $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['charset'] - end - end - when /postgresql/ - @encoding = config['encoding'] || ENV['CHARSET'] || 'utf8' - begin - ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) - ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => @encoding)) - ActiveRecord::Base.establish_connection(config) - rescue Exception => e - $stderr.puts e, *(e.backtrace) - $stderr.puts "Couldn't create database for #{config.inspect}" - end - end - else - $stderr.puts "#{config['database']} already exists" - end + task :create => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.create_current end namespace :drop do - # desc 'Drops all the local databases defined in config/database.yml' task :all => :load_config do - ActiveRecord::Base.configurations.each_value do |config| - # Skip entries that don't have a database key - next unless config['database'] - begin - # Only connect to local databases - local_database?(config) { drop_database(config) } - rescue Exception => e - $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}" - end - end + ActiveRecord::Tasks::DatabaseTasks.drop_all end end desc 'Drops the database for the current Rails.env (use db:drop:all to drop all databases)' - task :drop => :load_config do - configs_for_environment.each { |config| drop_database_and_rescue(config) } - end - - def local_database?(config, &block) - if config['host'].in?(['127.0.0.1', 'localhost']) || config['host'].blank? - yield - else - $stderr.puts "This task only modifies local databases. #{config['database']} is on a remote host." - end + task :drop => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.drop_current end - - desc "Migrate the database (options: VERSION=x, VERBOSE=false)." + desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration| @@ -162,6 +50,9 @@ db_namespace = namespace :db do else raise "unknown schema format #{ActiveRecord::Base.schema_format}" end + # Allow this task to be called as many times as required. An example is the + # migrate:redo task, which calls other two internally that depend on this one. + db_namespace['_dump'].reenable end namespace :migrate do @@ -197,18 +88,19 @@ 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.' next # means "return" for rake task end db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") + db_list.map! { |version| "%.3d" % version } file_list = [] ActiveRecord::Migrator.migrations_paths.each do |path| Dir.foreach(path) do |file| - # only files matching "20091231235959_some_name.rb" pattern - if match_data = /^(\d{14})_(.+)\.rb$/.match(file) + # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern + if match_data = /^(\d{3,})_(.+)\.rb$/.match(file) status = db_list.delete(match_data[1]) ? 'up' : 'down' file_list << [status, match_data[1], match_data[2].humanize] end @@ -243,48 +135,32 @@ db_namespace = namespace :db do end # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' - task :reset => :environment do + task :reset => [:environment, :load_config] do db_namespace["drop"].invoke db_namespace["setup"].invoke end # desc "Retrieves the charset for the current environment's database" - task :charset => :environment do - config = ActiveRecord::Base.configurations[Rails.env || 'development'] - case config['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.charset - when /postgresql/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.encoding - when /sqlite/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.encoding - else - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end + task :charset => [:environment, :load_config] do + puts ActiveRecord::Tasks::DatabaseTasks.charset_current end # desc "Retrieves the collation for the current environment's database" - task :collation => :environment do - config = ActiveRecord::Base.configurations[Rails.env || 'development'] - case config['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(config) - puts ActiveRecord::Base.connection.collation - else - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + task :collation => [:environment, :load_config] do + begin + puts ActiveRecord::Tasks::DatabaseTasks.collation_current + rescue NoMethodError + $stderr.puts 'Sorry, your database adapter is not supported yet, feel free to submit a patch' end end desc 'Retrieves the current schema version number' - task :version => :environment do + task :version => [:environment, :load_config] do puts "Current version: #{ActiveRecord::Migrator.current_version}" end # desc "Raises an error if there are pending migrations" - task :abort_if_pending_migrations => :environment do + task :abort_if_pending_migrations => [:environment, :load_config] do pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations if pending_migrations.any? @@ -307,20 +183,20 @@ db_namespace = namespace :db do namespace :fixtures do desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task :load => :environment do + task :load => [:environment, :load_config] do require 'active_record/fixtures' ActiveRecord::Base.establish_connection(Rails.env) base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.{yml,csv}"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| + (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| ActiveRecord::Fixtures.create_fixtures(fixtures_dir, fixture_file) end end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." - task :identify => :environment do + task :identify => [:environment, :load_config] do require 'active_record/fixtures' label, id = ENV['LABEL'], ENV['ID'] @@ -356,7 +232,7 @@ db_namespace = namespace :db do end desc 'Load a schema.rb file into the database' - task :load => :environment do + task :load => [:environment, :load_config] do file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" if File.exists?(file) load(file) @@ -365,31 +241,50 @@ db_namespace = namespace :db do end end - task :load_if_ruby => 'db:create' do + task :load_if_ruby => [:environment, 'db:create'] do db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby end + + namespace :cache do + desc 'Create a db/schema_cache.dump file.' + task :dump => [:environment, :load_config] do + con = ActiveRecord::Base.connection + filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump") + + con.schema_cache.clear! + con.tables.each { |table| con.schema_cache.add(table) } + open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) } + end + + desc 'Clear a db/schema_cache.dump file.' + task :clear => [:environment, :load_config] do + filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump") + FileUtils.rm(filename) if File.exists?(filename) + end + end + end namespace :structure do + def set_firebird_env(config) + ENV['ISC_USER'] = config['username'].to_s if config['username'] + ENV['ISC_PASSWORD'] = config['password'].to_s if config['password'] + end + + def firebird_db_string(config) + FireRuby::Database.db_string_for(config.symbolize_keys) + end + desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' - task :dump => :environment do + task :dump => [:environment, :load_config] do abcs = ActiveRecord::Base.configurations filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") case abcs[Rails.env]['adapter'] - when /mysql/, 'oci', 'oracle' + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.structure_dump(abcs[Rails.env], filename) + when 'oci', 'oracle' ActiveRecord::Base.establish_connection(abcs[Rails.env]) File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } - when /postgresql/ - set_psql_env(abcs[Rails.env]) - search_path = abcs[Rails.env]['schema_search_path'] - unless search_path.blank? - search_path = search_path.split(",").map{|search_path_part| "--schema=#{search_path_part.strip}" }.join(" ") - end - `pg_dump -i -s -x -O -f #{filename} #{search_path} #{abcs[Rails.env]['database']}` - raise 'Error dumping database' if $?.exitstatus == 1 - when /sqlite/ - dbfile = abcs[Rails.env]['database'] - `sqlite3 #{dbfile} .schema > #{filename}` when 'sqlserver' `smoscript -s #{abcs[Rails.env]['host']} -d #{abcs[Rails.env]['database']} -u #{abcs[Rails.env]['username']} -p #{abcs[Rails.env]['password']} -f #{filename} -A -U` when "firebird" @@ -403,6 +298,7 @@ db_namespace = namespace :db do if ActiveRecord::Base.connection.supports_migrations? File.open(filename, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } end + db_namespace['structure:dump'].reenable end # desc "Recreate the databases from the structure.sql file" @@ -412,18 +308,8 @@ db_namespace = namespace :db do abcs = ActiveRecord::Base.configurations filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") case abcs[env]['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(abcs[env]) - ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') - IO.read(filename).split("\n\n").each do |table| - ActiveRecord::Base.connection.execute(table) - end - when /postgresql/ - set_psql_env(abcs[env]) - `psql -f "#{filename}" #{abcs[env]['database']} #{abcs[env]['template']}` - when /sqlite/ - dbfile = abcs[env]['database'] - `sqlite3 #{dbfile} < "#{filename}"` + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.structure_load(abcs[env], filename) when 'sqlserver' `sqlcmd -S #{abcs[env]['host']} -d #{abcs[env]['database']} -U #{abcs[env]['username']} -P #{abcs[env]['password']} -i #{filename}` when 'oci', 'oracle' @@ -440,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 @@ -454,7 +340,14 @@ db_namespace = namespace :db do db_namespace["test:load_schema"].invoke when :sql db_namespace["test:load_structure"].invoke - end + end + end + + # desc "Recreate the test database from an existent schema.rb file" + task :load_schema => 'db:test:purge' do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) + ActiveRecord::Schema.verbose = false + db_namespace["schema:load"].invoke end # desc "Recreate the test database from an existent structure.sql file" @@ -467,33 +360,28 @@ db_namespace = namespace :db do end end - # desc "Recreate the test database from an existent schema.rb file" - task :load_schema => 'db:test:purge' do - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) - ActiveRecord::Schema.verbose = false - db_namespace["schema:load"].invoke + # desc "Recreate the test database from a fresh schema" + task :clone do + case ActiveRecord::Base.schema_format + when :ruby + db_namespace["test:clone_schema"].invoke + when :sql + db_namespace["test:clone_structure"].invoke + end end # desc "Recreate the test database from a fresh schema.rb file" - task :clone => %w(db:schema:dump db:test:load_schema) + task :clone_schema => ["db:schema:dump", "db:test:load_schema"] # desc "Recreate the test database from a fresh structure.sql file" task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ] # desc "Empty the test database" - task :purge => :environment do + task :purge => [:environment, :load_config] do abcs = ActiveRecord::Base.configurations case abcs['test']['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(:test) - ActiveRecord::Base.connection.recreate_database(abcs['test']['database'], mysql_creation_options(abcs['test'])) - when /postgresql/ - ActiveRecord::Base.clear_active_connections! - drop_database(abcs['test']) - create_database(abcs['test']) - when /sqlite/ - dbfile = abcs['test']['database'] - File.delete(dbfile) if File.exist?(dbfile) + when /mysql/, /postgresql/, /sqlite/ + ActiveRecord::Tasks::DatabaseTasks.purge abcs['test'] when 'sqlserver' test = abcs.deep_dup['test'] test_database = test['database'] @@ -516,25 +404,10 @@ db_namespace = namespace :db do # desc 'Check for pending migrations and load the test schema' task :prepare => 'db:abort_if_pending_migrations' do unless ActiveRecord::Base.configurations.blank? - db_namespace[{ :sql => 'test:clone_structure', :ruby => 'test:load' }[ActiveRecord::Base.schema_format]].invoke + db_namespace['test:load'].invoke end end end - - namespace :sessions do - # desc "Creates a sessions migration for use with ActiveRecord::SessionStore" - task :create => :environment do - 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 do - ActiveRecord::Base.connection.execute "DELETE FROM #{session_table_name}" - end - end end namespace :railties do @@ -542,8 +415,8 @@ namespace :railties do # desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2" task :migrations => :'db:load_config' do to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip } - railties = ActiveSupport::OrderedHash.new - Rails.application.railties.all do |railtie| + railties = {} + Rails.application.railties.each do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first) @@ -559,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 @@ -567,53 +440,3 @@ end task 'test:prepare' => 'db:test:prepare' -def drop_database(config) - case config['adapter'] - when /mysql/ - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection.drop_database config['database'] - when /sqlite/ - require 'pathname' - path = Pathname.new(config['database']) - file = path.absolute? ? path.to_s : File.join(Rails.root, path) - - FileUtils.rm(file) - when /postgresql/ - ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) - ActiveRecord::Base.connection.drop_database config['database'] - end -end - -def drop_database_and_rescue(config) - begin - drop_database(config) - rescue Exception => e - $stderr.puts "Couldn't drop #{config['database']} : #{e.inspect}" - end -end - -def configs_for_environment - environments = [Rails.env] - environments << 'test' if Rails.env.development? - ActiveRecord::Base.configurations.values_at(*environments).compact.reject { |config| config['database'].blank? } -end - -def session_table_name - ActiveRecord::SessionStore::Session.table_name -end - -def set_firebird_env(config) - ENV['ISC_USER'] = config['username'].to_s if config['username'] - ENV['ISC_PASSWORD'] = config['password'].to_s if config['password'] -end - -def firebird_db_string(config) - FireRuby::Database.db_string_for(config.symbolize_keys) -end - -def set_psql_env(config) - ENV['PGHOST'] = config['host'] if config['host'] - ENV['PGPORT'] = config['port'].to_s if config['port'] - ENV['PGPASSWORD'] = config['password'].to_s if config['password'] - ENV['PGUSER'] = config['username'].to_s if config['username'] -end diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb index 0b75983484..6a38211bff 100644 --- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb +++ b/activerecord/lib/active_record/railties/jdbcmysql_error.rb @@ -1,6 +1,6 @@ #FIXME Remove if ArJdbcMysql will give. module ArJdbcMySQL #:nodoc: - class Error < StandardError + class Error < StandardError #:nodoc: attr_accessor :error_number, :sql_state def initialize msg diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 836b15e2ce..b3c20c4aff 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -1,12 +1,10 @@ -require 'active_support/concern' -require 'active_support/core_ext/class/attribute' module ActiveRecord module ReadonlyAttributes extend ActiveSupport::Concern included do - config_attribute :_attr_readonly + class_attribute :_attr_readonly, instance_accessor: false self._attr_readonly = [] end @@ -22,5 +20,10 @@ module ActiveRecord self._attr_readonly end end + + def _attr_readonly + ActiveSupport::Deprecation.warn("Instance level _attr_readonly method is deprecated, please use class level method.") + defined?(@_attr_readonly) ? @_attr_readonly : self.class._attr_readonly + end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 8f8a3ec3bb..cf949a893f 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/object/inclusion' module ActiveRecord # = Active Record Reflection @@ -7,8 +5,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - extend ActiveModel::Configuration - config_attribute :reflections + class_attribute :reflections self.reflections = {} end @@ -21,13 +18,13 @@ module ActiveRecord # MacroReflection class has info for AggregateReflection and AssociationReflection # classes. module ClassMethods - def create_reflection(macro, name, options, active_record) + def create_reflection(macro, name, scope, options, active_record) case macro - when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many - klass = options[:through] ? ThroughReflection : AssociationReflection - reflection = klass.new(macro, name, options, active_record) - when :composed_of - reflection = AggregateReflection.new(macro, name, options, active_record) + 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) @@ -44,7 +41,8 @@ module ActiveRecord # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection # def reflect_on_aggregation(aggregation) - reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil + reflection = reflections[aggregation] + reflection if reflection.is_a?(AggregateReflection) end # Returns an array of AssociationReflection objects for all the @@ -68,7 +66,8 @@ module ActiveRecord # Invoice.reflect_on_association(:line_items).macro # returns :has_many # def reflect_on_association(association) - reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil + reflection = reflections[association] + reflection if reflection.is_a?(AssociationReflection) end # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. @@ -77,7 +76,6 @@ module ActiveRecord end end - # Abstract base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. class MacroReflection @@ -93,6 +91,8 @@ module ActiveRecord # <tt>has_many :clients</tt> returns <tt>:has_many</tt> attr_reader :macro + attr_reader :scope + # Returns the hash of options used for the macro. # # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt> @@ -103,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 ? @@ -138,10 +139,6 @@ module ActiveRecord active_record == other_aggregation.active_record end - def sanitized_conditions #:nodoc: - @sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions] - end - private def derive_class_name name.to_s.camelize @@ -152,6 +149,10 @@ module ActiveRecord # 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 @@ -173,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 @@ -192,6 +193,10 @@ module ActiveRecord @quoted_table_name ||= klass.quoted_table_name end + def join_table + @join_table ||= options[:join_table] || derive_join_table + end + def foreign_key @foreign_key ||= options[:foreign_key] || derive_foreign_key end @@ -239,6 +244,10 @@ module ActiveRecord def check_validity! check_validity_of_inverse! + + if has_and_belongs_to_many? && association_foreign_key == foreign_key + raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self) + end end def check_validity_of_inverse! @@ -267,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 @@ -321,6 +329,10 @@ module ActiveRecord macro == :belongs_to end + def has_and_belongs_to_many? + macro == :has_and_belongs_to_many + end + def association_class case macro when :belongs_to @@ -363,6 +375,10 @@ module ActiveRecord end end + def derive_join_table + [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + end + def primary_key(klass) klass.primary_key || raise(UnknownPrimaryKey.new(klass)) end @@ -431,29 +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 - - # And return - 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 ac70aeba67..2d0457636e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,33 +1,34 @@ # -*- coding: utf-8 -*- -require 'active_support/core_ext/object/blank' -require 'active_support/deprecation' module ActiveRecord # = Active Record Relation class Relation JoinOperation = Struct.new(:relation, :join_class, :on) - ASSOCIATION_METHODS = [:includes, :eager_load, :preload] - MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind, :references] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :uniq] + + MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, + :order, :joins, :where, :having, :bind, :references, + :extending] + + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, + :reverse_order, :uniq, :create_with] + + VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded - attr_accessor :extensions, :default_scoped + attr_accessor :default_scoped + alias :model :klass alias :loaded? :loaded alias :default_scoped? :default_scoped - def initialize(klass, table) - @klass, @table = klass, table - + def initialize(klass, table, values = {}) + @klass = klass + @table = table + @values = values @implicit_readonly = nil @loaded = false @default_scoped = false - - SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)} - (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} - @extensions = [] - @create_with_value = {} end def insert(values) @@ -73,27 +74,62 @@ 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 def initialize_copy(other) + @values = @values.dup + @values[:bind] = @values[:bind].dup if @values[:bind] reset end 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. @@ -141,58 +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 = if @readonly_value.nil? && !@klass.locking_enabled? - eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) - else - IdentityMap.without do - eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) - end - end - - preload = @preload_values - preload += @includes_values unless eager_loading? - preload.each do |associations| - ActiveRecord::Associations::Preloader.new(@records, associations).run - end - - # @readonly_value is true only if set explicitly. @implicit_readonly is true if there - # are JOINS and no explicit SELECT. - readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value - @records.each { |record| record.readonly! } if readonly - 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) @@ -211,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) } @@ -219,18 +215,17 @@ 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) } else - @limit_value ? to_a.many? : size > 1 + limit_value ? to_a.many? : size > 1 end end # 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 @@ -238,7 +233,10 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - @klass.with_scope(self, :overwrite) { yield } + previous, klass.current_scope = klass.current_scope, self + yield + ensure + klass.current_scope = previous end # Updates all records with details given if they match a set of conditions supplied, limits and order can @@ -249,50 +247,35 @@ 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.update_all "author = 'David'", "title LIKE '%Rails%'" - # - # # Update all avatars migrated more than a week ago - # Avatar.update_all ['migrated_at = ?', Time.now.utc], ['migrated_at > ?', 1.week.ago] + # Book.where('title LIKE ?', '%Rails%').update_all(author: 'David') # # # Update all books that match conditions, but limit it to 5 ordered by date - # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5 - # - # # Conditions from the current relation also works - # Book.where('title LIKE ?', '%Rails%').update_all(:author => 'David') - # - # # The same idea applies to limit and order # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David') - def update_all(updates, conditions = nil, options = {}) - IdentityMap.repository[symbolized_base_class].clear if IdentityMap.enabled? - if conditions || options.present? - where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) - else - stmt = Arel::UpdateManager.new(arel.engine) + def update_all(updates) + raise ArgumentError, "Empty list of attributes to change" if updates.blank? - stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) - stmt.table(table) - stmt.key = table[primary_key] + stmt = Arel::UpdateManager.new(arel.engine) - if joins_values.any? - @klass.connection.join_to_update(stmt, arel) - else - stmt.take(arel.limit) - stmt.order(*arel.orders) - stmt.wheres = arel.constraints - end + stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.table(table) + stmt.key = table[primary_key] - @klass.connection.update stmt, 'SQL', bind_values + if joins_values.any? + @klass.connection.join_to_update(stmt, arel) + else + stmt.take(arel.limit) + stmt.order(*arel.orders) + stmt.wheres = arel.constraints end + + @klass.connection.update stmt, 'SQL', bind_values end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -306,14 +289,14 @@ module ActiveRecord # ==== Examples # # # Updates one record - # Person.update(15, :user_name => 'Samuel', :group => 'expert') + # Person.update(15, user_name: 'Samuel', group: 'expert') # # # Updates multiple records # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } # Person.update(people.keys, people.values) def update(id, attributes) if id.is_a?(Array) - id.each.with_index.map {|one_id, idx| update(one_id, attributes[idx])} + id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } else object = find(id) object.update_attributes(attributes) @@ -346,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 @@ -383,17 +366,12 @@ module ActiveRecord end end - # Deletes the records matching +conditions+ without instantiating the records first, and hence not - # calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that - # goes straight to the database, much more efficient than +destroy_all+. Be careful with relations - # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns - # the number of rows affected. - # - # ==== Parameters - # - # * +conditions+ - Conditions are specified the same way as with +find+ method. - # - # ==== Example + # Deletes the records matching +conditions+ without instantiating the records + # first, and hence not calling the +destroy+ method nor invoking callbacks. This + # is a single SQL DELETE statement that goes straight to the database, much more + # efficient than +destroy_all+. Be careful with relations though, in particular + # <tt>:dependent</tt> rules defined on associations are not honored. Returns the + # number of rows affected. # # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) @@ -402,13 +380,27 @@ module ActiveRecord # Both calls delete the affected posts all at once with a single DELETE statement. # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. + # + # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error: + # + # Post.limit(100).delete_all + # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope def delete_all(conditions = nil) - IdentityMap.repository[symbolized_base_class] = {} if IdentityMap.enabled? + raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value + if conditions where(conditions).delete_all else - statement = arel.compile_delete - affected = @klass.connection.delete(statement, 'SQL', bind_values) + stmt = Arel::DeleteManager.new(arel.engine) + stmt.from(table) + + if joins_values.any? + @klass.connection.join_to_delete(stmt, arel, table[primary_key]) + else + stmt.wheres = arel.constraints + end + + affected = @klass.connection.delete(stmt, 'SQL', bind_values) reset affected @@ -436,14 +428,35 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) - IdentityMap.remove_by_id(self.symbolized_base_class, id_or_array) if IdentityMap.enabled? 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 @@ -453,26 +466,40 @@ 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) + @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 } - Hash[equalities.map { |where| [where.left.name, where.right] }] + binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] + + Hash[equalities.map { |where| + name = where.left.name + [name, binds.fetch(name.to_s) { where.right }] + }] end def scope_for_create @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? || - @includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?) + eager_load_values.any? || + includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?) end # Joins that are also marked for preloading. In which case we should just eager load them. @@ -480,9 +507,10 @@ module ActiveRecord # represent the same association, but that aren't matched by this. Also, we could have # nested hashes which partially match, e.g. { :a => :b } & { :a => [:b, :c] } def joined_includes_values - @includes_values & @joins_values + includes_values & joins_values end + # Compares two relations for equality. def ==(other) case other when Relation @@ -492,10 +520,6 @@ module ActiveRecord end end - def inspect - to_a.inspect - end - def pretty_print(q) q.pp(self.to_a) end @@ -510,8 +534,48 @@ module ActiveRecord end end + # Returns true if relation is blank. + def blank? + to_a.blank? + end + + def values + @values.dup + end + + def inspect + entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect) + entries[10] = '...' if entries.size == 11 + + "#<#{self.class.name} [#{entries.join(', ')}]>" + end + private + def exec_queries + default_scoped = with_default_scope + + if default_scoped.equal?(self) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) + + preload = preload_values + preload += includes_values unless eager_loading? + preload.each do |associations| + ActiveRecord::Associations::Preloader.new(@records, associations).run + end + + # @readonly_value is true only if set explicitly. @implicit_readonly is true if there + # are JOINS and no explicit SELECT. + readonly = readonly_value.nil? ? @implicit_readonly : readonly_value + @records.each { |record| record.readonly! } if readonly + else + @records = default_scoped.to_a + end + + @loaded = true + @records + end + def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| if join.is_a?(Arel::Nodes::StringJoin) diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 2fd89882ff..4d14506965 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,21 +1,26 @@ -require 'active_support/core_ext/object/blank' module ActiveRecord module Batches - # Yields each record that was found by the find +options+. The find is - # performed by find_in_batches with a batch size of 1000 (or as - # specified by the <tt>:batch_size</tt> option). + # Looping through a collection of records from the database + # (using the +all+ method, for example) is very inefficient + # since it will try to instantiate all the objects at once. # - # Example: + # In that case, batch processing methods allow you to work + # with the records in batches, thereby greatly reducing memory consumption. + # + # 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 + # end # # Person.where("age > 21").find_each do |person| # person.party_all_night! # end # - # Note: This method is only intended to use for batch processing of - # large amounts of records that wouldn't fit in memory all at once. If - # you just need to loop over less than 1000 records, it's probably - # better just to use the regular find methods. + # 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| records.each { |record| yield record } @@ -23,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 @@ -39,31 +44,29 @@ module ActiveRecord # primary keys. You can't set the limit either, that's used to control # the batch sizes. # - # Example: - # # Person.where("age > 21").find_in_batches do |group| # sleep(50) # Make sure it doesn't get too crowded in there! # group.each { |person| person.party_all_night! } # end + # + # # Let's process the next 2000 records + # Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group| + # group.each { |person| person.party_all_night! } + # end def find_in_batches(options = {}) + options.assert_valid_keys(:start, :batch_size) + relation = self unless arel.orders.blank? && arel.taken.blank? ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end - if (finder_options = options.except(:start, :batch_size)).present? - raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order].present? - raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit].present? - - relation = apply_finder_options(finder_options) - end - start = options.delete(:start).to_i 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 50239f7cb2..7c43d844d0 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -1,61 +1,30 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' module ActiveRecord module Calculations - # Count operates using three different approaches. - # - # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. - # * Count using column: By passing a column name to count, it will return a count of all the - # rows for the model with supplied column present. - # * Count using options will find the row count matched by the options used. - # - # The third approach, count using options, accepts an option hash as the only parameter. The options are: - # - # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. - # See conditions in the intro to ActiveRecord::Base. - # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" - # (rarely needed) or named associations in the same form used for the <tt>:include</tt> option, which will - # perform an INNER JOIN on the associated table(s). If the value is a string, then the records - # will be returned read-only since they will have attributes that do not correspond to the table's columns. - # Pass <tt>:readonly => false</tt> to override. - # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. - # The symbols named refer to already defined associations. When using named associations, count - # returns the number of DISTINCT items for the model you're counting. - # See eager loading under Associations. - # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). - # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, - # want to do a join but not include the joined columns. - # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as - # SELECT COUNT(DISTINCT posts.id) ... - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an - # alternate table name (or even the name of a database view). - # - # Examples for counting all: - # Person.count # returns the total count of all people - # - # Examples for counting by column: - # Person.count(:age) # returns the total count of all people whose age is present in database - # - # Examples for count with options: - # Person.count(:conditions => "age > 26") - # - # # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. - # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) - # - # # finds the number of rows matching the conditions and joins. - # Person.count(:conditions => "age > 26 AND job.salary > 60000", - # :joins => "LEFT JOIN jobs on jobs.person_id = person.id") - # - # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id) - # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*') - # - # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. - # Use Person.count instead. + # Count the records. + # + # Person.count + # # => the total count of all people + # + # Person.count(:age) + # # => returns the total count of all people whose age is present in database + # + # Person.count(:all) + # # => performs a COUNT(*) (:all is an alias for '*') + # + # Person.count(:age, distinct: true) + # # => counts the number of different age values + # + # Person.where("age > 26").count { |person| person.gender == 'female' } + # # => queries people where "age > 26" then count the loaded results filtering by gender def count(column_name = nil, options = {}) - column_name, options = nil, column_name if column_name.is_a?(Hash) - calculate(:count, column_name, options) + if block_given? + self.to_a.count { |item| yield item } + else + column_name, options = nil, column_name if column_name.is_a?(Hash) + calculate(:count, column_name, options) + end end # Calculates the average value on a given column. Returns +nil+ if there's @@ -89,30 +58,35 @@ module ActiveRecord # +calculate+ for examples with options. # # Person.sum('age') # => 4562 + # # => returns the total sum of all people's age + # + # Person.where('age > 100').sum { |person| person.age - 100 } + # # queries people where "age > 100" then perform a sum calculation with the block returns def sum(*args) if block_given? - self.to_a.sum(*args) {|*block_args| yield(*block_args)} + self.to_a.sum(*args) { |item| yield item } else calculate(:sum, *args) end end # This calculates aggregate values in the given column. Methods for count, sum, average, - # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>, - # <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query. + # minimum, and maximum have been added as shortcuts. # # There are two basic forms of output: + # # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float # for AVG, and the given column's type for everything else. - # * Grouped values: This returns an ordered hash of the values and groups them by the - # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association. # - # values = Person.maximum(:age, :group => 'last_name') + # * Grouped values: This returns an ordered hash of the values and groups them. It + # takes either a column name, or the name of a belongs_to association. + # + # values = Person.group('last_name').maximum(:age) # puts values["Drake"] # => 43 # # drake = Family.find_by_last_name('Drake') - # values = Person.maximum(:age, :group => :family) # Person belongs_to :family + # values = Person.group(:family).maximum(:age) # Person belongs_to :family # puts values[drake] # => 43 # @@ -120,73 +94,113 @@ module ActiveRecord # ... # end # - # Options: - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. - # See conditions in the intro to ActiveRecord::Base. - # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, - # the purpose of this is to access fields on joined tables in your conditions, order, or group clauses. - # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". - # (Rarely needed). - # The records will be returned read-only since they will have attributes that do not correspond to the - # table's columns. - # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). - # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example - # want to do a join, but not include the joined columns. - # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as - # SELECT COUNT(DISTINCT posts.id) ... - # # Examples: # Person.calculate(:count, :all) # The same as Person.count # Person.average(:age) # SELECT AVG(age) FROM people... - # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for - # # everyone with a last name other than 'Drake' # # # Selects the minimum age for any family without any minors - # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) + # Person.group(:last_name).having("min(age) > 17").minimum(:age) # # Person.sum("2 * age") def calculate(operation, column_name, options = {}) - if options.except(:distinct).present? - apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct]) - else - relation = with_default_scope + relation = with_default_scope - if relation.equal?(self) - if eager_loading? || (includes_values.present? && references_eager_loaded_tables?) - construct_relation_for_association_calculations.calculate(operation, column_name, options) - else - perform_calculation(operation, column_name, options) - end + if relation.equal?(self) + if has_include?(column_name) + construct_relation_for_association_calculations.calculate(operation, column_name, options) else - relation.calculate(operation, column_name, options) + perform_calculation(operation, column_name, options) end + else + relation.calculate(operation, column_name, options) end rescue ThrowResult 0 end - # This method is designed to perform select by a single column as direct SQL query - # Returns <tt>Array</tt> with values of the specified column name - # The values has same data type as column. + # Use <tt>pluck</tt> as a shortcut to select a single attribute without + # loading a bunch of records just to grab one attribute you want. + # + # Person.pluck(:name) + # + # instead of + # + # Person.all.map(&:name) + # + # Pluck returns an <tt>Array</tt> of attribute values type-casted to match + # the plucked column name, if it can be deduced. Plucking an SQL fragment + # returns String values by default. # # Examples: # - # Person.pluck(:id) # SELECT people.id FROM people - # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people - # Person.where(:confirmed => true).limit(5).pluck(:id) + # Person.pluck(:id) + # # SELECT people.id FROM people + # # => [1, 2, 3] + # + # Person.pluck(:id, :name) + # # SELECT people.id, people.name FROM people + # # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] + # + # Person.uniq.pluck(:role) + # # SELECT DISTINCT role FROM people + # # => ['admin', 'member', 'guest'] + # + # Person.where(:age => 21).limit(5).pluck(:id) + # # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5 + # # => [2, 3] + # + # Person.pluck('DATEDIFF(updated_at, created_at)') + # # SELECT DATEDIFF(updated_at, created_at) FROM people + # # => ['0', '27761', '173'] # - def pluck(column_name) - if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s) - column_name = "#{table_name}.#{column_name}" + def pluck(*column_names) + column_names.map! do |column_name| + if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s) + "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}" + else + column_name + end end - klass.connection.select_all(select(column_name).arel).map! do |attributes| - klass.type_cast_attribute(attributes.keys.first, klass.initialize_attributes(attributes)) + + if has_include?(column_names.first) + construct_relation_for_association_calculations.pluck(*column_names) + else + result = klass.connection.select_all(select(column_names).arel, nil, bind_values) + columns = result.columns.map do |key| + klass.column_types.fetch(key) { + result.column_types.fetch(key) { + Class.new { def type_cast(v); v; end }.new + } + } + end + + result = result.map do |attributes| + values = klass.initialize_attributes(attributes).values + + columns.zip(values).map do |column, value| + column.type_cast(value) + end + end + columns.one? ? result.map!(&:first) : result end end + # Pluck all the ID's for the relation using the table's primary key + # + # Examples: + # + # Person.ids # SELECT people.id FROM people + # Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id + def ids + pluck primary_key + end + private + def has_include?(column_name) + eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) + end + def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase @@ -204,7 +218,7 @@ module ActiveRecord distinct = nil if column_name =~ /\s*DISTINCT\s+/i end - if @group_values.any? + if group_values.any? execute_grouped_calculation(operation, column_name, distinct) else execute_simple_calculation(operation, column_name, distinct) @@ -242,14 +256,21 @@ module ActiveRecord query_builder = relation.arel end - type_cast_calculated_value(@klass.connection.select_value(query_builder), column_for(column_name), operation) + result = @klass.connection.select_value(query_builder, nil, relation.bind_values) + type_cast_calculated_value(result, column_for(column_name), operation) end def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: - group_attr = @group_values - association = @klass.reflect_on_association(group_attr.first.to_sym) - associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations - group_fields = Array(associated ? association.foreign_key : group_attr) + group_attrs = group_values + + if group_attrs.first.respond_to?(:to_sym) + association = @klass.reflect_on_association(group_attrs.first.to_sym) + associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + group_fields = Array(associated ? association.foreign_key : group_attrs) + else + group_fields = group_attrs + end + group_aliases = group_fields.map { |field| column_alias_for(field) } group_columns = group_aliases.zip(group_fields).map { |aliaz,field| [aliaz, column_for(field)] @@ -269,16 +290,20 @@ module ActiveRecord operation, distinct).as(aggregate_alias) ] - select_values += @select_values unless @having_values.empty? + select_values += select_values unless having_values.empty? select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| - "#{field} AS #{aliaz}" + if field.respond_to?(:as) + field.as(aliaz) + else + "#{field} AS #{aliaz}" + end } - relation = except(:group).group(group.join(',')) + relation = except(:group).group(group) relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation) + calculated_data = @klass.connection.select_all(relation, nil, bind_values) if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } @@ -286,11 +311,11 @@ module ActiveRecord key_records = Hash[key_records.map { |r| [r.id, r] }] end - ActiveSupport::OrderedHash[calculated_data.map do |row| - key = group_columns.map { |aliaz, column| + Hash[calculated_data.map do |row| + key = group_columns.map { |aliaz, column| type_cast_calculated_value(row[aliaz], column) } - key = key.first if key.size == 1 + key = key.first if key.size == 1 key = key_records[key] if associated [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)] end] @@ -305,6 +330,7 @@ module ActiveRecord # column_alias_for("count(*)") # => "count_all" # column_alias_for("count", "id") # => "count_id" def column_alias_for(*keys) + keys.map! {|k| k.respond_to?(:to_sql) ? k.to_sql : k} table_name = keys.join(' ') table_name.downcase! table_name.gsub!(/\*/, 'all') @@ -316,7 +342,7 @@ module ActiveRecord end def column_for(field) - field_name = field.to_s.split('.').last + field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last @klass.columns.detect { |c| c.name.to_s == field_name } end @@ -334,8 +360,8 @@ module ActiveRecord end def select_for_count - if @select_values.present? - select = @select_values.join(", ") + if select_values.present? + select = select_values.join(", ") select if select !~ /[,*]/ end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index f5fdf437bf..ab8b36c8ab 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,7 +1,6 @@ -require 'active_support/core_ext/module/delegation' module ActiveRecord - module Delegation + module Delegation # :nodoc: # Set up common delegations for performance (avoids method_missing) delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, @@ -32,12 +31,12 @@ module ActiveRecord protected def method_missing(method, *args, &block) - if Array.method_defined?(method) - ::ActiveRecord::Delegation.delegate method, :to => :to_a - to_a.send(method, *args, &block) - elsif @klass.respond_to?(method) + if @klass.respond_to?(method) ::ActiveRecord::Delegation.delegate_to_scoped_klass(method) scoping { @klass.send(method, *args, &block) } + elsif Array.method_defined?(method) + ::ActiveRecord::Delegation.delegate method, :to => :to_a + to_a.send(method, *args, &block) elsif arel.respond_to?(method) ::ActiveRecord::Delegation.delegate method, :to => :arel arel.send(method, *args, &block) @@ -46,4 +45,4 @@ module ActiveRecord end end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index f1ac421a50..84aaa39fed 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,85 +1,23 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord module FinderMethods - # Find operates with four different retrieval approaches: + # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). + # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key + # is an integer, find by id coerces its arguments using +to_i+. # - # * Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). - # If no record can be found for all of the listed ids, then RecordNotFound will be raised. - # * Find first - This will return the first record matched by the options used. These options can either be specific - # conditions or merely an order. If no record can be matched, +nil+ is returned. Use - # <tt>Model.find(:first, *args)</tt> or its shortcut <tt>Model.first(*args)</tt>. - # * Find last - This will return the last record matched by the options used. These options can either be specific - # conditions or merely an order. If no record can be matched, +nil+ is returned. Use - # <tt>Model.find(:last, *args)</tt> or its shortcut <tt>Model.last(*args)</tt>. - # * Find all - This will return all the records matched by the options used. - # If no records are found, an empty array is returned. Use - # <tt>Model.find(:all, *args)</tt> or its shortcut <tt>Model.all(*args)</tt>. - # - # All approaches accept an options hash as their last parameter. - # - # ==== Options - # - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>["user_name = ?", username]</tt>, - # or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro. - # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name". - # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # * <tt>:having</tt> - 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. - # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned. - # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, - # it would skip rows 0 through 4. - # * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed), - # named associations in the same form used for the <tt>:include</tt> option, which will perform an - # <tt>INNER JOIN</tt> on the associated table(s), - # or an array containing a mixture of both strings and named associations. - # If the value is a string, then the records will be returned read-only since they will - # have attributes that do not correspond to the table's columns. - # Pass <tt>:readonly => false</tt> to override. - # * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer - # to already defined associations. See eager loading under Associations. - # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, - # for example, want to do a join but not include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name"). - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed - # to an alternate table name (or even the name of a database view). - # * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated. - # * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". - # <tt>:lock => true</tt> gives connection's default exclusive lock, usually "FOR UPDATE". - # - # ==== Examples - # - # # find by id # Person.find(1) # returns the object for ID = 1 + # Person.find("1") # returns the object for ID = 1 # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # # Note that returned records may not be in the same order as the ids you - # provide since database rows are unordered. Give an explicit <tt>:order</tt> + # provide since database rows are unordered. Give an explicit <tt>order</tt> # to ensure the results are sorted. # - # ==== Examples - # - # # find first - # Person.first # returns the first object fetched by SELECT * FROM people - # Person.where(["user_name = ?", user_name]).first - # Person.where(["user_name = :u", { :u => user_name }]).first - # Person.order("created_on DESC").offset(5).first - # - # # find last - # Person.last # returns the last object fetched by SELECT * FROM people - # Person.where(["user_name = ?", user_name]).last - # Person.order("created_on DESC").offset(5).last - # - # # find all - # 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 + # ==== Find with lock # # Example for find with a lock: Imagine two concurrent transactions: # each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting @@ -93,30 +31,62 @@ module ActiveRecord # person.save! # end def find(*args) - return to_a.find { |*block_args| yield(*block_args) } if block_given? - - options = args.extract_options! - - if options.present? - apply_finder_options(options).find(*args) + if block_given? + to_a.find { |*block_args| yield(*block_args) } else - case args.first - when :first, :last, :all - send(args.first) - else - find_with_ids(*args) - end + find_with_ids(*args) end end - # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the - # same arguments to this method as you can to <tt>find(:first)</tt>. - def first(*args) - if args.any? - if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) - limit(*args).to_a + # Finds the first record matching the specified conditions. There + # is no implied ording so if order matters, you should specify it + # yourself. + # + # If no record is found, returns <tt>nil</tt>. + # + # Post.find_by name: 'Spartacus', rating: 4 + # Post.find_by "published_at < ?", 2.weeks.ago + def find_by(*args) + where(*args).take + end + + # Like <tt>find_by</tt>, except that if no record is found, raises + # an <tt>ActiveRecord::RecordNotFound</tt> error. + def find_by!(*args) + where(*args).take! + end + + # Gives a record (or N records if a parameter is supplied) without any implied + # order. The order will depend on the database implementation. + # If an order is supplied it will be respected. + # + # Person.take # returns an object fetched by SELECT * FROM people + # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5 + # Person.where(["name LIKE '%?'", name]).take + def take(limit = nil) + limit ? limit(limit).to_a : find_take + end + + # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. Note that <tt>take!</tt> accepts no arguments. + def take! + take or raise RecordNotFound + end + + # Find the first record (or first N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # + # Person.first # returns the first object fetched by SELECT * FROM people + # Person.where(["user_name = ?", user_name]).first + # Person.where(["user_name = :u", { :u => user_name }]).first + # Person.order("created_on DESC").offset(5).first + # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3 + def first(limit = nil) + if limit + if order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(limit).to_a else - apply_finder_options(args.first).first + limit(limit).to_a end else find_first @@ -129,18 +99,27 @@ module ActiveRecord first or raise RecordNotFound end - # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the - # same arguments to this method as you can to <tt>find(:last)</tt>. - def last(*args) - if args.any? - if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) - if order_values.empty? - order("#{primary_key} DESC").limit(*args).reverse - else - to_a.last(*args) - end + # Find the last record (or last N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # + # Person.last # returns the last object fetched by SELECT * FROM people + # Person.where(["user_name = ?", user_name]).last + # Person.order("created_on DESC").offset(5).last + # Person.last(3) # returns the last three objects fetched by SELECT * FROM people. + # + # Take note that in that last case, the results are sorted in ascending order: + # + # [#<Person id:2>, #<Person id:3>, #<Person id:4>] + # + # and not: + # + # [#<Person id:4>, #<Person id:3>, #<Person id:2>] + def last(limit = nil) + if limit + if order_values.empty? && primary_key + order(arel_table[primary_key].desc).limit(limit).reverse else - apply_finder_options(args.first).last + to_a.last(limit) end else find_last @@ -153,14 +132,8 @@ module ActiveRecord last or raise RecordNotFound end - # A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the - # same arguments to this method as you can to <tt>find(:all)</tt>. - def all(*args) - args.any? ? apply_finder_options(args.first).to_a : to_a - end - - # Returns true if a record exists in the table that matches the +id+ or - # conditions given, or false otherwise. The argument can take five forms: + # Returns +true+ if a record exists in the table that matches the +id+ or + # conditions given, or +false+ otherwise. The argument can take six forms: # # * Integer - Finds the record with this primary key. # * String - Finds the record with a primary key corresponding to this @@ -168,8 +141,9 @@ module ActiveRecord # * Array - Finds the record that matches these +find+-style conditions # (such as <tt>['color = ?', 'red']</tt>). # * Hash - Finds the record that matches these +find+-style conditions - # (such as <tt>{:color => 'red'}</tt>). - # * No args - Returns false if the table is empty, true otherwise. + # (such as <tt>{color: 'red'}</tt>). + # * +false+ - Returns always +false+. + # * No args - Returns +false+ if the table is empty, +true+ otherwise. # # For more information about specifying conditions as a Hash or Array, # see the Conditions section in the introduction to ActiveRecord::Base. @@ -178,29 +152,30 @@ module ActiveRecord # 'Jamie'</tt>), since it would be sanitized and then queried against # the primary key column, like <tt>id = 'name = \'Jamie\''</tt>. # - # ==== Examples # Person.exists?(5) # Person.exists?('5') - # Person.exists?(:name => "David") # Person.exists?(['name LIKE ?', "%#{query}%"]) + # Person.exists?(name: 'David') + # Person.exists?(false) # Person.exists? - def exists?(id = false) - return false if id.nil? - - id = id.id if ActiveRecord::Model === id + def exists?(conditions = :none) + conditions = conditions.id if ActiveRecord::Model === conditions + return false if !conditions join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) - relation = relation.except(:select, :order).select("1").limit(1) + relation = relation.except(:select, :order).select("1 AS one").limit(1) - case id + case conditions when Array, Hash - relation = relation.where(id) + relation = relation.where(conditions) else - relation = relation.where(table[primary_key].eq(id)) if id + relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none end - connection.select_value(relation, "#{name} Exists") ? true : false + connection.select_value(relation, "#{name} Exists", relation.bind_values) + rescue ThrowResult + false end protected @@ -208,19 +183,19 @@ module ActiveRecord def find_with_associations join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) - rows = connection.select_all(relation, 'SQL', relation.bind_values) + rows = connection.select_all(relation, 'SQL', relation.bind_values.dup) join_dependency.instantiate(rows) rescue ThrowResult [] end def construct_join_dependency_for_association_find - including = (@eager_load_values + @includes_values).uniq + including = (eager_load_values + includes_values).uniq ActiveRecord::Associations::JoinDependency.new(@klass, including, []) end def construct_relation_for_association_calculations - including = (@eager_load_values + @includes_values).uniq + including = (eager_load_values + includes_values).uniq join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first) relation = except(:includes, :eager_load, :preload) apply_join_dependency(relation, join_dependency) @@ -258,44 +233,6 @@ module ActiveRecord ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end - def find_by_attributes(match, attributes, *args) - conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}] - result = where(conditions).send(match.finder) - - if match.bang? && result.blank? - raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}" - else - yield(result) if block_given? - result - end - end - - def find_or_instantiator_by_attributes(match, attributes, *args) - options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {} - protected_attributes_for_create, unprotected_attributes_for_create = {}, {} - args.each_with_index do |arg, i| - if arg.is_a?(Hash) - protected_attributes_for_create = args[i].with_indifferent_access - else - unprotected_attributes_for_create[attributes[i]] = args[i] - end - end - - conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys - - record = where(conditions).first - - unless record - record = @klass.new(protected_attributes_for_create, options) do |r| - r.assign_attributes(unprotected_attributes_for_create, :without_protection => true) - end - yield(record) if block_given? - record.save if match.instantiator == :create - end - - record - end - def find_with_ids(*ids) return to_a.find { |*block_args| yield(*block_args) } if block_given? @@ -318,21 +255,11 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id - if IdentityMap.enabled? && where_values.blank? && - limit_value.blank? && order_values.blank? && - includes_values.blank? && preload_values.blank? && - readonly_value.nil? && joins_values.blank? && - !@klass.locking_enabled? && - record = IdentityMap.get(@klass, id) - return record - end - column = columns_hash[primary_key] - - substitute = connection.substitute_at(column, @bind_values.length) + substitute = connection.substitute_at(column, bind_values.length) relation = where(table[primary_key].eq(substitute)) - relation.bind_values = [[column, id]] - record = relation.first + relation.bind_values += [[column, id]] + record = relation.take unless record conditions = arel.where_sql @@ -344,18 +271,18 @@ 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 - @limit_value + if limit_value && ids.size > limit_value + limit_value else ids.size end # 11 ids with limit 3, offset 9 should give 2 results. - if @offset_value && (ids.size - @offset_value < expected_size) - expected_size = ids.size - @offset_value + if offset_value && (ids.size - offset_value < expected_size) + expected_size = ids.size - offset_value end if result.size == expected_size @@ -370,11 +297,24 @@ module ActiveRecord end end + def find_take + if loaded? + @records.first + else + @take ||= limit(1).to_a.first + end + end + def find_first if loaded? @records.first else - @first ||= limit(1).to_a[0] + @first ||= + 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 + end end end @@ -386,7 +326,7 @@ module ActiveRecord if offset_value || limit_value to_a.last else - reverse_order.limit(1).to_a[0] + reverse_order.limit(1).to_a.first end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb new file mode 100644 index 0000000000..e5b50673da --- /dev/null +++ b/activerecord/lib/active_record/relation/merger.rb @@ -0,0 +1,119 @@ +require 'active_support/core_ext/hash/keys' + +module ActiveRecord + class Relation + class HashMerger # :nodoc: + attr_reader :relation, :hash + + def initialize(relation, hash) + hash.assert_valid_keys(*Relation::VALUE_METHODS) + + @relation = relation + @hash = hash + end + + def merge + Merger.new(relation, other).merge + end + + # Applying values to a relation has some side effects. E.g. + # interpolation might take place for where values. So we should + # build a relation to merge in rather than directly merging + # the values. + def other + other = Relation.new(relation.klass, relation.table) + hash.each { |k, v| other.send("#{k}!", v) } + other + end + end + + class Merger # :nodoc: + attr_reader :relation, :values + + def initialize(relation, other) + if other.default_scoped? && other.klass != relation.klass + other = other.with_default_scope + end + + @relation = relation + @values = other.values + end + + def normal_values + Relation::SINGLE_VALUE_METHODS + + Relation::MULTI_VALUE_METHODS - + [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] + end + + def merge + normal_values.each do |name| + value = values[name] + relation.send("#{name}!", value) unless value.blank? + end + + merge_multi_values + merge_single_values + + relation + end + + private + + def merge_multi_values + relation.where_values = merged_wheres + relation.bind_values = merged_binds + + if values[:reordering] + # override any order specified in the original relation + relation.reorder! values[:order] + elsif values[:order] + # merge in order_values from r + relation.order! values[:order] + end + + relation.extend(*values[:extending]) unless values[:extending].blank? + end + + def merge_single_values + relation.from_value = values[:from] unless relation.from_value + relation.lock_value = values[:lock] unless relation.lock_value + relation.reverse_order_value = values[:reverse_order] + + unless values[:create_with].blank? + relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with]) + end + end + + def merged_binds + if values[:bind] + (relation.bind_values + values[:bind]).uniq(&:first) + else + relation.bind_values + end + end + + def merged_wheres + if values[:where] + merged_wheres = relation.where_values + values[:where] + + unless relation.where_values.empty? + # 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 == :== + nuke = seen[w.left] + seen[w.left] = true + end + nuke + }.reverse + end + + merged_wheres + else + relation.where_values + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 1d04e763f6..cb8f903474 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,12 +1,12 @@ module ActiveRecord class PredicateBuilder # :nodoc: def self.build_from_hash(engine, attributes, default_table) - predicates = attributes.map do |column, value| + attributes.map do |column, value| table = default_table if value.is_a?(Hash) table = Arel::Table.new(column, engine) - build_from_hash(engine, value, table) + value.map { |k,v| build(table[k.to_sym], v) } else column = column.to_s @@ -17,50 +17,49 @@ module ActiveRecord build(table[column.to_sym], value) end - end - predicates.flatten + end.flatten end def self.references(attributes) - references = attributes.map do |key, value| + attributes.map do |key, value| if value.is_a?(Hash) key else key = key.to_s key.split('.').first.to_sym if key.include?('.') end - end - references.compact + end.compact end private def self.build(attribute, value) case value - when ActiveRecord::Relation - value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? - attribute.in(value.arel.ast) when Array, ActiveRecord::Associations::CollectionProxy values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x} - ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)} + ranges, values = values.partition {|v| v.is_a?(Range)} - array_predicates = ranges.map {|range| attribute.in(range)} - - if values.include?(nil) + values_predicate = if values.include?(nil) values = values.compact + case values.length when 0 - array_predicates << attribute.eq(nil) + attribute.eq(nil) when 1 - array_predicates << attribute.eq(values.first).or(attribute.eq(nil)) + attribute.eq(values.first).or(attribute.eq(nil)) else - array_predicates << attribute.in(values).or(attribute.eq(nil)) + attribute.in(values).or(attribute.eq(nil)) end else - array_predicates << attribute.in(values) + attribute.in(values) end - array_predicates.inject {|composite, predicate| composite.or(predicate)} - when Range, Arel::Relation + array_predicates = ranges.map { |range| attribute.in(range) } + array_predicates << values_predicate + array_predicates.inject { |composite, predicate| composite.or(predicate) } + when ActiveRecord::Relation + value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? + attribute.in(value.arel.ast) + when Range attribute.in(value) when ActiveRecord::Model attribute.eq(value.id) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index b6d762c2e2..f6bacf4822 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,66 +1,134 @@ require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/object/blank' module ActiveRecord module QueryMethods extend ActiveSupport::Concern - attr_accessor :includes_values, :eager_load_values, :preload_values, - :select_values, :group_values, :order_values, :joins_values, - :where_values, :having_values, :bind_values, - :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, - :from_value, :reordering_value, :reverse_order_value, - :uniq_value, :references_values + Relation::MULTI_VALUE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_values # def select_values + @values[:#{name}] || [] # @values[:select] || [] + end # end + # + def #{name}_values=(values) # def select_values=(values) + raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + @values[:#{name}] = values # @values[:select] = values + end # end + CODE + end + + (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_value # def readonly_value + @values[:#{name}] # @values[:readonly] + end # end + CODE + end + + Relation::SINGLE_VALUE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_value=(value) # def readonly_value=(value) + raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + @values[:#{name}] = value # @values[:readonly] = value + end # end + CODE + end + def create_with_value # :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.reject! {|a| a.blank? } + args.empty? ? self : spawn.includes!(*args) + end - return self if args.empty? + # Like #includes, but modifies the relation in place. + def includes!(*args) + args.reject! {|a| a.blank? } - relation = clone - relation.includes_values = (relation.includes_values + args).flatten.uniq - relation + self.includes_values = (includes_values + args).flatten.uniq + 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) - return self if args.blank? + args.blank? ? self : spawn.eager_load!(*args) + end - relation = clone - relation.eager_load_values += args - relation + # 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) - return self if args.blank? + args.blank? ? self : spawn.preload!(*args) + end - relation = clone - relation.preload_values += args - relation + # Like #preload, but modifies relation in place. + def preload!(*args) + self.preload_values += args + self end # Used to indicate that an association is referenced by an SQL string, and should # therefore be JOINed in any query rather than loaded separately. # - # For example: - # # User.includes(:posts).where("posts.name = 'foo'") # # => Doesn't JOIN the posts table, resulting in an error. # # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN def references(*args) - return self if args.blank? + args.blank? ? self : spawn.references!(*args) + end + + # Like #references, but modifies relation in place. + def references!(*args) + args.flatten! - relation = clone - relation.references_values = (references_values + args.flatten.map(&:to_s)).uniq - relation + self.references_values = (references_values + args.map!(&:to_s)).uniq + self end # Works in two unique ways. # # 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. @@ -68,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 @@ -77,44 +145,77 @@ 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 - relation = clone - relation.select_values += Array.wrap(value) - relation + raise ArgumentError, 'Call this with at least one field' if fields.empty? + spawn.select!(*fields) end end + # 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) - return self if args.blank? + args.blank? ? self : spawn.group!(*args) + end + + # Like #group, but modifies relation in place. + def group!(*args) + args.flatten! - relation = clone - relation.group_values += args.flatten - relation + self.group_values += args + self end + # Allows to specify an order attribute: + # + # User.order('name') + # => SELECT "users".* FROM "users" ORDER BY name + # + # User.order('name DESC') + # => SELECT "users".* FROM "users" ORDER BY name DESC + # + # User.order('name DESC, email') + # => SELECT "users".* FROM "users" ORDER BY name DESC, email def order(*args) - return self if args.blank? + args.blank? ? self : spawn.order!(*args) + end + + # Like #order, but modifies relation in place. + def order!(*args) + args.flatten! - args = args.flatten references = args.reject { |arg| Arel::Node === arg } - .map { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 } - .compact + references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! + references!(references) if references.any? - relation = clone - relation = relation.references(references) if references.any? - relation.order_values += args - relation + self.order_values = args + self.order_values + self end # Replaces any existing order defined on the relation with the specified order. @@ -125,89 +226,226 @@ 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) - return self if args.blank? + args.blank? ? self : spawn.reorder!(*args) + end - relation = clone - relation.reordering_value = true - relation.order_values = args.flatten - relation + # Like #reorder, but modifies relation in place. + def reorder!(*args) + args.flatten! + + self.reordering_value = true + self.order_values = args + self end + # Performs a joins on +args+: + # + # User.joins(:posts) + # => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" def joins(*args) - return self if args.compact.blank? - - relation = clone + args.compact.blank? ? self : spawn.joins!(*args) + end + # Like #joins, but modifies relation in place. + def joins!(*args) args.flatten! - relation.joins_values += args - relation + self.joins_values += args + self end def bind(value) - relation = clone - relation.bind_values += [value] - relation + spawn.bind!(value) + end + + def bind!(value) + self.bind_values += [value] + self end + # Returns a new relation, which is the result of filtering the current relation + # according to the conditions in the arguments. + # + # #where accepts conditions in one of several formats. In the examples below, the resulting + # SQL is given as an illustration; the actual query generated may be different depending + # on the database adapter. + # + # === string + # + # A single string, without additional arguments, is passed to the query + # constructor as a SQL fragment, and used in the where clause of the query. + # + # Client.where("orders_count = '2'") + # # SELECT * from clients where orders_count = '2'; + # + # Note that building your own string from user input may expose your application + # to injection attacks if not done properly. As an alternative, it is recommended + # to use one of the following methods. + # + # === array + # + # If an array is passed, then the first element of the array is treated as a template, and + # the remaining elements are inserted into the template to generate the condition. + # Active Record takes care of building the query to avoid injection attacks, and will + # convert from the ruby type to the database type where needed. Elements are inserted + # into the string in the order in which they appear. + # + # User.where(["name = ? and email = ?", "Joe", "joe@example.com"]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # Alternatively, you can use named placeholders in the template, and pass a hash as the + # second element of the array. The names in the template are replaced with the corresponding + # values from the hash. + # + # User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # This can make for more readable code in complex queries. + # + # Lastly, you can use sprintf-style % escapes in the template. This works slightly differently + # than the previous methods; you are responsible for ensuring that the values in the template + # are properly quoted. The values are passed to the connector for quoting, but the caller + # is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting, + # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>. + # + # User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"]) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # If #where is called with multiple arguments, these are treated as if they were passed as + # the elements of a single array. + # + # User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" }) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'; + # + # When using strings to specify conditions, you can use any operator available from + # the database. While this provides the most flexibility, you can also unintentionally introduce + # dependencies on the underlying database. If your code is intended for general consumption, + # test with multiple database backends. + # + # === hash + # + # #where will also accept a hash condition, in which the keys are fields and the values + # are values to be searched for. + # + # Fields can be symbols or strings. Values can be single values, arrays, or ranges. + # + # User.where({ name: "Joe", email: "joe@example.com" }) + # # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com' + # + # User.where({ name: ["Alice", "Bob"]}) + # # SELECT * FROM users WHERE name IN ('Alice', 'Bob') + # + # User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight }) + # # SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000') + # + # === Joins + # + # If the relation is the result of a join, you may create a condition which uses any of the + # tables in the join. For string and array conditions, use the table name in the condition. + # + # User.joins(:posts).where("posts.created_at < ?", Time.now) + # + # For hash conditions, you can either use the table name in the key, or use a sub-hash. + # + # User.joins(:posts).where({ "posts.published" => true }) + # User.joins(:posts).where({ :posts => { :published => true } }) + # + # === empty condition + # + # If the condition returns true for blank?, then where is a no-op and returns the current relation. def where(opts, *rest) - return self if opts.blank? + opts.blank? ? self : spawn.where!(opts, *rest) + end - relation = clone - relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts - relation.where_values += build_where(opts, rest) - relation + # #where! is identical to #where, except that instead of returning a new relation, it adds + # the condition to the existing relation. + def where!(opts, *rest) + references!(PredicateBuilder.references(opts)) if Hash === opts + + self.where_values += build_where(opts, rest) + 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) - return self if opts.blank? + 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 - relation = clone - relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts - relation.having_values += build_where(opts, rest) - relation + self.having_values += build_where(opts, rest) + self end + # Specifies a limit for the number of records to retrieve. + # + # User.limit(10) # generated SQL has 'LIMIT 10' + # + # User.limit(10).limit(20) # generated SQL has 'LIMIT 20' def limit(value) - relation = clone - relation.limit_value = value - relation + spawn.limit!(value) + end + + # Like #limit, but modifies relation in place. + def limit!(value) + self.limit_value = value + self end + # Specifies the number of rows to skip before returning rows. + # + # User.offset(10) # generated SQL has "OFFSET 10" + # + # Should be used with order. + # + # User.offset(10).order("name ASC") def offset(value) - relation = clone - relation.offset_value = value - relation + 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) - relation = clone + spawn.lock!(locks) + end + # Like #lock, but modifies relation in place. + def lock!(locks = true) case locks when String, TrueClass, NilClass - relation.lock_value = locks || true + self.lock_value = locks || true else - relation.lock_value = false + self.lock_value = false end - relation + self 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. # - # This is useful in scenarios where you need a chainable response to a method - # or a scope that could return zero results. + # Used in cases where a method or scope could return zero records but the + # result needs to be chainable. # # For example: # @@ -226,25 +464,73 @@ module ActiveRecord # end # def none - NullRelation.new(@klass, @table) + 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) - relation = clone - relation.readonly_value = value - relation + 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) - relation = clone - relation.create_with_value = value ? create_with_value.merge(value) : {} - relation + 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 end - def from(value) - relation = clone - relation.from_value = value - relation + # Specifies table from which the records will be fetched. For example: + # + # Topic.select('title').from('posts') + # #=> SELECT title FROM posts + # + # Can accept other relation objects. For example: + # + # Topic.select('title').from(Topic.approved) + # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery + # + # Topic.select('a.title').from(Topic.approved, :a) + # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a + # + def from(value, subquery_name = nil) + spawn.from!(value, subquery_name) + end + + # Like #from, but modifies relation in place. + def from!(value, subquery_name = nil) + self.from_value = [value, subquery_name] + self end # Specifies whether the records should be unique or not. For example: @@ -258,9 +544,13 @@ module ActiveRecord # User.select(:name).uniq.uniq(false) # # => You can also remove the uniqueness def uniq(value = true) - relation = clone - relation.uniq_value = value - relation + spawn.uniq!(value) + end + + # Like #uniq, but modifies relation in place. + def uniq!(value = true) + self.uniq_value = value + self end # Used to extend a scope with additional methods, either through @@ -276,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 @@ -294,54 +584,71 @@ 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 # end - def extending(*modules) - modules << Module.new(&Proc.new) if block_given? + def extending(*modules, &block) + if modules.any? || block + spawn.extending!(*modules, &block) + else + self + end + end - return self if modules.empty? + # Like #extending, but modifies relation in place. + def extending!(*modules, &block) + modules << Module.new(&block) if block_given? - relation = clone - relation.send(:apply_modules, modules.flatten) - relation + self.extending_values += modules.flatten + extend(*extending_values) if extending_values.any? + + self end + # Reverse the existing order clause on the relation. + # + # User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC' def reverse_order - relation = clone - relation.reverse_order_value = !relation.reverse_order_value - relation + 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? + build_joins(arel, joins_values) unless joins_values.empty? - collapse_wheres(arel, (@where_values - ['']).uniq) + collapse_wheres(arel, (where_values - ['']).uniq) - arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty? + arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty? - arel.take(connection.sanitize_limit(@limit_value)) if @limit_value - arel.skip(@offset_value) if @offset_value + arel.take(connection.sanitize_limit(limit_value)) if limit_value + arel.skip(offset_value.to_i) if offset_value - arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? + arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty? - order = @order_values - order = reverse_sql_order(order) if @reverse_order_value + order = order_values + order = reverse_sql_order(order) if reverse_order_value arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty? - build_select(arel, @select_values.uniq) + build_select(arel, select_values.uniq) - arel.distinct(@uniq_value) - arel.from(@from_value) if @from_value - arel.lock(@lock_value) if @lock_value + arel.distinct(uniq_value) + arel.from(build_from) if from_value + arel.lock(lock_value) if lock_value arel end @@ -389,6 +696,17 @@ module ActiveRecord end end + def build_from + opts, name = from_value + case opts + when Relation + name ||= 'subquery' + opts.arel.as(name.to_s) + else + opts + end + end + def build_joins(manager, joins) buckets = joins.group_by do |join| case join @@ -443,13 +761,6 @@ module ActiveRecord end end - def apply_modules(modules) - unless modules.empty? - @extensions += modules - modules.each {|extension| extend(extension) } - end - end - def reverse_sql_order(order_query) order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 7131aa29b6..5394c1b28b 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,75 +1,53 @@ -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' module ActiveRecord module SpawnMethods - def merge(r) - return self unless r - return to_a & r if r.is_a?(Array) - merged_relation = clone - - r = r.with_default_scope if r.default_scoped? && r.klass != klass - - Relation::ASSOCIATION_METHODS.each do |method| - value = r.send(:"#{method}_values") - - unless value.empty? - if method == :includes - merged_relation = merged_relation.includes(value) - else - merged_relation.send(:"#{method}_values=", value) - end - end - end - - (Relation::MULTI_VALUE_METHODS - [:joins, :where, :order]).each do |method| - value = r.send(:"#{method}_values") - merged_relation.send(:"#{method}_values=", merged_relation.send(:"#{method}_values") + value) if value.present? - end - - merged_relation.joins_values += r.joins_values - - merged_wheres = @where_values + r.where_values - - unless @where_values.empty? - # Remove duplicates, last one wins. - seen = Hash.new { |h,table| h[table] = {} } - 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 - end - nuke - }.reverse - end - - merged_relation.where_values = merged_wheres + # This is overridden by Associations::CollectionProxy + def spawn #:nodoc: + clone + end - (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with, :reordering]).each do |method| - value = r.send(:"#{method}_value") - merged_relation.send(:"#{method}_value=", value) unless value.nil? + # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>. + # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. + # + # ==== Examples + # + # Post.where(:published => true).joins(:comments).merge( Comment.where(:spam => false) ) + # # Performs a single join query with both where conditions. + # + # recent_posts = Post.order('created_at DESC').first(5) + # Post.where(:published => true).merge(recent_posts) + # # 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 + elsif other + spawn.merge!(other) + else + self end + end - merged_relation.lock_value = r.lock_value unless merged_relation.lock_value - - merged_relation = merged_relation.create_with(r.create_with_value) unless r.create_with_value.empty? - - if (r.reordering_value) - # override any order specified in the original relation - merged_relation.reordering_value = true - merged_relation.order_values = r.order_values + # Like #merge, but applies changes in place. + def merge!(other) + if !other.is_a?(Relation) && other.respond_to?(:to_proc) + instance_exec(&other) else - # merge in order_values from r - merged_relation.order_values += r.order_values + klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger + klass.new(self, other).merge end - - # Apply scope extension modules - merged_relation.send :apply_modules, r.extensions - - merged_relation end # Removes from the query the condition(s) specified in +skips+. @@ -80,20 +58,9 @@ module ActiveRecord # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order # def except(*skips) - result = self.class.new(@klass, table) + result = Relation.new(klass, table, values.except(*skips)) result.default_scoped = default_scoped - - ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method| - result.send(:"#{method}_values=", send(:"#{method}_values")) - end - - (Relation::SINGLE_VALUE_METHODS - skips).each do |method| - result.send(:"#{method}_value=", send(:"#{method}_value")) - end - - # Apply scope extension modules - result.send(:apply_modules, extensions) - + result.extend(*extending_values) if extending_values.any? result end @@ -105,44 +72,11 @@ module ActiveRecord # Post.order('id asc').only(:where, :order) # uses the specified order # def only(*onlies) - result = self.class.new(@klass, table) + result = Relation.new(klass, table, values.slice(*onlies)) result.default_scoped = default_scoped - - ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) & onlies).each do |method| - result.send(:"#{method}_values=", send(:"#{method}_values")) - end - - (Relation::SINGLE_VALUE_METHODS & onlies).each do |method| - result.send(:"#{method}_value=", send(:"#{method}_value")) - end - - # Apply scope extension modules - result.send(:apply_modules, extensions) - + result.extend(*extending_values) if extending_values.any? result end - VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, :references, - :order, :select, :readonly, :group, :having, :from, :lock ] - - def apply_finder_options(options) - relation = clone - return relation unless options - - options.assert_valid_keys(VALID_FIND_OPTIONS) - finders = options.dup - finders.delete_if { |key, value| value.nil? && key != :limit } - - ((VALID_FIND_OPTIONS - [:conditions, :include, :extend]) & finders.keys).each do |finder| - relation = relation.send(finder, finders[finder]) - end - - relation = relation.where(finders[:conditions]) if options.has_key?(:conditions) - relation = relation.includes(finders[:include]) if options.has_key?(:include) - relation = relation.extending(finders[:extend]) if options.has_key?(:extend) - - relation - end - end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 60a2e90e23..2414a4bbd7 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -8,12 +8,13 @@ module ActiveRecord class Result include Enumerable - attr_reader :columns, :rows + attr_reader :columns, :rows, :column_types - def initialize(columns, rows) - @columns = columns - @rows = rows - @hash_rows = nil + def initialize(columns, rows, column_types = {}) + @columns = columns + @rows = rows + @hash_rows = nil + @column_types = column_types end def each @@ -27,6 +28,7 @@ module ActiveRecord alias :map! :map alias :collect! :map + # Returns true if there are no records. def empty? rows.empty? end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 2d7d83d160..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 @@ -37,9 +36,9 @@ module ActiveRecord # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'" def sanitize_sql_for_assignment(assignments) case assignments - when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments) - else assignments + when Array; sanitize_sql_array(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments) + else assignments end end @@ -57,8 +56,8 @@ module ActiveRecord def expand_hash_conditions_for_aggregates(attrs) expanded_attrs = {} attrs.each do |attr, value| - unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil? - mapping = aggregate_mapping(aggregation) + 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 @@ -180,15 +179,8 @@ module ActiveRecord end # TODO: Deprecate this - def quoted_id #:nodoc: - quote_value(id, column_for_attribute(self.class.primary_key)) - end - - private - - # Quote strings appropriately for SQL statements. - def quote_value(value, column = nil) - self.class.connection.quote(value, column) + def quoted_id + self.class.quote_value(id, column_for_attribute(self.class.primary_key)) end end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index d815ab05ac..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 @@ -34,6 +33,15 @@ module ActiveRecord ActiveRecord::Migrator.migrations_paths end + def define(info, &block) + instance_eval(&block) + + unless info[:version].blank? + initialize_schema_migrations_table + assume_migrated_upto_version(info[:version], migrations_paths) + end + end + # Eval the given block. All methods available to the current connection # adapter are available within the block, so you can easily use the # database definition DSL to build up your schema (+create_table+, @@ -46,13 +54,7 @@ module ActiveRecord # ... # end def self.define(info={}, &block) - schema = new - schema.instance_eval(&block) - - unless info[:version].blank? - initialize_schema_migrations_table - assume_migrated_upto_version(info[:version], schema.migrations_paths) - end + new.define(info, &block) end end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 2a565b51c6..a25a8d79bd 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -40,7 +40,7 @@ module ActiveRecord def header(stream) define_params = @version ? ":version => #{@version}" : "" - if stream.respond_to?(:external_encoding) + if stream.respond_to?(:external_encoding) && stream.external_encoding stream.puts "# encoding: #{stream.external_encoding.name}" end @@ -55,7 +55,7 @@ module ActiveRecord # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # -# It's strongly recommended to check this file into your version control system. +# It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(#{define_params}) do @@ -70,8 +70,8 @@ HEADER @connection.tables.sort.each do |tbl| next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| case ignored - when String; tbl == ignored - when Regexp; tbl =~ ignored + when String; remove_prefix_and_suffix(tbl) == ignored + when Regexp; remove_prefix_and_suffix(tbl) =~ ignored else raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' end @@ -92,7 +92,7 @@ HEADER pk = @connection.primary_key(table) end - tbl.print " create_table #{table.inspect}" + tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" if columns.detect { |c| c.name == pk } if pk != 'id' tbl.print %Q(, :primary_key => "#{pk}") @@ -175,7 +175,7 @@ HEADER when BigDecimal value.to_s when Date, DateTime, Time - "'" + value.to_s(:db) + "'" + "'#{value.to_s(:db)}'" else value.inspect end @@ -185,7 +185,7 @@ HEADER if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| statement_parts = [ - ('add_index ' + index.table.inspect), + ('add_index ' + remove_prefix_and_suffix(index.table).inspect), index.columns.inspect, (':name => ' + index.name.inspect), ] @@ -197,6 +197,8 @@ HEADER index_orders = (index.orders || {}) statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty? + statement_parts << (':where => ' + index.where.inspect) if index.where + ' ' + statement_parts.join(', ') end @@ -204,5 +206,9 @@ HEADER stream.puts end end + + def remove_prefix_and_suffix(table) + table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2") + end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 236ec563d2..ca22154c84 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -7,7 +7,11 @@ module ActiveRecord attr_accessible :version def self.table_name - Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix + "#{Base.table_name_prefix}schema_migrations#{Base.table_name_suffix}" + end + + def self.index_name + "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" end def self.create_table @@ -15,14 +19,13 @@ module ActiveRecord connection.create_table(table_name, :id => false) do |t| t.column :version, :string, :null => false end - connection.add_index table_name, :version, :unique => true, - :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" + connection.add_index table_name, :version, :unique => true, :name => index_name end end def self.drop_table if connection.table_exists?(table_name) - connection.remove_index table_name, :name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" + connection.remove_index table_name, :name => index_name connection.drop_table(table_name) end end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index a8f5e96190..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 @@ -10,118 +9,6 @@ module ActiveRecord end module ClassMethods - # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be - # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while - # <tt>:create</tt> parameters are an attributes hash. - # - # class Article < ActiveRecord::Base - # def self.create_with_scope - # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do - # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 - # a = create(1) - # a.blog_id # => 1 - # end - # end - # end - # - # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of - # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged. - # - # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing - # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the - # array of strings format for your joins. - # - # class Article < ActiveRecord::Base - # def self.find_with_scope - # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do - # with_scope(:find => limit(10)) do - # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 - # end - # with_scope(:find => where(:author_id => 3)) do - # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 - # end - # end - # end - # end - # - # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method. - # - # class Article < ActiveRecord::Base - # def self.find_with_exclusive_scope - # with_scope(:find => where(:blog_id => 1).limit(1)) do - # with_exclusive_scope(:find => limit(10)) do - # all # => SELECT * from articles LIMIT 10 - # end - # end - # end - # end - # - # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. - def with_scope(scope = {}, action = :merge, &block) - # If another Active Record class has been passed in, get its current scope - scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope) - - previous_scope = self.current_scope - - if scope.is_a?(Hash) - # Dup first and second level of hash (method and params). - scope = scope.dup - scope.each do |method, params| - scope[method] = params.dup unless params == true - end - - scope.assert_valid_keys([ :find, :create ]) - relation = construct_finder_arel(scope[:find] || {}) - relation.default_scoped = true unless action == :overwrite - - if previous_scope && previous_scope.create_with_value && scope[:create] - scope_for_create = if action == :merge - previous_scope.create_with_value.merge(scope[:create]) - else - scope[:create] - end - - relation = relation.create_with(scope_for_create) - else - scope_for_create = scope[:create] - scope_for_create ||= previous_scope.create_with_value if previous_scope - relation = relation.create_with(scope_for_create) if scope_for_create - end - - scope = relation - end - - scope = previous_scope.merge(scope) if previous_scope && action == :merge - - self.current_scope = scope - begin - yield - ensure - self.current_scope = previous_scope - end - end - - protected - - # Works like with_scope, but discards any nested properties. - def with_exclusive_scope(method_scoping = {}, &block) - if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) } - raise ArgumentError, <<-MSG - New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope: - - User.unscoped.where(:active => true) - - Or call unscoped with a block: - - User.unscoped do - User.where(:active => true).all - end - - MSG - end - with_scope(method_scoping, :overwrite, &block) - end - def current_scope #:nodoc: Thread.current["#{self}_current_scope"] end @@ -129,15 +16,6 @@ module ActiveRecord def current_scope=(scope) #:nodoc: Thread.current["#{self}_current_scope"] = scope end - - private - - def construct_finder_arel(options = {}, scope = nil) - relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options - relation = scope.merge(relation) if scope - relation - end - end def populate_with_current_scope_attributes diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 5f05d146f2..a2a85d4b96 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord module Scoping @@ -7,7 +6,7 @@ module ActiveRecord included do # Stores the default scope for the class - config_attribute :default_scopes + class_attribute :default_scopes, instance_writer: false self.default_scopes = [] end @@ -30,14 +29,14 @@ module ActiveRecord # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } # - # It is recommended to 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 @@ -51,7 +50,7 @@ module ActiveRecord # the model. # # class Article < ActiveRecord::Base - # default_scope where(:published => true) + # default_scope { where(:published => true) } # end # # Article.all # => SELECT * FROM articles WHERE published = true @@ -62,12 +61,6 @@ module ActiveRecord # Article.new.published # => true # Article.create.published # => true # - # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: - # - # class Article < ActiveRecord::Base - # default_scope { where(:published_at => Time.now - 1.week) } - # end - # # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> # macro, and it will be called when building the default scope.) # @@ -75,8 +68,8 @@ module ActiveRecord # be merged together: # # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # default_scope where(:rating => 'G') + # default_scope { where(:published => true) } + # default_scope { where(:rating => 'G') } # end # # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' @@ -92,21 +85,30 @@ module ActiveRecord # # Should return a scope, you can call 'super' here etc. # end # end - def default_scope(scope = {}) + def default_scope(scope = nil) scope = Proc.new if block_given? + + if scope.is_a?(Relation) || !scope.respond_to?(:call) + ActiveSupport::Deprecation.warn( + "Calling #default_scope without a block is deprecated. For example instead " \ + "of `default_scope where(color: 'red')`, please use " \ + "`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \ + "self.default_scope.)" + ) + end + self.default_scopes = default_scopes + [scope] end def build_default_scope #:nodoc: - if method(:default_scope).owner != ActiveRecord::Scoping::Default::ClassMethods + if !Base.is_a?(method(:default_scope).owner) + # The user has defined their own default scope method, so call that evaluate_default_scope { default_scope } elsif default_scopes.any? evaluate_default_scope do default_scopes.inject(relation) do |default_scope, scope| - if scope.is_a?(Hash) - default_scope.apply_finder_options(scope) - elsif !scope.is_a?(Relation) && scope.respond_to?(:call) - default_scope.merge(scope.call) + if !scope.is_a?(Relation) && scope.respond_to?(:call) + default_scope.merge(unscoped { scope.call }) else default_scope.merge(scope) end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 0edc3f1dcc..75f31229b5 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -1,8 +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' module ActiveRecord # = Active Record Named \Scopes @@ -11,33 +9,25 @@ 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) - if options - scoped.apply_finder_options(options) + def all + if current_scope + current_scope.clone else - if current_scope - current_scope.clone - else - scope = relation.clone - scope.default_scoped = true - scope - end + scope = relation + scope.default_scoped = true + scope end end @@ -48,7 +38,7 @@ module ActiveRecord if current_scope current_scope.scope_for_create else - scope = relation.clone + scope = relation scope.default_scoped = true scope.scope_for_create end @@ -171,29 +161,27 @@ module ActiveRecord # Article.published.featured.latest_article # Article.featured.titles - def scope(name, scope_options = {}) - name = name.to_sym - valid_scope_name?(name) - extension = Module.new(&Proc.new) if block_given? - - scope_proc = lambda do |*args| - options = scope_options.respond_to?(:call) ? unscoped { scope_options.call(*args) } : scope_options - options = scoped.apply_finder_options(options) if options.is_a?(Hash) - - relation = scoped.merge(options) + def scope(name, body, &block) + extension = Module.new(&block) if block - extension ? relation.extending(extension) : relation + # Check body.is_a?(Relation) to prevent the relation actually being + # loaded by respond_to? + if body.is_a?(Relation) || !body.respond_to?(:call) + ActiveSupport::Deprecation.warn( + "Using #scope without passing a callable object is deprecated. For " \ + "example `scope :red, where(color: 'red')` should be changed to " \ + "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \ + "in the former usage and it makes the implementation more complicated " \ + "and buggy. (If you prefer, you can just define a class method named " \ + "`self.red`.)" + ) end - singleton_class.send(:redefine_method, name, &scope_proc) - end - - protected + singleton_class.send(:define_method, name) do |*args| + options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body + relation = all.merge(options) - def valid_scope_name?(name) - if respond_to?(name, true) - logger.warn "Creating scope :#{name}. " \ - "Overwriting existing method #{self.name}.#{name}." + extension ? relation.extending(extension) : relation end end end diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 41e3b92499..e8dd312a47 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -1,9 +1,21 @@ module ActiveRecord #:nodoc: + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :include_root_in_json, instance_accessor: false + self.include_root_in_json = true + end + # = Active Record Serialization module Serialization extend ActiveSupport::Concern include ActiveModel::Serializers::JSON + included do + singleton_class.class_eval do + remove_method :include_root_in_json + delegate :include_root_in_json, to: 'ActiveRecord::Model' + end + end + def serializable_hash(options = nil) options = options.try(:clone) || {} diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 2e60521638..834d01a1e8 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -18,8 +18,8 @@ module ActiveRecord #:nodoc: # <id type="integer">1</id> # <approved type="boolean">false</approved> # <replies-count type="integer">0</replies-count> - # <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="datetime">2003-07-16T09:28:00+1200</written-on> + # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> + # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> # <content>Have a nice day</content> # <author-email-address>david@loudthinking.com</author-email-address> # <parent-id></parent-id> @@ -177,11 +177,6 @@ module ActiveRecord #:nodoc: end class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: - def initialize(*args) - super - options[:except] = Array(options[:except]) | Array(@serializable.class.inheritance_column) - end - class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb deleted file mode 100644 index ce43ae8066..0000000000 --- a/activerecord/lib/active_record/session_store.rb +++ /dev/null @@ -1,358 +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) - find :first, :conditions => {:session_id=>session_id} - 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)}") - 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 - - # 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)} - 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 1c7b839e5e..25f528498e 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -1,6 +1,8 @@ +require 'active_support/core_ext/hash/indifferent_access' + module ActiveRecord # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. - # It's like a simple key/value store backed into your record when you don't care about being able to + # It's like a simple key/value store baked into your record when you don't care about being able to # query that store outside the context of a single record. # # You can then declare accessors to this store that are then accessible just like any other attribute @@ -10,41 +12,102 @@ module ActiveRecord # Make sure that you declare the database column used for the serialized store as a text, so there's # plenty of room. # + # You can set custom coder to encode/decode your serialized attributes to/from different formats. + # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. + # # Examples: # # class User < ActiveRecord::Base - # store :settings, accessors: [ :color, :homepage ] + # store :settings, accessors: [ :color, :homepage ], coder: JSON # 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 # + # # There is no difference between strings and symbols for accessing custom attributes + # u.settings[:country] # => 'Denmark' + # u.settings['country'] # => 'Denmark' + # # # Add additional accessors to an existing store through store_accessor # class SuperUser < User # store_accessor :settings, :privileges, :servants # end + # + # The stored attribute names can be retrieved using +stored_attributes+. + # + # User.stored_attributes[:settings] # [:color, :homepage] module Store extend ActiveSupport::Concern + included do + class_attribute :stored_attributes, instance_accessor: false + self.stored_attributes = {} + end + module ClassMethods def store(store_attribute, options = {}) - serialize store_attribute, Hash + serialize store_attribute, IndifferentCoder.new(options[:coder]) store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors end def store_accessor(store_attribute, *keys) - keys.flatten.each do |key| + keys = keys.flatten + keys.each do |key| define_method("#{key}=") do |value| - send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) - send(store_attribute)[key] = value - send("#{store_attribute}_will_change!") + attribute = initialize_store_attribute(store_attribute) + if value != attribute[key] + send :"#{store_attribute}_will_change!" + attribute[key] = value + end end define_method(key) do - send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) - send(store_attribute)[key] + initialize_store_attribute(store_attribute)[key] + end + end + + self.stored_attributes[store_attribute] ||= [] + self.stored_attributes[store_attribute] |= keys + end + end + + private + def initialize_store_attribute(store_attribute) + attribute = send(store_attribute) + unless attribute.is_a?(HashWithIndifferentAccess) + attribute = IndifferentCoder.as_indifferent_hash(attribute) + send :"#{store_attribute}=", attribute + end + attribute + end + + class IndifferentCoder + def initialize(coder_or_class_name) + @coder = + if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump) + coder_or_class_name + else + ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object) end + end + + def dump(obj) + @coder.dump self.class.as_indifferent_hash(obj) + end + + def load(yaml) + self.class.as_indifferent_hash @coder.load(yaml) + end + + def self.as_indifferent_hash(obj) + case obj + when HashWithIndifferentAccess + obj + when Hash + obj.with_indifferent_access + else + HashWithIndifferentAccess.new end end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb new file mode 100644 index 0000000000..b41cc68b6a --- /dev/null +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -0,0 +1,122 @@ +module ActiveRecord + module Tasks # :nodoc: + module DatabaseTasks # :nodoc: + extend self + + LOCAL_HOSTS = ['127.0.0.1', 'localhost'] + + def register_task(pattern, task) + @tasks ||= {} + @tasks[pattern] = task + end + + register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks) + register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) + register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) + + def create(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).create + rescue Exception => error + $stderr.puts error, *(error.backtrace) + $stderr.puts "Couldn't create database for #{configuration.inspect}" + end + + def create_all + each_local_configuration { |configuration| create configuration } + end + + def create_current(environment = Rails.env) + each_current_configuration(environment) { |configuration| + create configuration + } + ActiveRecord::Base.establish_connection environment + end + + def drop(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).drop + rescue Exception => error + $stderr.puts error, *(error.backtrace) + $stderr.puts "Couldn't drop #{configuration['database']}" + end + + def drop_all + each_local_configuration { |configuration| drop configuration } + end + + def drop_current(environment = Rails.env) + each_current_configuration(environment) { |configuration| + drop configuration + } + end + + def charset_current(environment = Rails.env) + charset ActiveRecord::Base.configurations[environment] + end + + def charset(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).charset + end + + def collation_current(environment = Rails.env) + collation ActiveRecord::Base.configurations[environment] + end + + def collation(*arguments) + configuration = arguments.first + class_for_adapter(configuration['adapter']).new(*arguments).collation + end + + def purge(configuration) + class_for_adapter(configuration['adapter']).new(configuration).purge + end + + def structure_dump(*arguments) + configuration = arguments.first + filename = arguments.delete_at 1 + class_for_adapter(configuration['adapter']).new(*arguments).structure_dump(filename) + end + + def structure_load(*arguments) + configuration = arguments.first + filename = arguments.delete_at 1 + class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) + end + + private + + def class_for_adapter(adapter) + key = @tasks.keys.detect { |pattern| adapter[pattern] } + @tasks[key] + end + + def each_current_configuration(environment) + environments = [environment] + environments << 'test' if environment.development? + + configurations = ActiveRecord::Base.configurations.values_at(*environments) + configurations.compact.each do |configuration| + yield configuration unless configuration['database'].blank? + end + end + + def each_local_configuration + ActiveRecord::Base.configurations.each_value do |configuration| + next unless configuration['database'] + + if local_database?(configuration) + yield configuration + else + $stderr.puts "This task only modifies local databases. #{configuration['database']} is on a remote host." + end + end + end + + def local_database?(configuration) + configuration['host'].blank? || LOCAL_HOSTS.include?(configuration['host']) + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb new file mode 100644 index 0000000000..85d08402f9 --- /dev/null +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -0,0 +1,118 @@ +module ActiveRecord + module Tasks # :nodoc: + class MySQLDatabaseTasks # :nodoc: + + DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8' + DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci' + ACCESS_DENIED_ERROR = 1045 + + delegate :connection, :establish_connection, to: ActiveRecord::Base + + def initialize(configuration) + @configuration = configuration + end + + def create + establish_connection configuration_without_database + connection.create_database configuration['database'], creation_options + establish_connection configuration + rescue error_class => error + raise error unless error.errno == ACCESS_DENIED_ERROR + + $stdout.print error.error + establish_connection root_configuration_without_database + connection.create_database configuration['database'], creation_options + connection.execute grant_statement.gsub(/\s+/, ' ').strip + establish_connection configuration + rescue error_class => error + $stderr.puts error.error + $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" + $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['charset'] + end + + def drop + establish_connection configuration + connection.drop_database configuration['database'] + end + + def purge + establish_connection :test + connection.recreate_database configuration['database'], creation_options + end + + def charset + connection.charset + end + + def collation + connection.collation + end + + def structure_dump(filename) + establish_connection configuration + File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } + end + + def structure_load(filename) + args = ['mysql'] + args.concat(['--user', configuration['username']]) if configuration['username'] + args << "--password=#{configuration['password']}" if configuration['password'] + args.concat(['--default-character-set', configuration['charset']]) if configuration['charset'] + configuration.slice('host', 'port', 'socket', 'database').each do |k, v| + args.concat([ "--#{k}", v ]) if v + end + args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) + Kernel.system(*args) + end + + private + + def configuration + @configuration + end + + def configuration_without_database + configuration.merge('database' => nil) + end + + def creation_options + { + charset: (configuration['charset'] || DEFAULT_CHARSET), + collation: (configuration['collation'] || DEFAULT_COLLATION) + } + end + + def error_class + case configuration['adapter'] + when /jdbc/ + require 'active_record/railties/jdbcmysql_error' + ArJdbcMySQL::Error + when /mysql2/ + Mysql2::Error + else + Mysql::Error + end + end + + def grant_statement + <<-SQL +GRANT ALL PRIVILEGES ON #{configuration['database']}.* + TO '#{configuration['username']}'@'localhost' +IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; + SQL + end + + def root_configuration_without_database + configuration_without_database.merge( + 'username' => 'root', + 'password' => root_password + ) + end + + def root_password + $stdout.print "Please provide the root password for your mysql installation\n>" + $stdin.gets.strip + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb new file mode 100644 index 0000000000..ea5cb888fb --- /dev/null +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -0,0 +1,85 @@ +require 'shellwords' + +module ActiveRecord + module Tasks # :nodoc: + class PostgreSQLDatabaseTasks # :nodoc: + + DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8' + + delegate :connection, :establish_connection, :clear_active_connections!, + to: ActiveRecord::Base + + def initialize(configuration) + @configuration = configuration + end + + def create(master_established = false) + establish_master_connection unless master_established + connection.create_database configuration['database'], + configuration.merge('encoding' => encoding) + establish_connection configuration + end + + def drop + establish_master_connection + connection.drop_database configuration['database'] + end + + def charset + connection.encoding + end + + def collation + connection.collation + end + + def purge + clear_active_connections! + drop + create true + end + + def structure_dump(filename) + set_psql_env + search_path = configuration['schema_search_path'] + unless search_path.blank? + search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") + end + + command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" + raise 'Error dumping database' unless Kernel.system(command) + + File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } + end + + def structure_load(filename) + set_psql_env + Kernel.system("psql -f #{filename} #{configuration['database']}") + end + + private + + def configuration + @configuration + end + + def encoding + configuration['encoding'] || DEFAULT_ENCODING + end + + def establish_master_connection + establish_connection configuration.merge( + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + end + + def set_psql_env + ENV['PGHOST'] = configuration['host'] if configuration['host'] + ENV['PGPORT'] = configuration['port'].to_s if configuration['port'] + ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] + ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb new file mode 100644 index 0000000000..da01058a82 --- /dev/null +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -0,0 +1,55 @@ +module ActiveRecord + module Tasks # :nodoc: + class SQLiteDatabaseTasks # :nodoc: + + delegate :connection, :establish_connection, to: ActiveRecord::Base + + def initialize(configuration, root = Rails.root) + @configuration, @root = configuration, root + end + + def create + if File.exist?(configuration['database']) + $stderr.puts "#{configuration['database']} already exists" + return + end + + establish_connection configuration + connection + end + + def drop + require 'pathname' + path = Pathname.new configuration['database'] + file = path.absolute? ? path.to_s : File.join(root, path) + + FileUtils.rm(file) if File.exist?(file) + end + alias :purge :drop + + def charset + connection.encoding + end + + def structure_dump(filename) + dbfile = configuration['database'] + `sqlite3 #{dbfile} .schema > #{filename}` + end + + def structure_load(filename) + dbfile = configuration['database'] + `sqlite3 #{dbfile} < "#{filename}"` + end + + private + + def configuration + @configuration + end + + def root + @root + end + end + end +end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 4d881f0f7d..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') @@ -7,18 +6,8 @@ module ActiveRecord # # Defines some test assertions to test against SQL queries. class TestCase < ActiveSupport::TestCase #:nodoc: - setup :cleanup_identity_map - - def setup - cleanup_identity_map - end - def teardown - SQLCounter.log.clear - end - - def cleanup_identity_map - ActiveRecord::IdentityMap.clear + SQLCounter.clear_log end def assert_date_from_db(expected, actual, message = nil) @@ -32,47 +21,57 @@ module ActiveRecord end def assert_sql(*patterns_to_match) - SQLCounter.log = [] + SQLCounter.clear_log yield - SQLCounter.log + SQLCounter.log_all ensure failed_patterns = [] patterns_to_match.each do |pattern| - failed_patterns << pattern unless SQLCounter.log.any?{ |sql| pattern === sql } + failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql } end assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" end - def assert_queries(num = 1) - SQLCounter.log = [] + def assert_queries(num = 1, options = {}) + ignore_none = options.fetch(:ignore_none) { num == :any } + SQLCounter.clear_log yield ensure - assert_equal num, SQLCounter.log.size, "#{SQLCounter.log.size} instead of #{num} queries were executed.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" + the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log + if num == :any + assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed." + else + mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}" + assert_equal num, the_log.size, mesg + end end def assert_no_queries(&block) - prev_ignored_sql = SQLCounter.ignored_sql - SQLCounter.ignored_sql = [] - assert_queries(0, &block) - ensure - SQLCounter.ignored_sql = prev_ignored_sql + assert_queries(0, :ignore_none => true, &block) end end class SQLCounter class << self - attr_accessor :ignored_sql, :log + attr_accessor :ignored_sql, :log, :log_all + def clear_log; self.log = []; self.log_all = []; end end - self.log = [] + self.clear_log self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/] # FIXME: this needs to be refactored so specific database can add their own - # ignored SQL. This ignored SQL is for Oracle. - ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] - + # ignored SQL, or better yet, use a different notification for the queries + # instead examining the SQL content. + oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] + mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/] + postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im] + + [oracle_ignored, mysql_ignored, postgresql_ignored].each do |db_ignored_sql| + ignored_sql.concat db_ignored_sql + end attr_reader :ignore @@ -85,8 +84,10 @@ module ActiveRecord # FIXME: this seems bad. we should probably have a better way to indicate # the query was cached - return if 'CACHE' == values[:name] || ignore =~ sql - self.class.log << sql + return if 'CACHE' == values[:name] + + self.class.log_all << sql + self.class.log << sql unless ignore =~ sql end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index c717fdea47..c32e0d6bf8 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,6 +1,10 @@ -require 'active_support/core_ext/class/attribute' module ActiveRecord + ActiveSupport.on_load(:active_record_config) do + mattr_accessor :record_timestamps, instance_accessor: false + self.record_timestamps = true + end + # = Active Record Timestamp # # Active Record automatically timestamps create and update operations if the @@ -33,8 +37,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - config_attribute :record_timestamps, :instance_writer => true - self.record_timestamps = true + config_attribute :record_timestamps, instance_writer: true end def initialize_dup(other) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index b492377d18..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] @@ -251,7 +269,6 @@ module ActiveRecord remember_transaction_record_state yield rescue Exception - IdentityMap.remove(self) if IdentityMap.enabled? restore_transaction_record_state raise ensure @@ -270,7 +287,6 @@ module ActiveRecord def rolledback!(force_restore_state = false) #:nodoc: run_callbacks :rollback ensure - IdentityMap.remove(self) if IdentityMap.enabled? restore_transaction_record_state(force_restore_state) end @@ -292,7 +308,13 @@ module ActiveRecord status = nil self.class.transaction do add_to_transaction - status = yield + begin + status = yield + rescue ActiveRecord::Rollback + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + status = nil + end + raise ActiveRecord::Rollback unless status end status @@ -302,32 +324,26 @@ 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) - unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @destroyed - end + @_start_transaction_state[:new_record] = @new_record + @_start_transaction_state[:destroyed] = @destroyed @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 end # 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) - @attributes = @attributes.dup if @attributes.frozen? + 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] @destroyed = restore_state[:destroyed] if restore_state.has_key?(:id) @@ -336,13 +352,15 @@ module ActiveRecord @attributes.delete(self.class.primary_key) @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 4b075183c3..cef2bbd563 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -14,7 +14,7 @@ module ActiveRecord def initialize(record) @record = record errors = @record.errors.full_messages.join(", ") - super(I18n.t("activerecord.errors.messages.record_invalid", :errors => errors)) + super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid")) end end @@ -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 7fd5d76ba5..c117872ac8 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -1,6 +1,8 @@ +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 @@ -24,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) @@ -33,8 +35,14 @@ module ActiveRecord relation = relation.and(table[scope_item].eq(scope_value)) end - if finder_class.unscoped.where(relation).exists? - record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value)) + relation = finder_class.unscoped.where(relation) + + if options[:conditions] + relation = relation.merge(options[:conditions]) + end + + if relation.exists? + record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope, :conditions).merge(:value => value)) end end @@ -49,7 +57,7 @@ module ActiveRecord class_hierarchy = [record.class] while class_hierarchy.first != @klass - class_hierarchy.insert(0, class_hierarchy.first.superclass) + class_hierarchy.prepend(class_hierarchy.first.superclass) end class_hierarchy.detect { |klass| !klass.abstract_class? } @@ -79,44 +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 # - # 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, + # 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') + # 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, # 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>: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 # @@ -172,16 +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::SQLiteAdapter # * 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.rb b/activerecord/lib/rails/generators/active_record.rb index 4b3d1db216..297cd094c2 100644 --- a/activerecord/lib/rails/generators/active_record.rb +++ b/activerecord/lib/rails/generators/active_record.rb @@ -1,14 +1,12 @@ require 'rails/generators/named_base' require 'rails/generators/migration' require 'rails/generators/active_model' -require 'rails/generators/active_record/migration' require 'active_record' module ActiveRecord module Generators class Base < Rails::Generators::NamedBase #:nodoc: include Rails::Generators::Migration - extend ActiveRecord::Generators::Migration # Set the current directory as base for the inherited generators. def self.base_root diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb deleted file mode 100644 index 7f2f2e06a5..0000000000 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord - module Generators - module Migration - # Implement the required interface for Rails::Generators::Migration. - def next_migration_number(dirname) #:nodoc: - next_migration_number = current_migration_number(dirname) + 1 - if ActiveRecord::Base.timestamped_migrations - [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max - else - "%.3d" % next_migration_number - end - end - end - end -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..a3c274d9b9 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -7,19 +7,49 @@ module ActiveRecord def create_migration_file set_local_assigns! + validate_file_name! migration_template "migration.rb", "db/migrate/#{file_name}.rb" 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 + + private + + def validate_file_name! + unless file_name =~ /^[_a-z0-9]+$/ + raise IllegalMigrationNameError.new(file_name) + end + 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 d084a00ed7..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 -%> 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 == 'add' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> - <% if attribute.has_index? && migration_action == 'add' %> - add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> - <% end -%> +<%- 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 -%> - <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> +<%- if migration_action -%> + <%- if attribute.reference? -%> + add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> + <%- else -%> + add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- if attribute.has_index? -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> <%- end -%> <%- end -%> +<%- end -%> end <%- end -%> end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 99a022461e..8e6ef20285 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -14,6 +14,7 @@ module ActiveRecord def create_migration_file return unless options[:migration] && options[:parent].nil? + attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false migration_template "migration.rb", "db/migrate/create_#{table_name}.rb" end @@ -27,7 +28,11 @@ module ActiveRecord end def attributes_with_index - attributes.select { |a| a.has_index? || (a.reference? && options[:indexes]) } + attributes.select { |a| !a.reference? && a.has_index? } + end + + def accessible_attributes + attributes.reject(&:reference?) end hook_for :test_framework diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 5c47f8b241..2cca17b94f 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -1,7 +1,12 @@ <% module_namespacing do -%> class <%= class_name %> < <%= parent_class_name.classify %> <% attributes.select {|attr| attr.reference? }.each do |attribute| -%> - belongs_to :<%= attribute.name %> + belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %> +<% end -%> +<% if !accessible_attributes.empty? -%> + attr_accessible <%= accessible_attributes.map {|a| ":#{a.name}" }.sort.join(', ') %> +<% else -%> + # attr_accessible :title, :body <% end -%> end <% end -%> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/module.rb b/activerecord/lib/rails/generators/active_record/model/templates/module.rb index fca2908080..a3bf1c37b6 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/module.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/module.rb @@ -1,7 +1,7 @@ <% module_namespacing do -%> module <%= class_path.map(&:camelize).join('::') %> def self.table_name_prefix - '<%= class_path.join('_') %>_' + '<%= namespaced? ? namespaced_class_path.join('_') : class_path.join('_') %>_' end end <% end -%> diff --git a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb 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/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 69dfd2503e..1199be68eb 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -1,6 +1,6 @@ module ActiveRecord - class Base - def self.fake_connection(config) + module ConnectionHandling + def fake_connection(config) ConnectionAdapters::FakeAdapter.new nil, logger end end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index fa2ba8d592..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 @@ -120,6 +123,20 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_mysql_default_in_strict_mode + result = @connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [["STRICT_ALL_TABLES"]], result.rows + end + + 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})) + 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 + private def run_without_connection diff --git a/activerecord/test/cases/adapters/mysql/enum_test.rb b/activerecord/test/cases/adapters/mysql/enum_test.rb new file mode 100644 index 0000000000..40af317ad1 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/enum_test.rb @@ -0,0 +1,10 @@ +require "cases/helper" + +class MysqlEnumTest < ActiveRecord::TestCase + class EnumTest < ActiveRecord::Base + end + + def test_enum_limit + assert_equal 5, EnumTest.columns.first.limit + end +end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 7fe2c02c04..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 @@ -46,6 +48,46 @@ module ActiveRecord assert_equal str, value end + def test_tables_quoting + begin + @conn.tables(nil, "foo-bar", nil) + flunk + rescue => e + # assertion for *quoted* database properly + assert_match(/database 'foo-bar'/, e.inspect) + end + end + + def test_pk_and_sequence_for + pk, seq = @conn.pk_and_sequence_for('ex') + assert_equal 'id', pk + assert_equal @conn.default_sequence_name('ex', 'id'), seq + end + + def test_pk_and_sequence_for_with_non_standard_primary_key + @conn.exec_query('drop table if exists ex_with_non_standard_pk') + @conn.exec_query(<<-eosql) + CREATE TABLE `ex_with_non_standard_pk` ( + `code` INT(11) DEFAULT NULL auto_increment, + PRIMARY KEY (`code`)) + eosql + pk, seq = @conn.pk_and_sequence_for('ex_with_non_standard_pk') + assert_equal 'code', pk + assert_equal @conn.default_sequence_name('ex_with_non_standard_pk', 'code'), seq + end + + def test_pk_and_sequence_for_with_custom_index_type_pk + @conn.exec_query('drop table if exists ex_with_custom_index_type_pk') + @conn.exec_query(<<-eosql) + CREATE TABLE `ex_with_custom_index_type_pk` ( + `id` INT(11) DEFAULT NULL auto_increment, + PRIMARY KEY USING BTREE (`id`)) + eosql + pk, seq = @conn.pk_and_sequence_for('ex_with_custom_index_type_pk') + assert_equal 'id', pk + assert_equal @conn.default_sequence_name('ex_with_custom_index_type_pk', 'id'), seq + end + private def insert(ctx, data) binds = data.map { |name, value| diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 292c7efebb..aff971a955 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -34,11 +34,11 @@ class MysqlReservedWordTest < ActiveRecord::TestCase 'select'=>'id int auto_increment primary key', 'values'=>'id int auto_increment primary key, group_id int', 'distinct'=>'id int auto_increment primary key', - 'distincts_selects'=>'distinct_id int, select_id int' + 'distinct_select'=>'distinct_id int, select_id int' end def teardown - drop_tables_directly ['group', 'select', 'values', 'distinct', 'distincts_selects', 'order'] + drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order'] end # create tables with reserved-word names and columns @@ -80,7 +80,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #activerecord model class with reserved-word table name def test_activerecord_model - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select x = nil assert_nothing_raised { x = Group.new } x.order = 'x' @@ -94,7 +94,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_one association with reserved-word table name def test_has_one_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select v = nil assert_nothing_raised { v = Group.find(1).values } assert_equal 2, v.id @@ -102,7 +102,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # belongs_to association with reserved-word table name def test_belongs_to_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select gs = nil assert_nothing_raised { gs = Select.find(2).groups } assert_equal gs.length, 2 @@ -111,7 +111,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_and_belongs_to_many with reserved-word table name def test_has_and_belongs_to_many - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select s = nil assert_nothing_raised { s = Distinct.find(1).selects } assert_equal s.length, 2 @@ -130,7 +130,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase end def test_associations_work_with_reserved_words - assert_nothing_raised { Select.find(:all, :include => [:groups]) } + 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/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index 29f885c6e7..d94bb629a7 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -20,7 +20,7 @@ module ActiveRecord end def test_schema - assert @omgpost.find(:first) + assert @omgpost.first end def test_primary_key diff --git a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb index cd9c1041dc..5e8065d80d 100644 --- a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb @@ -9,7 +9,7 @@ module ActiveRecord def test_update_question_marks str = "foo?bar" - x = Topic.find :first + x = Topic.first x.title = str x.content = str x.save! @@ -28,7 +28,7 @@ module ActiveRecord def test_update_null_bytes str = "foo\0bar" - x = Topic.find :first + x = Topic.first x.title = str x.content = str x.save! diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 8e2b9ca9a5..c63e4fe5b6 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -4,6 +4,13 @@ class MysqlConnectionTest < ActiveRecord::TestCase def setup super @connection = ActiveRecord::Model.connection + @connection.extend(LogIntercepter) + @connection.intercepted = true + end + + def teardown + @connection.intercepted = false + @connection.logged = [] end def test_no_automatic_reconnection_after_timeout @@ -29,6 +36,43 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert @connection.active? end + # TODO: Below is a straight up copy/paste from mysql/connection_test.rb + # I'm not sure what the correct way is to share these tests between + # adapters in minitest. + def test_mysql_default_in_strict_mode + result = @connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [["STRICT_ALL_TABLES"]], result.rows + end + + 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})) + 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 + + def test_logs_name_structure_dump + @connection.structure_dump + assert_equal "SCHEMA", @connection.logged[0][1] + assert_equal "SCHEMA", @connection.logged[2][1] + end + + def test_logs_name_show_variable + @connection.show_variable 'foo' + assert_equal "SCHEMA", @connection.logged[0][1] + end + + def test_logs_name_rename_column_sql + @connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))" + @connection.logged = [] + @connection.send(:rename_column_sql, 'bar_baz', 'foo', 'foo2') + assert_equal "SCHEMA", @connection.logged[0][1] + ensure + @connection.execute "DROP TABLE `bar_baz`" + end + private def run_without_connection diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb new file mode 100644 index 0000000000..f3a05e48ad --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -0,0 +1,10 @@ +require "cases/helper" + +class Mysql2EnumTest < ActiveRecord::TestCase + class EnumTest < ActiveRecord::Base + end + + def test_enum_limit + assert_equal 5, EnumTest.columns.first.limit + end +end diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 3a9744e78f..9fd07f014e 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -34,11 +34,11 @@ class MysqlReservedWordTest < ActiveRecord::TestCase 'select'=>'id int auto_increment primary key', 'values'=>'id int auto_increment primary key, group_id int', 'distinct'=>'id int auto_increment primary key', - 'distincts_selects'=>'distinct_id int, select_id int' + 'distinct_select'=>'distinct_id int, select_id int' end def teardown - drop_tables_directly ['group', 'select', 'values', 'distinct', 'distincts_selects', 'order'] + drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order'] end # create tables with reserved-word names and columns @@ -80,7 +80,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #activerecord model class with reserved-word table name def test_activerecord_model - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select x = nil assert_nothing_raised { x = Group.new } x.order = 'x' @@ -94,7 +94,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_one association with reserved-word table name def test_has_one_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select v = nil assert_nothing_raised { v = Group.find(1).values } assert_equal 2, v.id @@ -102,7 +102,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # belongs_to association with reserved-word table name def test_belongs_to_associations - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select gs = nil assert_nothing_raised { gs = Select.find(2).groups } assert_equal gs.length, 2 @@ -111,7 +111,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase # has_and_belongs_to_many with reserved-word table name def test_has_and_belongs_to_many - create_test_fixtures :select, :distinct, :group, :values, :distincts_selects + create_test_fixtures :select, :distinct, :group, :values, :distinct_select s = nil assert_nothing_raised { s = Distinct.find(1).selects } assert_equal s.length, 2 @@ -130,7 +130,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase end def test_associations_work_with_reserved_words - assert_nothing_raised { Select.find(:all, :include => [:groups]) } + 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/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index d5676bc522..2c0ed73c92 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -20,7 +20,7 @@ module ActiveRecord end def test_schema - assert @omgpost.find(:first) + assert @omgpost.first end def test_primary_key @@ -35,6 +35,17 @@ module ActiveRecord def test_table_exists_wrong_schema assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") end + + def test_tables_quoting + begin + @connection.tables(nil, "foo-bar", nil) + flunk + rescue => e + # assertion for *quoted* database properly + assert_match(/database 'foo-bar'/, e.inspect) + end + end + end end end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index e4746d4aa3..1b4f4a5fc9 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -3,8 +3,6 @@ require 'cases/helper' class PostgresqlActiveSchemaTest < ActiveRecord::TestCase def setup ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do - alias_method :real_execute, :execute - remove_method :execute def execute(sql, name = nil) sql end end end @@ -12,7 +10,6 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase def teardown ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do remove_method :execute - alias_method :execute, :real_execute end end @@ -21,6 +18,22 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, :encoding => :latin1) end + def test_create_database_with_collation_and_ctype + assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'UTF8' LC_COLLATE = 'ja_JP.UTF8' LC_CTYPE = 'ja_JP.UTF8'), create_database(:aimonetti, :encoding => :"UTF8", :collation => :"ja_JP.UTF8", :ctype => :"ja_JP.UTF8") + end + + def test_add_index + # add_index calls index_name_exists? which can't work since execute is stubbed + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) do |*| + false + end + + expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active') + assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'") + + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:remove_method, :index_name_exists?) + end + private def method_missing(method_symbol, *arguments) ActiveRecord::Base.connection.send(method_symbol, *arguments) diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 4baec749ff..1ff307c735 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -8,12 +8,31 @@ module ActiveRecord def setup super @connection = ActiveRecord::Base.connection + @connection.extend(LogIntercepter) + @connection.intercepted = true + end + + def teardown + @connection.intercepted = false + @connection.logged = [] end def test_encoding assert_not_nil @connection.encoding end + def test_collation + assert_not_nil @connection.collation + end + + def test_ctype + assert_not_nil @connection.ctype + end + + def test_default_client_min_messages + assert_equal "warning", @connection.client_min_messages + end + # Ensure, we can set connection params using the example of Generic # Query Optimizer (geqo). It is 'on' per default. def test_connection_options @@ -25,5 +44,115 @@ module ActiveRecord expect = NonExistentTable.connection.query('show geqo').first.first assert_equal 'off', expect end + + def test_tables_logs_name + @connection.tables('hello') + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_indexes_logs_name + @connection.indexes('items', 'hello') + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_table_exists_logs_name + @connection.table_exists?('items') + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_table_alias_length_logs_name + @connection.instance_variable_set("@table_alias_length", nil) + @connection.table_alias_length + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_current_database_logs_name + @connection.current_database + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_encoding_logs_name + @connection.encoding + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_schema_names_logs_name + @connection.schema_names + assert_equal 'SCHEMA', @connection.logged[0][1] + end + + def test_reconnection_after_simulated_disconnection_with_verify + assert @connection.active? + original_connection_pid = @connection.query('select pg_backend_pid()') + + # Fail with bad connection on next query attempt. + raw_connection = @connection.raw_connection + raw_connection_class = class << raw_connection ; self ; end + raw_connection_class.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def query_fake(*args) + if !( @called ||= false ) + self.stubs(:status).returns(PGconn::CONNECTION_BAD) + @called = true + raise PGError + else + self.unstub(:status) + query_unfake(*args) + end + end + + alias query_unfake query + alias query query_fake + CODE + + begin + @connection.verify! + new_connection_pid = @connection.query('select pg_backend_pid()') + ensure + raw_connection_class.class_eval <<-CODE + alias query query_unfake + undef query_fake + CODE + end + + assert_not_equal original_connection_pid, new_connection_pid, "Should have a new underlying connection pid" + end + + # Must have with_manual_interventions set to true for this + # test to run. + # When prompted, restart the PostgreSQL server with the + # "-m fast" option or kill the individual connection assuming + # you know the incantation to do that. + # To restart PostgreSQL 9.1 on OS X, installed via MacPorts, ... + # sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast" + def test_reconnection_after_actual_disconnection_with_verify + skip "with_manual_interventions is false in configuration" unless ARTest.config['with_manual_interventions'] + + original_connection_pid = @connection.query('select pg_backend_pid()') + + # Sanity check. + assert @connection.active? + + puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' + + 'server with the "-m fast" option) and then press enter.' + $stdin.gets + + @connection.verify! + + assert @connection.active? + + # If we get no exception here, then either we re-connected successfully, or + # we never actually got disconnected. + new_connection_pid = @connection.query('select pg_backend_pid()') + + assert_not_equal original_connection_pid, new_connection_pid, + "umm -- looks like you didn't break the connection, because we're still " + + "successfully querying with the same connection pid." + + # Repair all fixture connections so other tests won't break. + @fixture_connections.each do |c| + c.verify! + end + end + end end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index ce08e4c6a7..a4d9286d52 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -27,6 +27,9 @@ end class PostgresqlTimestampWithZone < ActiveRecord::Base end +class PostgresqlUUID < ActiveRecord::Base +end + class PostgresqlDataTypeTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -61,6 +64,9 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase @first_oid = PostgresqlOid.find(1) @connection.execute("INSERT INTO postgresql_timestamp_with_zones (time) VALUES ('2010-01-01 10:00:00-1')") + + @connection.execute("INSERT INTO postgresql_uuids (guid, compact_guid) VALUES('d96c3da0-96c1-012f-1316-64ce8f32c6d8', 'f06c715096c1012f131764ce8f32c6d8')") + @first_uuid = PostgresqlUUID.find(1) end def test_data_type_of_array_types @@ -86,9 +92,9 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_data_type_of_network_address_types - assert_equal :string, @first_network_address.column_for_attribute(:cidr_address).type - assert_equal :string, @first_network_address.column_for_attribute(:inet_address).type - assert_equal :string, @first_network_address.column_for_attribute(:mac_address).type + assert_equal :cidr, @first_network_address.column_for_attribute(:cidr_address).type + assert_equal :inet, @first_network_address.column_for_attribute(:inet_address).type + assert_equal :macaddr, @first_network_address.column_for_attribute(:mac_address).type end def test_data_type_of_bit_string_types @@ -100,6 +106,10 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type end + def test_data_type_of_uuid_types + assert_equal :uuid, @first_uuid.column_for_attribute(:guid).type + end + def test_array_values assert_equal '{35000,21000,18000,17000}', @first_array.commission_by_quarter assert_equal '{foo,bar,baz}', @first_array.nicknames @@ -134,12 +144,20 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '-1 years -2 days', @first_time.time_interval end - def test_network_address_values - assert_equal '192.168.0.0/24', @first_network_address.cidr_address - assert_equal '172.16.1.254', @first_network_address.inet_address + def test_network_address_values_ipaddr + cidr_address = IPAddr.new '192.168.0.0/24' + inet_address = IPAddr.new '172.16.1.254' + + assert_equal cidr_address, @first_network_address.cidr_address + assert_equal inet_address, @first_network_address.inet_address assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address end + def test_uuid_values + assert_equal 'd96c3da0-96c1-012f-1316-64ce8f32c6d8', @first_uuid.guid + assert_equal 'f06c7150-96c1-012f-1317-64ce8f32c6d8', @first_uuid.compact_guid + end + def test_bit_string_values assert_equal '00010101', @first_bit_string.bit_string assert_equal '00010101', @first_bit_string.bit_string_varying @@ -200,8 +218,8 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_update_network_address - new_cidr_address = '10.1.2.3/32' - new_inet_address = '10.0.0.0/8' + new_inet_address = '10.1.2.3/32' + new_cidr_address = '10.0.0.0/8' new_mac_address = 'bc:de:f0:12:34:56' assert @first_network_address.cidr_address = new_cidr_address assert @first_network_address.inet_address = new_inet_address diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 0b61f61572..619d581d5f 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -22,6 +22,13 @@ module ActiveRecord assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain assert_match %(Seq Scan on audit_logs), explain end + + def test_dont_explain_for_set_search_path + queries = Thread.current[:available_queries_for_explain] = [] + ActiveRecord::Base.connection.schema_search_path = "public" + assert queries.empty? + end + end end end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 33bf4478cc..23bafde17b 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -1,4 +1,8 @@ +# encoding: utf-8 + require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlHstoreTest < ActiveRecord::TestCase class Hstore < ActiveRecord::Base @@ -10,12 +14,13 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase begin @connection.transaction do @connection.create_table('hstores') do |t| - t.hstore 'tags' + t.hstore 'tags', :default => '' end end rescue ActiveRecord::StatementInvalid return skip "do not test on PG without hstore" end + @column = Hstore.columns.find { |c| c.name == 'tags' } end def teardown @@ -23,30 +28,83 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase end def test_column - column = Hstore.columns.find { |c| c.name == 'tags' } - assert column - assert_equal :hstore, column.type + assert_equal :hstore, @column.type end def test_type_cast_hstore - column = Hstore.columns.find { |c| c.name == 'tags' } - assert column + assert @column data = "\"1\"=>\"2\"" - hash = column.class.cast_hstore data + hash = @column.class.string_to_hstore data assert_equal({'1' => '2'}, hash) - assert_equal({'1' => '2'}, column.type_cast(data)) + assert_equal({'1' => '2'}, @column.type_cast(data)) + + assert_equal({}, @column.type_cast("")) + assert_equal({'key'=>nil}, @column.type_cast('key => NULL')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b"))) + end + + def test_gen1 + assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) + end + + def test_gen2 + assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''})) + end + + def test_gen3 + assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''})) + end + + def test_gen4 + assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''})) + end + + def test_parse1 + assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c')) + end + + def test_parse2 + assert_equal({" " => " "}, @column.type_cast("\\ =>\\ ")) + end + + def test_parse3 + assert_equal({"=" => ">"}, @column.type_cast("==>>")) end + def test_parse4 + assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w')) + end + + def test_parse5 + assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w')) + end + + def test_parse6 + assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w')) + end + + def test_parse7 + assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w')) + end + + def test_rewrite + @connection.execute "insert into hstores (tags) VALUES ('1=>2')" + x = Hstore.first + x.tags = { '"a\'' => 'b' } + assert x.save! + end + + def test_select @connection.execute "insert into hstores (tags) VALUES ('1=>2')" - x = Hstore.find :first + x = Hstore.first assert_equal({'1' => '2'}, x.tags) end def test_select_multikey @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" - x = Hstore.find :first + x = Hstore.first assert_equal({'1' => '2', '2' => '3'}, x.tags) end @@ -54,6 +112,10 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_cycle('a' => 'b', '1' => '2') end + def test_nil + assert_cycle('a' => nil) + end + def test_quotes assert_cycle('a' => 'b"ar', '1"foo' => '2') end @@ -74,13 +136,19 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_cycle('a=>b' => 'bar', '1"foo' => '2') end + def test_quoting_special_characters + assert_cycle('ca' => 'cà', 'ac' => 'àc') + end + private def assert_cycle hash + # test creation x = Hstore.create!(:tags => hash) x.reload assert_equal(hash, x.tags) - # make sure updates work + # test updating + x = Hstore.create!(:tags => {}) x.tags = hash x.save! x.reload diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb new file mode 100644 index 0000000000..d64037eec0 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -0,0 +1,71 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlJSONTest < ActiveRecord::TestCase + class JsonDataType < ActiveRecord::Base + self.table_name = 'json_data_type' + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.transaction do + @connection.create_table('json_data_type') do |t| + t.json 'payload', :default => {} + end + end + rescue ActiveRecord::StatementInvalid + return skip "do not test on PG without json" + end + @column = JsonDataType.columns.find { |c| c.name == 'payload' } + end + + def teardown + @connection.execute 'drop table if exists json_data_type' + end + + def test_column + assert_equal :json, @column.type + end + + def test_type_cast_json + assert @column + + data = "{\"a_key\":\"a_value\"}" + hash = @column.class.string_to_json data + assert_equal({'a_key' => 'a_value'}, hash) + assert_equal({'a_key' => 'a_value'}, @column.type_cast(data)) + + assert_equal({}, @column.type_cast("{}")) + assert_equal({'key'=>nil}, @column.type_cast('{"key": null}')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q({"c":"}", "\"a\"":"b \"a b"}))) + end + + def test_rewrite + @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" + x = JsonDataType.first + x.payload = { '"a\'' => 'b' } + assert x.save! + end + + def test_select + @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" + x = JsonDataType.first + assert_equal({'k' => 'v'}, x.payload) + end + + def test_select_multikey + @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| + x = JsonDataType.first + assert_equal({'k1' => 'v1', 'k2' => 'v2', 'k3' => [1,2,3]}, x.payload) + end + + def test_null_json + @connection.execute %q|insert into json_data_type (payload) VALUES(null)| + x = JsonDataType.first + assert_equal(nil, x.payload) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index d57794daf8..92e31a3e44 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -49,6 +49,33 @@ module ActiveRecord assert_equal expect, id end + def test_insert_sql_with_returning_disabled + connection = connection_without_insert_returning + id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)") + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect, id + end + + def test_exec_insert_with_returning_disabled + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq') + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect, result.rows.first.first + end + + def test_exec_insert_with_returning_disabled_and_no_sequence_name_given + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id') + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect, result.rows.first.first + end + + def test_sql_for_insert_with_returning_disabled + connection = connection_without_insert_returning + result = connection.sql_for_insert('sql', nil, nil, nil, 'binds') + assert_equal ['sql', 'binds'], result + end + def test_serial_sequence assert_equal 'public.accounts_id_seq', @connection.serial_sequence('accounts', 'id') @@ -179,6 +206,17 @@ module ActiveRecord assert_equal Arel.sql('$2'), bind end + def test_partial_index + @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100" + index = @connection.indexes('ex').find { |idx| idx.name == 'partial' } + assert_equal "(number > 100)", index.where + end + + def test_distinct_with_nulls + assert_equal "DISTINCT posts.title, posts.updater_id AS alias_0", @connection.distinct("posts.title", ["posts.updater_id desc nulls first"]) + assert_equal "DISTINCT posts.title, posts.updater_id AS alias_0", @connection.distinct("posts.title", ["posts.updater_id desc nulls last"]) + end + private def insert(ctx, data) binds = data.map { |name, value| @@ -193,6 +231,10 @@ module ActiveRecord ctx.exec_insert(sql, 'SQL', binds) end + + def connection_without_insert_returning + ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false)) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 172055f15c..f8a605b67c 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -19,6 +19,18 @@ module ActiveRecord assert_equal 'f', @conn.type_cast(false, nil) assert_equal 'f', @conn.type_cast(false, c) end + + def test_quote_float_nan + nan = 0.0/0 + c = Column.new(nil, 1, 'float') + assert_equal "'NaN'", @conn.quote(nan, c) + end + + def test_quote_float_infinity + infinity = 1.0/0 + c = Column.new(nil, 1, 'float') + assert_equal "'Infinity'", @conn.quote(infinity, c) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 18670b4177..9208f53997 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -24,6 +24,8 @@ class SchemaTest < ActiveRecord::TestCase 'moment timestamp without time zone default now()' ] PK_TABLE_NAME = 'table_with_pk' + UNMATCHED_SEQUENCE_NAME = 'unmatched_primary_key_default_value_seq' + UNMATCHED_PK_TABLE_NAME = 'table_with_unmatched_sequence_for_pk' class Thing1 < ActiveRecord::Base self.table_name = "test_schema.things" @@ -60,6 +62,8 @@ class SchemaTest < ActiveRecord::TestCase @connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);" @connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);" @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)" + @connection.execute "CREATE SEQUENCE #{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" + @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME} (id integer NOT NULL DEFAULT nextval('#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}'::regclass), CONSTRAINT unmatched_pkey PRIMARY KEY (id))" end def teardown @@ -67,6 +71,45 @@ class SchemaTest < ActiveRecord::TestCase @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE" end + def test_schema_names + assert_equal ["public", "test_schema", "test_schema2"], @connection.schema_names + end + + def test_create_schema + begin + @connection.create_schema "test_schema3" + assert @connection.schema_names.include? "test_schema3" + ensure + @connection.drop_schema "test_schema3" + end + end + + def test_raise_create_schema_with_existing_schema + begin + @connection.create_schema "test_schema3" + assert_raises(ActiveRecord::StatementInvalid) do + @connection.create_schema "test_schema3" + end + ensure + @connection.drop_schema "test_schema3" + end + end + + def test_drop_schema + begin + @connection.create_schema "test_schema3" + ensure + @connection.drop_schema "test_schema3" + end + assert !@connection.schema_names.include?("test_schema3") + end + + def test_raise_drop_schema_with_nonexisting_schema + assert_raises(ActiveRecord::StatementInvalid) do + @connection.drop_schema "test_schema3" + end + end + def test_schema_change_with_prepared_stmt altered = false @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]] @@ -179,13 +222,13 @@ class SchemaTest < ActiveRecord::TestCase end def test_raise_on_unquoted_schema_name - assert_raise(ActiveRecord::StatementInvalid) do + assert_raises(ActiveRecord::StatementInvalid) do with_schema_search_path '$user,public' end end def test_without_schema_search_path - assert_raise(ActiveRecord::StatementInvalid) { columns(TABLE_NAME) } + assert_raises(ActiveRecord::StatementInvalid) { columns(TABLE_NAME) } end def test_ignore_nil_schema_search_path @@ -241,12 +284,12 @@ class SchemaTest < ActiveRecord::TestCase def test_pk_and_sequence_for_with_schema_specified [ %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), - %(#{SCHEMA_NAME}."#{PK_TABLE_NAME}"), - %(#{SCHEMA_NAME}.#{PK_TABLE_NAME}) + %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") ].each do |given| pk, seq = @connection.pk_and_sequence_for(given) assert_equal 'id', pk, "primary key should be found when table referenced as #{given}" - assert_equal "#{SCHEMA_NAME}.#{PK_TABLE_NAME}_id_seq", seq, "sequence name should be found when table referenced as #{given}" + assert_equal "#{PK_TABLE_NAME}_id_seq", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}") + assert_equal "#{UNMATCHED_SEQUENCE_NAME}", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") end end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index 337f43c421..26507ad654 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -27,4 +27,69 @@ class TimestampTest < ActiveRecord::TestCase d = Developer.create!(:name => 'aaron', :updated_at => -1.0 / 0.0) assert_equal(-1.0 / 0.0, d.updated_at) end + + def test_default_datetime_precision + ActiveRecord::Base.connection.create_table(:foos) + ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime + ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime + assert_nil activerecord_column_option('foos', 'created_at', 'precision') + end + + def test_timestamp_data_type_with_precision + ActiveRecord::Base.connection.create_table(:foos) + ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, :precision => 0 + ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime, :precision => 5 + assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision') + assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision') + end + + def test_timestamps_helper_with_custom_precision + ActiveRecord::Base.connection.create_table(:foos) do |t| + t.timestamps :precision => 4 + end + assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') + assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') + end + + def test_passing_precision_to_timestamp_does_not_set_limit + ActiveRecord::Base.connection.create_table(:foos) do |t| + t.timestamps :precision => 4 + end + assert_nil activerecord_column_option("foos", "created_at", "limit") + assert_nil activerecord_column_option("foos", "updated_at", "limit") + end + + def test_invalid_timestamp_precision_raises_error + assert_raises ActiveRecord::ActiveRecordError do + ActiveRecord::Base.connection.create_table(:foos) do |t| + t.timestamps :precision => 7 + end + end + end + + def test_postgres_agrees_with_activerecord_about_precision + ActiveRecord::Base.connection.create_table(:foos) do |t| + t.timestamps :precision => 4 + end + assert_equal '4', pg_datetime_precision('foos', 'created_at') + assert_equal '4', pg_datetime_precision('foos', 'updated_at') + end + + private + + def pg_datetime_precision(table_name, column_name) + results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'") + result = results.find do |result_hash| + result_hash["column_name"] == column_name + end + result && result["datetime_precision"] + end + + def activerecord_column_option(tablename, column_name, option) + result = ActiveRecord::Base.connection.columns(tablename).find do |column| + column.name == column_name + end + result && result.send(option) + end + end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 575b4806c1..7eef4ace81 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -57,6 +57,14 @@ class CopyTableTest < ActiveRecord::TestCase end end + def test_copy_table_with_unconventional_primary_key + test_copy_table('owners', 'owners_unconventional') do |from, to, options| + original_pk = @connection.primary_key('owners') + copied_pk = @connection.primary_key('owners_unconventional') + assert_equal original_pk, copied_pk + end + end + protected def copy_table(from, to, options = {}) @connection.copy_table(from, to, {:temporary => true}.merge(options)) diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 46da1b0a2b..2ba9143cd5 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -1,10 +1,11 @@ require "cases/helper" require 'bigdecimal' require 'yaml' +require 'securerandom' module ActiveRecord module ConnectionAdapters - class SQLiteAdapter + class SQLite3Adapter class QuotingTest < ActiveRecord::TestCase def setup @conn = Base.sqlite3_connection :database => ':memory:', @@ -12,6 +13,14 @@ module ActiveRecord :timeout => 100 end + def test_type_cast_binary_encoding_without_logger + @conn.extend(Module.new { def logger; end }) + column = Struct.new(:type, :name).new(:string, "foo") + binary = SecureRandom.hex + expected = binary.dup.encode!('utf-8') + assert_equal expected, @conn.type_cast(binary, column) + end + def test_type_cast_symbol assert_equal 'foo', @conn.type_cast(:foo, nil) end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 17bde6cb62..4e26c5dda1 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -20,6 +20,14 @@ module ActiveRecord number integer ) eosql + + @conn.extend(LogIntercepter) + @conn.intercepted = true + end + + def teardown + @conn.intercepted = false + @conn.logged = [] end def test_column_types @@ -58,18 +66,6 @@ module ActiveRecord end end - def test_connection_no_adapter - assert_raises(ArgumentError) do - Base.sqlite3_connection :database => ':memory:' - end - end - - def test_connection_wrong_adapter - assert_raises(ArgumentError) do - Base.sqlite3_connection :database => ':memory:',:adapter => 'vuvuzela' - end - end - def test_bad_timeout assert_raises(TypeError) do Base.sqlite3_connection :database => ':memory:', @@ -239,13 +235,23 @@ module ActiveRecord end def test_tables_logs_name - name = "hello" - assert_logged [[name, []]] do - @conn.tables(name) + assert_logged [['SCHEMA', []]] do + @conn.tables('hello') assert_not_nil @conn.logged.first.shift end end + def test_indexes_logs_name + assert_logged [["PRAGMA index_list(\"items\")", 'SCHEMA', []]] do + @conn.indexes('items', 'hello') + end + end + + def test_table_exists_logs_name + assert @conn.table_exists?('items') + assert_equal 'SCHEMA', @conn.logged[0][1] + end + def test_columns columns = @conn.columns('items').sort_by { |x| x.name } assert_equal 2, columns.length @@ -281,7 +287,6 @@ module ActiveRecord end def test_indexes_logs - intercept_logs_on @conn assert_difference('@conn.logged.length') do @conn.indexes('items') end @@ -333,21 +338,10 @@ module ActiveRecord private def assert_logged logs - intercept_logs_on @conn yield assert_equal logs, @conn.logged end - def intercept_logs_on ctx - @conn.extend(Module.new { - attr_accessor :logged - def log sql, name, binds = [] - @logged << [sql, name, binds] - yield - end - }) - @conn.logged = [] - end end end end diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index ae272e2c4b..2f04c60a9a 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' module ActiveRecord::ConnectionAdapters - class SQLiteAdapter + class SQLite3Adapter class StatementPoolTest < ActiveRecord::TestCase def test_cache_is_per_pid return skip('must support fork') unless Process.respond_to?(:fork) diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index 3e0e6dce2c..48b06a767f 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -109,6 +109,26 @@ class AggregationsTest < ActiveRecord::TestCase 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 diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 588adc38e3..b2eac0349b 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -37,6 +37,13 @@ if ActiveRecord::Base.connection.supports_migrations? end end end + + def test_schema_subclass + Class.new(ActiveRecord::Schema).define(:version => 9) do + create_table :fruits + end + assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + end 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 1160d236c9..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.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key) + 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.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key_symbols) + 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 @@ -168,6 +168,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase sponsor.sponsorable = Member.new :name => "Bert" assert_equal Member, sponsor.association(:sponsorable).send(:klass) + assert_equal "members", sponsor.association(:sponsorable).aliased_table_name end def test_with_polymorphic_and_condition @@ -180,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.find(2, :include => :firm_with_select ).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 @@ -297,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 @@ -333,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.find(:first, :order => "id"), 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 @@ -393,9 +394,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_association_assignment_sticks - post = Post.find(:first) + post = Post.first - author1, author2 = Author.find(:all, :limit => 2) + author1, author2 = Author.all.merge!(:limit => 2).to_a assert_not_nil author1 assert_not_nil author2 @@ -497,14 +498,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do Account.find(@account.id).save! - Account.find(@account.id, :include => :firm).save! + Account.all.merge!(:includes => :firm).find(@account.id).save! end @account.firm.delete assert_nothing_raised do Account.find(@account.id).save! - Account.find(@account.id, :include => :firm).save! + Account.all.merge!(:includes => :firm).find(@account.id).save! end end @@ -517,19 +518,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase authors(:david).destroy end - assert_equal [], AuthorAddress.find_all_by_id([author_address.id, author_address_extra.id]) + assert_equal [], AuthorAddress.where(id: [author_address.id, author_address_extra.id]) assert_equal [author_address.id], AuthorAddress.destroyed_author_address_ids end 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 @@ -704,4 +705,27 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal toy, sponsor.reload.sponsorable end + + test "stale tracking doesn't care about the type" do + apple = Firm.create("name" => "Apple") + citibank = Account.create("credit_limit" => 10) + + citibank.firm_id = apple.id + citibank.firm # load it + + citibank.firm_id = apple.id.to_s + + assert !citibank.association(:firm).stale_target? + end + + def test_reflect_the_most_recent_change + author1, author2 = Author.limit(2) + post = Post.new(:title => "foo", :body=> "bar") + + post.author = author1 + post.author_id = author2.id + + assert post.save + assert_equal post.author_id, author2.id + 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 6733f3e889..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.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") + 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.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id") + 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.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id") + 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.find(:all, :include=>{:posts=>[:comments, :author]}, :order=>"authors.id") + 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.find(:all, :include=>{:posts=>:comments}, :conditions=>"authors.id=1", :order=>"authors.id") + 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.find(:all, :include=>{:account=>{:firm=>:account}}, :order=>"companies.id") + 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.find(:all, :include => :replies, :order => 'topics.id') + 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.find(:all, :include => :replies, :order => ['topics.id', 'replies_topics.id']) + 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.find(:all, :include => :topic, :order => 'topics.id') + 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.find(:first, :include => { :posts => [ :special_comments , :very_special_comment ] }, :order => ['authors.name', 'comments.body', 'very_special_comments_posts.body'], :conditions => 'posts.id = 4') + 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.find(:all, :include => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4') + 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.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC') + 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.find(:first, :include=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id') + 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.find(:first, :include=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC') + 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_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb index 7965bb404c..75a6295350 100644 --- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -24,12 +24,11 @@ class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = false - post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging ) + post = Namespaced::Post.includes(:tagging).find_by_title('Great stuff') assert_nil post.tagging - ActiveRecord::IdentityMap.clear ActiveRecord::Base.store_full_sti_class = true - post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging ) + post = Namespaced::Post.includes(:tagging).find_by_title('Great stuff') assert_instance_of Tagging, post.tagging ensure ActiveRecord::Base.store_full_sti_class = old 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 1e1958410c..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,8 +92,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase end def test_include_query - res = 0 - res = ShapeExpression.find :all, :include => [ :shape, { :paint => :non_poly } ] + 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| @@ -123,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.all :include => includes, :conditions => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name' + 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 07d0b24613..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.find(:all, :include => :octopus) + 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.find(:all, :include => :virus) + 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.find(:all, :include => :passes) + 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.find(:all, :include => :messes) - Mess.find(:all, :include => :crises) + 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.find(:all, :include => :successes) + 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.find(:all, :include => :compresses) + 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 b79c69bbb5..124bf65d3a 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -29,48 +29,43 @@ class EagerAssociationTest < ActiveRecord::TestCase :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books, :developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors - def setup - # preheat table existence caches - Comment.find_by_id(1) - end - def test_eager_with_has_one_through_join_model_with_conditions_on_the_through - member = Member.find(members(:some_other_guy).id, :include => :favourite_club) + 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.find(:all, :include => :comments) + 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.find(:first, :include => :comments, :conditions => "posts.title = 'Welcome to the weblog'") + 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.find(:all, :include => :last_comment) + 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.find(:all, :include => :last_comment, :order => 'comments.id DESC') + 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.find( - :all, :include => :comments, :references => :comments, - :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'" - ) + posts = authors(:david).posts.references(:comments).merge( + :includes => :comments, + :where => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'" + ).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.find(:all, :include => :comments, :order => "posts.id DESC") + list = Post.all.merge!(:includes => :comments, :order => "posts.id DESC").to_a [:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments, :authorless, :thinking, :welcome ].each_with_index do |post, index| @@ -84,14 +79,14 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_loading_with_multiple_associations - posts = Post.find(:all, :include => [ :comments, :author, :categories ], :order => "posts.id") + 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.find :all, :conditions => 'post_id = 1', :include => [:post => :author] + comments = Comment.all.merge!(:where => 'post_id = 1', :includes => [:post => :author]).to_a assert_no_queries do comments.each {|comment| comment.post.author.name} end @@ -99,25 +94,25 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.find(:all, :include=>:comments) + 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.find(:all, :include=>:comments) + 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.find(:all, :include=>:categories) + 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.find(:all, :include=>:categories) + posts = Post.all.merge!(:includes=>:categories).to_a assert_equal 11, posts.size end @@ -154,8 +149,8 @@ class EagerAssociationTest < ActiveRecord::TestCase popular_post.readers.create!(:person => people(:michael)) popular_post.readers.create!(:person => people(:david)) - readers = Reader.find(:all, :conditions => ["post_id = ?", popular_post.id], - :include => {:post => :comments}) + readers = Reader.all.merge!(:where => ["post_id = ?", popular_post.id], + :includes => {:post => :comments}).to_a readers.each do |reader| assert_equal [comment], reader.post.comments end @@ -167,8 +162,8 @@ class EagerAssociationTest < ActiveRecord::TestCase car_post.categories << categories(:technology) comment = car_post.comments.create!(:body => "hmm") - categories = Category.find(:all, :conditions => { 'posts.id' => car_post.id }, - :include => {:posts => :comments}) + categories = Category.all.merge!(:where => { 'posts.id' => car_post.id }, + :includes => {:posts => :comments}).to_a categories.each do |category| assert_equal [comment], category.posts[0].comments end @@ -186,10 +181,10 @@ 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.find(author_id, :include => {:posts_with_comments => :comments}) } # 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.uniq! + assert_nil post_with_comments.comments.to_a.uniq! end end @@ -197,7 +192,7 @@ class EagerAssociationTest < ActiveRecord::TestCase author = authors(:david) post = author.post_about_thinking_with_last_comment last_comment = post.last_comment - author = assert_queries(ActiveRecord::IdentityMap.enabled? ? 2 : 3) { Author.find(author.id, :include => {:post_about_thinking_with_last_comment => :last_comment})} # find the author, then find the posts, then find the comments + author = assert_queries(3) { Author.all.merge!(:includes => {:post_about_thinking_with_last_comment => :last_comment}).find(author.id)} # find the author, then find the posts, then find the comments assert_no_queries do assert_equal post, author.post_about_thinking_with_last_comment assert_equal last_comment, author.post_about_thinking_with_last_comment.last_comment @@ -208,7 +203,7 @@ class EagerAssociationTest < ActiveRecord::TestCase post = posts(:welcome) author = post.author author_address = author.author_address - post = assert_queries(ActiveRecord::IdentityMap.enabled? ? 2 : 3) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author, then find the address + post = assert_queries(3) { Post.all.merge!(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author, then find the address assert_no_queries do assert_equal author, post.author_with_address assert_equal author_address, post.author_with_address.author_address @@ -218,7 +213,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once post = posts(:welcome) post.update_attributes!(:author => nil) - post = assert_queries(1) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author which is null so no query for the author or address + post = assert_queries(1) { Post.all.merge!(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author which is null so no query for the author or address assert_no_queries do assert_equal nil, post.author_with_address end @@ -227,85 +222,85 @@ 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.find(sponsor.id, :include => :sponsorable) } + 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.find(:all, :include => :comments, :order => "posts.id") + 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.find(:all, :include => :hello_posts_with_hash_conditions) + Author.all.merge!(:includes => :hello_posts_with_hash_conditions).to_a end - assert !Author.find(authors(:david).id, :include => :hello_posts_with_hash_conditions).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.find(posts(:authorless).id, :include => :author).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.find(posts(:authorless).id, :include => {:author => :author_addresss}) + Post.all.merge!(:includes => {:author => :author_addresss}).find(posts(:authorless).id) end end def test_nested_loading_through_has_one_association - aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}) + 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.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'author_addresses.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.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'authors.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.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'posts.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.find( - author_addresses(:david_address).id, :include => {:author => :posts}, - :conditions => "author_addresses.id > 0", :references => :author_addresses - ) + aa = AuthorAddress.references(:author_addresses).merge( + :includes => {:author => :posts}, + :where => "author_addresses.id > 0" + ).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_on_association - aa = AuthorAddress.find( - author_addresses(:david_address).id, :include => {:author => :posts}, - :conditions => "authors.id > 0", :references => :authors - ) + aa = AuthorAddress.references(:authors).merge( + :includes => {:author => :posts}, + :where => "authors.id > 0" + ).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_on_nested_association - aa = AuthorAddress.find( - author_addresses(:david_address).id, :include => {:author => :posts}, - :conditions => "posts.id > 0", :references => :posts - ) + aa = AuthorAddress.references(:posts).merge( + :includes => {:author => :posts}, + :where => "posts.id > 0" + ).find author_addresses(:david_address).id assert_equal aa.author.posts.count, aa.author.posts.length end def test_eager_association_loading_with_belongs_to_and_foreign_keys - pets = Pet.find(:all, :include => :owner) + pets = Pet.all.merge!(:includes => :owner).to_a assert_equal 3, pets.length end def test_eager_association_loading_with_belongs_to - comments = Comment.find(:all, :include => :post) + comments = Comment.all.merge!(:includes => :post).to_a assert_equal 11, comments.length titles = comments.map { |c| c.post.title } assert titles.include?(posts(:welcome).title) @@ -313,31 +308,31 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_association_loading_with_belongs_to_and_limit - comments = Comment.find(:all, :include => :post, :limit => 5, :order => 'comments.id') + 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.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :order => 'comments.id') + 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.find(:all, :include => :post, :limit => 3, :offset => 2, :order => 'comments.id') + 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.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id') + 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.find(:all, :include => :post, :conditions => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id') + comments = Comment.all.merge!(:includes => :post, :where => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id').to_a assert_equal 3, comments.length assert_equal [6,7,8], comments.collect { |c| c.id } end @@ -345,7 +340,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name assert_nothing_raised do ActiveSupport::Deprecation.silence do - Comment.find(:all, :include => :post, :conditions => ['posts.id = ?',4]) + Comment.all.merge!(:includes => :post, :where => ['posts.id = ?',4]).to_a end end end @@ -353,7 +348,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_hash comments = [] assert_nothing_raised do - comments = Comment.find(:all, :include => :post, :conditions => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id') + comments = Comment.all.merge!(:includes => :post, :where => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id').to_a end assert_equal 3, comments.length assert_equal [5,6,7], comments.collect { |c| c.id } @@ -366,14 +361,14 @@ class EagerAssociationTest < ActiveRecord::TestCase quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id') assert_nothing_raised do ActiveSupport::Deprecation.silence do - Comment.find(:all, :include => :post, :conditions => ["#{quoted_posts_id} = ?",4]) + 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.find(:all, :include => :post, :order => 'posts.id') + Comment.all.merge!(:includes => :post, :order => 'posts.id').to_a end end @@ -381,55 +376,55 @@ 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.find(:all, :include => :post, :order => quoted_posts_id) + 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.find(:all, :include => [:author, :very_special_comment], :limit => 1, :order => 'posts.id') + 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.find(:all, :include => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id') + 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.find(:first, :include => :favorite_author) + author_favorite = AuthorFavorite.all.merge!(:includes => :favorite_author).first assert_equal authors(:mary), assert_no_queries { author_favorite.favorite_author } end def test_eager_load_belongs_to_quotes_table_and_column_names - job = Job.find jobs(:unicyclist).id, :include => :ideal_reference + job = Job.includes(:ideal_reference).find jobs(:unicyclist).id references(:michael_unicyclist) assert_no_queries{ assert_equal references(:michael_unicyclist), job.ideal_reference} end def test_eager_load_has_one_quotes_table_and_column_names - michael = Person.find(people(:michael), :include => :favourite_reference) + 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.find(people(:michael), :include => :references) + 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.find(people(:michael), :include => :jobs) + 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.find(subscribers(:second).id, :include => :subscriptions) + subscriber =Subscriber.all.merge!(:includes => :subscriptions).find(subscribers(:second).id) assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id) end @@ -447,25 +442,25 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_load_has_many_through_with_string_keys books = books(:awdr, :rfr) - subscriber = Subscriber.find(subscribers(:second).id, :include => :books) + 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.find(subscriptions(:webster_awdr).id, :include => :subscriber) + 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.find(:all, :include => :comments, :joins => "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", :limit => 1, :order => 'author_id') + 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.find(:all, :include => :comments, :order => 'posts.id') - posts_with_author = people(:michael).posts.find(:all, :include => :author, :order => 'posts.id') - posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ], :order => 'posts.id') + posts_with_comments = people(:michael).posts.merge(:includes => :comments, :order => 'posts.id').to_a + posts_with_author = people(:michael).posts.merge(:includes => :author, :order => 'posts.id').to_a + posts_with_comments_and_author = people(:michael).posts.merge(:includes => [ :comments, :author ], :order => 'posts.id').to_a assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size } assert_equal authors(:david), assert_no_queries { posts_with_author.first.author } assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author } @@ -476,32 +471,32 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.create!(:author => author, :title => "TITLE", :body => "BODY") author.author_favorites.create(:favorite_author_id => 1) author.author_favorites.create(:favorite_author_id => 2) - posts_with_author_favorites = author.posts.find(:all, :include => :author_favorites) + 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.find(:first, :include => :special_post_comments, :order => 'authors.id') + 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.find(:first, :include => :special_nonexistant_post_comments, :order => 'authors.id') + 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.find(:first, :include => :hello_post_comments, - :order => 'authors.id').hello_post_comments.sort_by(&:id), - Author.find(:first, :order => 'authors.id').hello_post_comments.sort_by(&:id) + assert_equal Author.all.merge!(:includes => :hello_post_comments, + :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.find(authors(:david).id, :include => :comments_with_order_and_conditions).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.find(authors(:david).id, :include => :comments_with_include).comments_with_include.to_a + author_comments = Author.all.merge!(:includes => :comments_with_include).find(authors(:david).id).comments_with_include.to_a assert_no_queries do author_comments.first.post.title end @@ -509,7 +504,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_through_with_conditions_join_model_with_include post_tags = Post.find(posts(:welcome).id).misc_tags - eager_post_tags = Post.find(1, :include => :misc_tags).misc_tags + eager_post_tags = Post.all.merge!(:includes => :misc_tags).find(1).misc_tags assert_equal post_tags, eager_post_tags end @@ -520,16 +515,16 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit - posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2) + 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.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id") + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id").to_a else - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.body = 'hello'", :order => "posts.id") + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a end assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } @@ -537,9 +532,9 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions_array if current_adapter?(:OpenBaseAdapter) - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id") + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id").to_a else - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "posts.body = ?", 'hello' ], :order => "posts.id") + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a end assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } @@ -547,7 +542,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers posts = ActiveSupport::Deprecation.silence do - Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) + Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "authors.name = ?", 'David' ]).to_a end assert_equal 2, posts.size @@ -558,41 +553,41 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit_and_high_offset - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => { 'authors.name' => 'David' }) + 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.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, - :conditions => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ], - :references => [:authors, :comments]) + posts = Post.references(:authors, :comments). + 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.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, - :conditions => { 'authors.name' => 'David', 'comments.body' => 'go crazy' }) + 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.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => { 'authors.name' => 'David' }) + 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.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.title = 'magic forest'") + posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.title = 'magic forest'").to_a assert_equal 0, posts.size end def test_eager_count_performed_on_a_has_many_association_with_multi_table_conditional author = authors(:david) author_posts_without_comments = author.posts.select { |post| post.comments.blank? } - assert_equal author_posts_without_comments.size, author.posts.count(:all, :include => :comments, :conditions => 'comments.id is null', :references => :comments) + assert_equal author_posts_without_comments.size, author.posts.includes(:comments).where('comments.id is null').references(:comments).count end def test_eager_count_performed_on_a_has_many_through_association_with_multi_table_conditional @@ -602,7 +597,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_and_belongs_to_many_and_limit - posts = Post.find(:all, :include => :categories, :order => "posts.id", :limit => 3) + 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 @@ -611,9 +606,9 @@ class EagerAssociationTest < ActiveRecord::TestCase assert posts[1].categories.include?(categories(:general)) end - # This is only really relevant when the identity map is off. Since the preloader for habtm - # gets raw row hashes from the database and then instantiates them, this test ensures that - # it only instantiates one actual object per record from the database. + # Since the preloader for habtm gets raw row hashes from the database and then + # instantiates them, this test ensures that it only instantiates one actual + # object per record from the database. def test_has_and_belongs_to_many_should_not_instantiate_same_records_multiple_times welcome = posts(:welcome) categories = Category.includes(:posts) @@ -628,80 +623,47 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers - posts = authors(:david).posts.find(:all, - :include => :comments, - :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", - :references => :comments, - :limit => 2 - ) + posts = + authors(:david).posts + .includes(:comments) + .where("comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'") + .references(:comments) + .limit(2) + .to_a assert_equal 2, posts.size - count = Post.count( - :include => [ :comments, :author ], - :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", - :references => [:authors, :comments], - :limit => 2 - ) + count = + Post.includes(:comments, :author) + .where("authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')") + .references(:authors, :comments) + .limit(2) + .count assert_equal count, posts.size end def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers posts = nil - Post.send(:with_scope, :find => { - :include => :comments, - :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", - :references => :comments - }) do - posts = authors(:david).posts.find(:all, :limit => 2) - assert_equal 2, posts.size - end + Post.includes(:comments) + .where("comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'") + .references(:comments) + .scoping do - Post.send(:with_scope, :find => { - :include => [ :comments, :author ], - :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", - :references => [:authors, :comments] - }) do - count = Post.count(:limit => 2) - assert_equal count, posts.size + posts = authors(:david).posts.limit(2).to_a + assert_equal 2, posts.size end - end - def test_eager_with_has_many_and_limit_and_scoped_and_explicit_conditions_on_the_eagers - Post.send(:with_scope, :find => { :conditions => "1=1" }) do - posts = authors(:david).posts.find(:all, - :include => :comments, - :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", - :references => :comments, - :limit => 2 - ) - assert_equal 2, posts.size + Post.includes(:comments, :author) + .where("authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')") + .references(:authors, :comments) + .scoping do - count = Post.count( - :include => [ :comments, :author ], - :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", - :references => [:authors, :comments], - :limit => 2 - ) + count = Post.limit(2).count assert_equal count, posts.size end end - def test_eager_with_scoped_order_using_association_limiting_without_explicit_scope - posts_with_explicit_order = Post.find( - :all, :conditions => 'comments.id is not null', :references => :comments, - :include => :comments, :order => 'posts.id DESC', :limit => 2 - ) - posts_with_scoped_order = Post.send(:with_scope, :find => {:order => 'posts.id DESC'}) do - Post.find( - :all, :conditions => 'comments.id is not null', - :references => :comments, :include => :comments, :limit => 2 - ) - end - assert_equal posts_with_explicit_order, posts_with_scoped_order - end - def test_eager_association_loading_with_habtm - posts = Post.find(:all, :include => :categories, :order => "posts.id") + 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 @@ -710,23 +672,23 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_inheritance - SpecialPost.find(:all, :include => [ :comments ]) + SpecialPost.all.merge!(:includes => [ :comments ]).to_a end def test_eager_has_one_with_association_inheritance - post = Post.find(4, :include => [ :very_special_comment ]) + 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.find(4, :include => [ :special_comments ]) + 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.find(6, :include => [ :special_categories ]) + 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 @@ -735,8 +697,8 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_one_dependent_does_not_destroy_dependent assert_not_nil companies(:first_firm).account - f = Firm.find(:first, :include => :account, - :conditions => ["companies.name = ?", "37signals"]) + 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 end @@ -750,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.find(6, :include=> :monkeys ) + Post.all.merge!(:includes=> :monkeys ).find(6) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.find(6, :include=>[ :monkeys ]) + Post.all.merge!(:includes=>[ :monkeys ]).find(6) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.find(6, :include=>[ 'monkeys' ]) + 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.find(6, :include=>[ :monkeys, :elephants ]) + 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 @@ -773,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 @@ -781,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 @@ -789,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 @@ -797,59 +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.find(:all, :order=>"#{className.table_name}.#{className.primary_key}", :include=>include) + 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.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.all.merge!( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => 'UPPER(posts.title)', :limit => 2, :offset => 1 - ) + ).to_a ) assert_equal( posts(:sti_post_and_comments, :sti_comments), - Post.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.all.merge!( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1 - ) + ).to_a ) end def test_limited_eager_with_multiple_order_columns assert_equal( posts(:thinking, :sti_comments), - Post.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.all.merge!( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => ['UPPER(posts.title)', 'posts.id'], :limit => 2, :offset => 1 - ) + ).to_a ) assert_equal( posts(:sti_post_and_comments, :sti_comments), - Post.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.all.merge!( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => ['UPPER(posts.title) DESC', 'posts.id'], :limit => 2, :offset => 1 - ) + ).to_a ) end def test_limited_eager_with_numeric_in_association assert_equal( people(:david, :susan), - Person.find( - :all, :include => [:readers, :primary_contact, :number1_fan], - :conditions => "number1_fans_people.first_name like 'M%'", - :references => :number1_fans_people, + 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 - ) + ).to_a ) end @@ -862,9 +823,9 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_polymorphic_type_condition - post = Post.find(posts(:thinking).id, :include => :taggings) + post = Post.all.merge!(:includes => :taggings).find(posts(:thinking).id) assert post.taggings.include?(taggings(:thinking_general)) - post = SpecialPost.find(posts(:thinking).id, :include => :taggings) + post = SpecialPost.all.merge!(:includes => :taggings).find(posts(:thinking).id) assert post.taggings.include?(taggings(:thinking_general)) end @@ -915,13 +876,13 @@ class EagerAssociationTest < ActiveRecord::TestCase end end def test_eager_with_valid_association_as_string_not_symbol - assert_nothing_raised { Post.find(:all, :include => 'comments') } + 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.find :all, :conditions => "123.456 = 123.456", :include => :post + Comment.all.merge!(:where => "123.456 = 123.456", :includes => :post).to_a end end @@ -965,31 +926,31 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_count_with_include if current_adapter?(:SybaseAdapter) - assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15", :references => :comments) + assert_equal 3, authors(:david).posts_with_comments.where("len(comments.body) > 15").references(:comments).count elsif current_adapter?(:OpenBaseAdapter) - assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(FETCHBLOB(comments.body)) > 15", :references => :comments) + assert_equal 3, authors(:david).posts_with_comments.where("length(FETCHBLOB(comments.body)) > 15").references(:comments).count else - assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(comments.body) > 15", :references => :comments) + assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count end end def test_load_with_sti_sharing_association assert_queries(2) do #should not do 1 query per subclass - Comment.find :all, :include => :post + Comment.includes(:post).to_a end end def test_conditions_on_join_table_with_include_and_limit - assert_equal 3, Developer.find(:all, :include => 'projects', :conditions => { 'developers_projects.access_level' => 1 }, :limit => 5).size + 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.find(:all, :include => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).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.find(:all, :joins => :comments, :include => :author, :order => 'comments.id DESC') + 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} @@ -997,64 +958,72 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_loading_with_conditions_on_joined_table_preloads posts = assert_queries(2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + 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(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + posts = assert_queries(2) do + 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(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'", :order => 'posts.id') + posts = assert_queries(2) do + 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(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2", :order => 'posts.id') + posts = assert_queries(2) do + 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.find(:all, :select => 'distinct posts.*', :include => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + 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(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + posts = assert_queries(2) do + Post.all.merge!(:select => 'distinct posts.*', :includes => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :where => "comments.body like 'Thank you%'", :order => 'posts.id').to_a end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} - end def test_eager_loading_with_select_on_joined_table_preloads posts = assert_queries(2) do - Post.find(:all, :select => 'posts.*, authors.name as author_name', :include => :comments, :joins => :author, :order => 'posts.id') + Post.all.merge!(:select => 'posts.*, authors.name as author_name', :includes => :comments, :joins => :author, :order => 'posts.id').to_a end assert_equal 'David', posts[0].author_name assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments} end def test_eager_loading_with_conditions_on_join_model_preloads - Author.columns - authors = assert_queries(2) do - Author.find(:all, :include => :author_address, :joins => :comments, :conditions => "posts.title like 'Welcome%'") + 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.find(:all, :include => :primary_contact) + 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} @@ -1063,15 +1032,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_preload_has_many_uses_exclusive_scope - people = Person.males.find :all, :include => :agents + people = Person.males.includes(:agents).to_a people.each do |person| assert_equal Person.find(person.id).agents, person.agents end end def test_preload_has_many_using_primary_key - expected = Firm.find(:first).clients_using_primary_key.to_a - firm = Firm.find :first, :include => :clients_using_primary_key + expected = Firm.first.clients_using_primary_key.to_a + firm = Firm.includes(:clients_using_primary_key).first assert_no_queries do assert_equal expected, firm.clients_using_primary_key end @@ -1081,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.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies'[0,30]+'.name' + firm = Firm.all.merge!(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies'[0,30]+'.name').find(1) else - firm = Firm.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name' + 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 @@ -1092,7 +1061,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preload_has_one_using_primary_key expected = accounts(:signals37) - firm = Firm.find :first, :include => :account_using_primary_key, :order => 'companies.id' + 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 @@ -1100,7 +1069,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_include_has_one_using_primary_key expected = accounts(:signals37) - firm = Firm.find(:all, :include => :account_using_primary_key, :order => 'accounts.id').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 @@ -1116,7 +1085,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preloading_empty_belongs_to_polymorphic t = Tagging.create!(:taggable_type => 'Post', :taggable_id => Post.maximum(:id) + 1, :tag => tags(:general)) - tagging = assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) { Tagging.preload(:taggable).find(t.id) } + tagging = assert_queries(2) { Tagging.preload(:taggable).find(t.id) } assert_no_queries { assert_nil tagging.taggable } end @@ -1164,9 +1133,26 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_deep_including_through_habtm - posts = Post.find(:all, :include => {:categories => :categorizations}, :order => "posts.id") + 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 } end + + test "scoping with a circular preload" do + assert_equal Comment.find(1), Comment.preload(:post => :comments).scoping { Comment.find(1) } + end + + test "circular preload does not modify unscoped" do + expected = FirstPost.unscoped.find(2) + FirstPost.preload(:comments => :first_post).find(1) + assert_equal expected, FirstPost.unscoped.find(2) + end + + test "preload ignores the scoping" do + assert_equal( + Comment.find(1).post, + Post.where('1 = 0').scoping { Comment.preload(:post).find(1).post } + ) + end end 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 f457dfb9b3..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 @@ -123,11 +126,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert active_record.developers.include?(david) end - def test_triple_equality - assert !(Array === Developer.find(1).projects) - assert Developer.find(1).projects === Array - end - def test_adding_single jamis = Developer.find(2) jamis.projects.reload # causing the collection to load @@ -338,6 +336,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, project.developers.size end + def test_uniq_when_association_already_loaded + project = projects(:active_record) + project.developers << [ developers(:jamis), developers(:david), developers(:jamis), developers(:david) ] + assert_equal 3, Project.includes(:developers).find(project.id).developers.size + end + def test_deleting david = Developer.find(1) active_record = Project.find(1) @@ -355,7 +359,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_deleting_array david = Developer.find(1) david.projects.reload - david.projects.delete(Project.find(:all)) + david.projects.delete(Project.all.to_a) assert_equal 0, david.projects.size assert_equal 0, david.projects(true).size end @@ -375,10 +379,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase active_record.developers.reload assert_equal 3, active_record.developers_by_sql.size - active_record.developers_by_sql.delete(Developer.find(:all)) + active_record.developers_by_sql.delete(Developer.all) assert_equal 0, active_record.developers_by_sql(true).size end + def test_deleting_all_with_sql + project = Project.find(1) + project.developers_by_sql.delete_all + assert_equal 0, project.developers_by_sql.size + end + def test_deleting_all david = Developer.find(1) david.projects.reload @@ -416,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) @@ -497,7 +507,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_include_uses_array_include_after_loaded project = projects(:active_record) - project.developers.class # force load target + project.developers.load_target developer = project.developers.first @@ -548,41 +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.find(:all).size - assert_equal 3, projects(:active_record).limited_developers.find(:all, :limit => nil).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.find(:first, :conditions => "name = 'Jamis'") + 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_dynamic_find_all_should_respect_association_order - # Developers are ordered 'name DESC, id DESC' - low_id_jamis = developers(:jamis) - middle_id_jamis = developers(:poor_jamis) - high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis') - - assert_equal [high_id_jamis, middle_id_jamis, low_id_jamis], projects(:active_record).developers.find(:all, :conditions => "name = 'Jamis'") - assert_equal [high_id_jamis, middle_id_jamis, low_id_jamis], projects(:active_record).developers.find_all_by_name('Jamis') - end - - def test_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 - end - - def test_dynamic_find_all_should_respect_association_limit - assert_equal 1, projects(:active_record).limited_developers.find(:all, :conditions => "name = 'Jamis'").length - assert_equal 1, projects(:active_record).limited_developers.find_all_by_name('Jamis').length - end - - def test_dynamic_find_all_order_should_override_association_limit - assert_equal 2, projects(:active_record).limited_developers.find(:all, :conditions => "name = 'Jamis'", :limit => 9_000).length - assert_equal 2, projects(:active_record).limited_developers.find_all_by_name('Jamis', :limit => 9_000).length + assert_equal ['projects.id', 'developers.name desc, developers.id desc'], ordered_developers.order_values end def test_dynamic_find_all_should_respect_readonly_access @@ -603,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 @@ -632,7 +622,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_consider_type - developer = Developer.find(:first) + developer = Developer.first special_project = SpecialProject.create("name" => "Special Project") other_project = developer.projects.first @@ -649,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} @@ -667,7 +657,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase categories(:technology).select_testing_posts(true).each do |o| assert_respond_to o, :correctness_marker end - assert_respond_to categories(:technology).select_testing_posts.find(:first), :correctness_marker + assert_respond_to categories(:technology).select_testing_posts.first, :correctness_marker end def test_habtm_selects_all_columns_by_default @@ -681,10 +671,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_join_table_alias assert_equal( 3, - Developer.find( - :all, :include => {:projects => :developers}, :references => :developers_projects_join, - :conditions => 'developers_projects_join.joined_on IS NOT NULL' - ).size + Developer.references(:developers_projects_join).merge( + :includes => {:projects => :developers}, + :where => 'developers_projects_join.joined_on IS NOT NULL' + ).to_a.size ) end @@ -697,16 +687,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal( 3, - Developer.find( - :all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL', - :references => :developers_projects_join, :group => group.join(",") - ).size + Developer.references(:developers_projects_join).merge( + :includes => {:projects => :developers}, :where => 'developers_projects_join.joined_on IS NOT NULL', + :group => group.join(",") + ).to_a.size ) end def test_find_grouped - all_posts_from_category1 = Post.find(:all, :conditions => "category_id = 1", :joins => :categories) - grouped_posts_of_category1 = Post.find(:all, :conditions => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories) + 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 @@ -780,15 +770,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, project.developers.size assert_equal 1, developer.projects.size - assert_equal developer, project.developers.find(:first) - assert_equal project, developer.projects.find(:first) + assert_equal developer, project.developers.first + assert_equal project, developer.projects.first end def test_self_referential_habtm_without_foreign_key_set_should_raise_exception assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) { - Member.class_eval do - has_and_belongs_to_many :friends, :class_name => "Member", :join_table => "member_friends" - end + SelfMember.new.friends } end @@ -798,13 +786,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert Category.find(1).posts_with_authors_sorted_by_author_id.find_by_title('Welcome to the weblog') end - def test_counting_on_habtm_association_and_not_array - david = Developer.find(1) - # Extra parameter just to make sure we aren't falling back to - # Array#count in Ruby >=1.8.7, which would raise an ArgumentError - assert_nothing_raised { david.projects.count(:all, :conditions => '1=1') } - end - def test_count david = Developer.find(1) assert_equal 2, david.projects.count @@ -827,7 +808,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_association_proxy_transaction_method_starts_transaction_in_association_class Post.expects(:transaction) - Category.find(:first).posts.transaction do + Category.first.posts.transaction do # nothing end end @@ -837,11 +818,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase # clear cache possibly created by other tests david.projects.reset_column_information - assert_queries(1) { david.projects.columns; david.projects.columns } + assert_queries(:any) { david.projects.columns } + assert_no_queries { david.projects.columns } ## and again to verify that reset_column_information clears the cache correctly david.projects.reset_column_information - assert_queries(1) { david.projects.columns; david.projects.columns } + + assert_queries(:any) { david.projects.columns } + assert_no_queries { david.projects.columns } end def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause @@ -860,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 3967009c82..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 @@ -130,6 +136,28 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal car.id, bulb.car_id end + def test_association_protect_foreign_key + invoice = Invoice.create + + line_item = invoice.line_items.new + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.new :invoice_id => invoice.id + 1 + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.build + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.build :invoice_id => invoice.id + 1 + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.create + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.create :invoice_id => invoice.id + 1 + assert_equal invoice.id, line_item.invoice_id + end + def test_association_conditions_bypass_attribute_protection car = Car.create(:name => 'honda') @@ -156,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 @@ -171,7 +199,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_no_sql_should_be_fired_if_association_already_loaded Car.create(:name => 'honda') bulbs = Car.first.bulbs - bulbs.inspect # to load all instances of bulbs + bulbs.to_a # to load all instances of bulbs assert_no_queries do bulbs.first() @@ -199,48 +227,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal person, person.readers.first.person end - def test_find_or_create_by_resets_cached_counters - person = Person.create! :first_name => 'tenderlove' - post = Post.first - - assert_equal [], person.readers - assert_nil person.readers.find_by_post_id(post.id) - - person.readers.find_or_create_by_post_id(post.id) - - assert_equal 1, person.readers.count - assert_equal 1, person.readers.length - assert_equal post, person.readers.first.post - assert_equal person, person.readers.first.person - end - def force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.each {|f| } end # 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.find(:first, :order => "id").clients.count + assert_equal 2, Firm.all.merge!(:order => "id").first.clients.count end def test_counting - assert_equal 2, Firm.find(:first, :order => "id").plain_clients.count - end - - def test_counting_with_empty_hash_conditions - assert_equal 2, Firm.find(:first, :order => "id").plain_clients.count(:conditions => {}) - end - - def test_counting_with_single_conditions - assert_equal 1, Firm.find(:first, :order => "id").plain_clients.count(:conditions => ['name=?', "Microsoft"]) + assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count end def test_counting_with_single_hash - assert_equal 1, Firm.find(:first, :order => "id").plain_clients.count(:conditions => {:name => "Microsoft"}) + 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.find(:first, :order => "id").plain_clients.count(:name) + assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count(:name) end def test_counting_with_association_limit @@ -250,7 +255,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding - assert_equal 2, Firm.find(:first, :order => "id").clients.length + assert_equal 2, Firm.all.merge!(:order => "id").first.clients.length end def test_finding_array_compatibility @@ -259,112 +264,80 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_with_blank_conditions [[], {}, nil, ""].each do |blank| - assert_equal 2, Firm.find(:first, :order => "id").clients.find(:all, :conditions => blank).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.find(:all).size - assert_equal 2, companies(:first_firm).limited_clients.find(:all, :limit => nil).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 - end - - def test_dynamic_find_last_without_specified_order - assert_equal companies(:second_client), companies(:first_firm).unsorted_clients.find_last_by_type('Client') + 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.find(:first, :conditions => "type = 'Client'") + 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 - def test_dynamic_find_all_should_respect_association_order - assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find(:all, :conditions => "type = 'Client'") - 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.find(:all, :conditions => "type = 'Client'").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.find(:all, :conditions => "type = 'Client'", :limit => 9_000).length - assert_equal 2, companies(:first_firm).limited_clients.find_all_by_type('Client', :limit => 9_000).length - end - - def test_dynamic_find_all_should_respect_readonly_access - companies(:first_firm).readonly_clients.find(:all).each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } } - companies(:first_firm).readonly_clients.find(:all).each { |c| assert c.readonly? } - end - - def test_dynamic_find_or_create_from_two_attributes_using_an_association - author = authors(:david) - number_of_posts = Post.count - another = author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") - assert_equal number_of_posts + 1, Post.count - assert_equal another, author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") - assert another.persisted? - end - def test_cant_save_has_many_readonly_association authors(:david).readonly_comments.each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } } authors(:david).readonly_comments.each { |c| assert c.readonly? } end - def test_triple_equality - # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - assert !(Array === Firm.find(:first, :order => "id").clients) - assert Firm.find(:first, :order => "id").clients === Array - end - def test_finding_default_orders - assert_equal "Summit", Firm.find(:first, :order => "id").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.find(:first, :order => "id").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.find(:first, :order => "id").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.find(:first, :order => "id").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.find(:first, :order => "id").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.find(:first, :order => "id").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.find(:first, :order => "id") + 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.find(:first, :order => "id").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.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.find(:first, :order => "id").clients_using_counter_sql.size - assert Firm.find(:first, :order => "id").clients_using_counter_sql.any? - assert_equal 0, Firm.find(:first, :order => "id").clients_using_zero_counter_sql.size - assert !Firm.find(:first, :order => "id").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.find(:first, :order => "id").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 @@ -379,7 +352,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_ids - firm = Firm.find(:first, :order => "id") + firm = Firm.all.merge!(:order => "id").first assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find } @@ -399,7 +372,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_string_ids_when_using_finder_sql - firm = Firm.find(:first, :order => "id") + firm = Firm.order("id").first client = firm.clients_using_finder_sql.find("2") assert_kind_of Client, client @@ -415,9 +388,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_all - firm = Firm.find(:first, :order => "id") - assert_equal 2, firm.clients.find(:all, :conditions => "#{QUOTED_TYPE} = 'Client'").length - assert_equal 1, firm.clients.find(:all, :conditions => "name = 'Summit'").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 @@ -436,7 +409,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) assert_queries(2) do - firm.clients.find_each(:batch_size => 1, :conditions => {:name => "Microsoft"}) do |c| + firm.clients.where(name: 'Microsoft').find_each(batch_size: 1) do |c| assert_equal firm.id, c.firm_id assert_equal "Microsoft", c.name end @@ -461,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.find(:first, :order => "id") - summit = firm.clients.find(:all, :conditions => "name = 'Summit'") - assert_equal summit, firm.clients.find(:all, :conditions => ["name = ?", "Summit"]) - assert_equal summit, firm.clients.find(:all, :conditions => ["name = :name", { :name => "Summit" }]) + 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.find(:first, :order => "id") + firm = Firm.all.merge!(:order => "id").first client2 = Client.find(2) - assert_equal firm.clients.first, firm.clients.find(:first, :order => "id") - assert_equal client2, firm.clients.find(:first, :conditions => "#{QUOTED_TYPE} = 'Client'", :order => "id") + 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.find(:first, :order => "id") + firm = Firm.all.merge!(:order => "id").first client2 = Client.find(2) - assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = ?", 'Client'], :order => "id") - assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }], :order => "id") + 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.find(:all, :joins => :audit_logs, :conditions => {'audit_logs.message' => nil, :name => 'Smith'}) + Developer.all.merge!(:joins => :audit_logs, :where => {'audit_logs.message' => nil, :name => 'Smith'}).to_a end end @@ -493,8 +466,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_grouped - all_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1") - grouped_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count') + 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 @@ -552,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.find(:first, :order => "id") + firm = Firm.all.merge!(:order => "id").first firm.plain_clients.create! end end @@ -717,57 +690,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !companies(:first_firm).clients_of_firm.loaded? end - def test_find_or_initialize - the_client = companies(:first_firm).clients.find_or_initialize_by_name("Yet another client") - assert_equal companies(:first_firm).id, the_client.firm_id - assert_equal "Yet another client", the_client.name - assert !the_client.persisted? - end - - def test_find_or_create_updates_size - number_of_clients = companies(:first_firm).clients.size - the_client = companies(:first_firm).clients.find_or_create_by_name("Yet another client") - assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size - assert_equal the_client, companies(:first_firm).clients.find_or_create_by_name("Yet another client") - assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size - end - - def test_find_or_initialize_updates_collection_size - number_of_clients = companies(:first_firm).clients_of_firm.size - companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client") - assert_equal number_of_clients + 1, companies(:first_firm).clients_of_firm.size - end - - def test_find_or_initialize_returns_the_instantiated_object - client = companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client") - assert_equal client, companies(:first_firm).clients_of_firm[-1] - end - - def test_find_or_initialize_only_instantiates_a_single_object - number_of_clients = Client.count - companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client").save! - companies(:first_firm).save! - assert_equal number_of_clients+1, Client.count - end - - def test_find_or_create_with_hash - post = authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody') - assert_equal post, authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody') - assert post.persisted? - end - - def test_find_or_create_with_one_attribute_followed_by_hash - post = authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody') - assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody') - assert post.persisted? - end - - def test_find_or_create_should_work_with_block - post = authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'} - assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'} - assert post.persisted? - end - def test_deleting force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first) @@ -784,7 +706,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_deleting_updates_counter_cache - topic = Topic.first(:order => "id ASC") + topic = Topic.order("id ASC").first assert_equal topic.replies.to_a.size, topic.replies_count topic.replies.delete(topic.replies.first) @@ -802,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) @@ -811,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) @@ -876,12 +798,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase client_id = firm.clients_of_firm.first.id assert_equal 1, firm.clients_of_firm.size - cleared = firm.clients_of_firm.clear + firm.clients_of_firm.clear assert_equal 0, firm.clients_of_firm.size assert_equal 0, firm.clients_of_firm(true).size assert_equal [], Client.destroyed_client_ids[firm.id] - assert_equal firm.clients_of_firm.object_id, cleared.object_id # Should not be destroyed since the association is not dependent. assert_nothing_raised do @@ -947,11 +868,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Client.create(:client_of => firm.id, :name => "BigShot Inc.") Client.create(:client_of => firm.id, :name => "SmallTime Inc.") # only one of two clients is included in the association due to the :conditions key - assert_equal 2, Client.find_all_by_client_of(firm.id).size + assert_equal 2, Client.where(client_of: firm.id).size assert_equal 1, firm.dependent_conditional_clients_of_firm.size firm.destroy # only the correctly associated client should have been deleted - assert_equal 1, Client.find_all_by_client_of(firm.id).size + assert_equal 1, Client.where(client_of: firm.id).size end def test_dependent_association_respects_optional_sanitized_conditions_on_delete @@ -959,11 +880,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Client.create(:client_of => firm.id, :name => "BigShot Inc.") Client.create(:client_of => firm.id, :name => "SmallTime Inc.") # only one of two clients is included in the association due to the :conditions key - assert_equal 2, Client.find_all_by_client_of(firm.id).size + assert_equal 2, Client.where(client_of: firm.id).size assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size firm.destroy # only the correctly associated client should have been deleted - assert_equal 1, Client.find_all_by_client_of(firm.id).size + assert_equal 1, Client.where(client_of: firm.id).size end def test_dependent_association_respects_optional_hash_conditions_on_delete @@ -971,22 +892,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Client.create(:client_of => firm.id, :name => "BigShot Inc.") Client.create(:client_of => firm.id, :name => "SmallTime Inc.") # only one of two clients is included in the association due to the :conditions key - assert_equal 2, Client.find_all_by_client_of(firm.id).size + assert_equal 2, Client.where(client_of: firm.id).size assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size firm.destroy # only the correctly associated client should have been deleted - assert_equal 1, Client.find_all_by_client_of(firm.id).size + assert_equal 1, Client.where(client_of: firm.id).size end def test_delete_all_association_with_primary_key_deletes_correct_records - firm = Firm.find(:first) + 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 - firm = Firm.find(:first) + firm = Firm.first firm.destroy assert_nil Client.find_by_id(old_record.id) end @@ -1021,10 +942,24 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, summit.client_of end - def test_deleting_type_mismatch + def test_deleting_by_fixnum_id david = Developer.find(1) - david.projects.reload - assert_raise(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) } + + assert_difference 'david.projects.count', -1 do + assert_equal 1, david.projects.delete(1).size + end + + assert_equal 1, david.projects.size + end + + def test_deleting_by_string_id + david = Developer.find(1) + + assert_difference 'david.projects.count', -1 do + assert_equal 1, david.projects.delete('1').size + end + + assert_equal 1, david.projects.size end def test_deleting_self_type_mismatch @@ -1094,7 +1029,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) assert_equal 2, firm.clients.size firm.destroy - assert Client.find(:all, :conditions => "firm_id=#{firm.id}").empty? + assert Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.empty? end def test_dependence_for_associations_with_hash_condition @@ -1104,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.find(:first, :order => "id") + firm = Firm.all.merge!(:order => "id").first assert_equal 2, firm.clients.size client = firm.clients.first @@ -1132,7 +1067,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.destroy rescue "do nothing" - assert_equal 2, Client.find(:all, :conditions => "firm_id=#{firm.id}").size + assert_equal 2, Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.size end def test_dependence_on_account @@ -1156,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') @@ -1166,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 + + def test_restrict_with_exception + firm = RestrictedWithExceptionFirm.create!(:name => 'restrict') + firm.companies.create(:name => 'child') - firm = RestrictedFirm.create!(:name => 'restrict') + 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? @@ -1184,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 @@ -1195,16 +1135,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_adding_array_and_collection - assert_nothing_raised { Firm.find(:first).clients + Firm.find(:all).last.clients } - end - - def test_find_all_without_conditions - firm = companies(:first_firm) - assert_equal 2, firm.clients.find(:all).length + assert_nothing_raised { Firm.first.clients + Firm.all.last.clients } end def test_replace_with_less - firm = Firm.find(:first, :order => "id") + firm = Firm.all.merge!(:order => "id").first firm.clients = [companies(:first_client)] assert firm.save, "Could not save firm" firm.reload @@ -1218,7 +1153,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_replace_with_new - firm = Firm.find(:first, :order => "id") + firm = Firm.all.merge!(:order => "id").first firm.clients = [companies(:second_client), Client.new("name" => "New Client")] firm.save firm.reload @@ -1318,29 +1253,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_dynamic_find_should_respect_association_order_for_through - assert_equal Comment.find(10), authors(:david).comments_desc.find(:first, :conditions => "comments.type = 'SpecialComment'") + 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 - 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.find(:all, :conditions => "comments.type = 'SpecialComment'") - 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.find(:all, :conditions => "comments.type = 'SpecialComment'").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.find(:all, :conditions => "comments.type = 'SpecialComment'", :limit => 9_000).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.find(:all, :include => :people).length - end - def test_has_many_through_respects_hash_conditions assert_equal authors(:david).hello_posts, authors(:david).hello_posts_with_hash_conditions assert_equal authors(:david).hello_post_comments, authors(:david).hello_post_comments_with_hash_conditions @@ -1348,7 +1264,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_include_uses_array_include_after_loaded firm = companies(:first_firm) - firm.clients.class # force load target + firm.clients.load_target client = firm.clients.first @@ -1398,7 +1314,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_first_or_last_on_loaded_association_should_not_fetch_with_query firm = companies(:first_firm) - firm.clients.class # force load target + firm.clients.load_target assert firm.clients.loaded? assert_no_queries do @@ -1452,13 +1368,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, author.essays.size end - assert_equal author.essays, Essay.find_all_by_writer_id("David") - + assert_equal author.essays, Essay.where(writer_id: "David") end def test_has_many_custom_primary_key david = authors(:david) - assert_equal david.essays, Essay.find_all_by_writer_id("David") + assert_equal david.essays, Essay.where(writer_id: "David") end def test_blank_custom_primary_key_on_new_record_should_not_run_queries @@ -1470,17 +1385,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - def test_calling_first_or_last_with_find_options_on_loaded_association_should_fetch_with_query - firm = companies(:first_firm) - firm.clients.class # force load target - - assert_queries 2 do - assert firm.clients.loaded? - firm.clients.first(:order => 'name') - firm.clients.last(:order => 'name') - end - end - def test_calling_first_or_last_with_integer_on_association_should_load_association firm = companies(:first_firm) @@ -1538,20 +1442,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = Namespaced::Firm.create({ :name => 'Some Company' }) firm.clients.create({ :name => 'Some Client' }) - stats = Namespaced::Firm.find(firm.id, { + stats = Namespaced::Firm.all.merge!( :select => "#{Namespaced::Firm.table_name}.id, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients", :joins => :clients, :group => "#{Namespaced::Firm.table_name}.id" - }) + ).find firm.id assert_equal 1, stats.num_clients.to_i - ensure ActiveRecord::Base.store_full_sti_class = old end def test_association_proxy_transaction_method_starts_transaction_in_association_class Comment.expects(:transaction) - Post.find(:first).comments.transaction do + Post.first.comments.transaction do # nothing end end @@ -1568,14 +1471,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_creating_using_primary_key - firm = Firm.find(:first, :order => "id") + 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 @@ -1584,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 @@ -1692,15 +1595,56 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [bulb2], car.reload.bulbs end - def test_building_has_many_association_with_restrict_dependency - option_before = ActiveRecord::Base.dependent_restrict_raises - ActiveRecord::Base.dependent_restrict_raises = true + def test_replace_returns_target + car = Car.create(:name => 'honda') + bulb1 = car.bulbs.create + bulb2 = car.bulbs.create + bulb3 = Bulb.create + + assert_equal [bulb1, bulb2], car.bulbs + result = car.bulbs.replace([bulb3, bulb1]) + assert_equal [bulb1, bulb3], car.bulbs + assert_equal [bulb1, bulb3], result + end + + def test_collection_association_with_private_kernel_method + firm = companies(:first_firm) + assert_equal [accounts(:signals37)], firm.accounts.open + end + + test "first_or_initialize adds the record to the association" do + firm = Firm.create! name: 'omg' + client = firm.clients_of_firm.first_or_initialize + assert_equal [client], firm.clients_of_firm + end + + test "first_or_create adds the record to the association" do + firm = Firm.create! name: 'omg' + firm.clients_of_firm.load_target + client = firm.clients_of_firm.first_or_create name: 'lol' + assert_equal [client], firm.clients_of_firm + assert_equal [client], firm.reload.clients_of_firm + end + + test "delete_all, when not loaded, doesn't load the records" do + post = posts(:welcome) + assert post.taggings_with_delete_all.count > 0 + assert !post.taggings_with_delete_all.loaded? + + # 2 queries: one DELETE and another to update the counter cache + assert_queries(2) do + 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 - assert_deprecated { klass.has_many :companies, :dependent => :restrict } - assert_not_deprecated { klass.has_many :companies } - ensure - ActiveRecord::Base.dependent_restrict_raises = option_before + 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 9cc09194dc..36e5ba9660 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -44,17 +44,33 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_associate_existing - posts(:thinking); people(:david) # Warm cache + post = posts(:thinking) + person = people(:david) assert_queries(1) do - posts(:thinking).people << people(:david) + post.people << person end assert_queries(1) do - assert posts(:thinking).people.include?(people(:david)) + assert post.people.include?(person) end - assert posts(:thinking).reload.people(true).include?(people(:david)) + assert post.reload.people(true).include?(person) + end + + def test_associate_existing_with_strict_mass_assignment_sanitizer + SecureReader.mass_assignment_sanitizer = :strict + + SecureReader.new + + post = posts(:thinking) + person = people(:david) + + assert_queries(1) do + post.secure_people << person + end + ensure + SecureReader.mass_assignment_sanitizer = :logger end def test_associate_existing_record_twice_should_add_to_target_twice @@ -311,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) @@ -321,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 @@ -517,7 +533,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_count_with_include_should_alias_join_table - assert_equal 2, people(:michael).posts.count(:include => :readers) + assert_equal 2, people(:michael).posts.includes(:readers).count end def test_inner_join_with_quoted_table_name @@ -552,7 +568,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_association_proxy_transaction_method_starts_transaction_in_association_class Tag.expects(:transaction) - Post.find(:first).tags.transaction do + Post.first.tags.transaction do # nothing end end @@ -635,7 +651,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_collection_singular_ids_setter company = companies(:rails_core) - dev = Developer.find(:first) + dev = Developer.first company.developer_ids = [dev.id] assert_equal [dev], company.developers @@ -655,7 +671,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set company = companies(:rails_core) - ids = [Developer.find(:first).id, -9999] + ids = [Developer.first.id, -9999] assert_raises(ActiveRecord::RecordNotFound) {company.developer_ids= ids} end @@ -688,6 +704,17 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert author.comments.include?(comment) end + def test_through_association_readonly_should_be_false + assert !people(:michael).posts.first.readonly? + assert !people(:michael).posts.to_a.first.readonly? + end + + def test_can_update_through_association + assert_nothing_raised do + people(:michael).posts.first.update_attributes!(:title => "Can write") + end + end + def test_has_many_through_polymorphic_with_primary_key_option assert_equal [categories(:general)], authors(:david).essay_categories @@ -715,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 @@ -786,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 246877bbed..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.find(:all, :include => :account) + 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.find(1, :include => :account_with_select).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 @@ -294,13 +276,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_dependence_with_missing_association_and_nullify Account.destroy_all - firm = DependentFirm.find(:first) + firm = DependentFirm.first assert_nil firm.account firm.destroy end def test_finding_with_interpolated_condition - firm = Firm.find(:first) + firm = Firm.first superior = firm.clients.create(:name => 'SuperiorCo') superior.rating = 10 superior.save @@ -346,14 +328,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do Firm.find(@firm.id).save! - Firm.find(@firm.id, :include => :account).save! + Firm.all.merge!(:includes => :account).find(@firm.id).save! end @firm.account.destroy assert_nothing_raised do Firm.find(@firm.id).save! - Firm.find(@firm.id, :include => :account).save! + Firm.all.merge!(:includes => :account).find(@firm.id).save! end end @@ -448,6 +430,22 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal car.id, bulb.car_id end + def test_association_protect_foreign_key + pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?") + + ship = pirate.build_ship + assert_equal pirate.id, ship.pirate_id + + ship = pirate.build_ship :pirate_id => pirate.id + 1 + assert_equal pirate.id, ship.pirate_id + + ship = pirate.create_ship + assert_equal pirate.id, ship.pirate_id + + ship = pirate.create_ship :pirate_id => pirate.id + 1 + assert_equal pirate.id, ship.pirate_id + end + def test_association_conditions_bypass_attribute_protection car = Car.create(:name => 'honda') @@ -508,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 2503349c08..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.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"]) + 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.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"]) + 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.find(@member.id, :include => :favourite_club).favourite_club - memberships(:membership_of_favourite_club).update_column(:favourite, false) - assert_equal nil, Member.find(@member.id, :include => :favourite_club).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.find(@member.id, :include => :hairy_club).hairy_club - clubs(:moustache_club).update_column(:name, "Association of Clean-Shaven Persons") - assert_equal nil, Member.find(@member.id, :include => :hairy_club).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.find(:all, :include => :sponsored_member, :conditions => ["name = ?","Moustache and Eyebrow Fancier Club"]) + 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.find(:all, :include => :club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #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.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #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.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC') #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.find(:all, :include => :member_type) + 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.find(@club.id, :include => :sponsored_member).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.find(@club.id, :include => :sponsored_member).save! + Club.all.merge!(:includes => :sponsored_member).find(@club.id).save! end end diff --git a/activerecord/test/cases/associations/identity_map_test.rb b/activerecord/test/cases/associations/identity_map_test.rb deleted file mode 100644 index 9b8635774c..0000000000 --- a/activerecord/test/cases/associations/identity_map_test.rb +++ /dev/null @@ -1,137 +0,0 @@ -require "cases/helper" -require 'models/author' -require 'models/post' - -if ActiveRecord::IdentityMap.enabled? -class InverseHasManyIdentityMapTest < ActiveRecord::TestCase - fixtures :authors, :posts - - def test_parent_instance_should_be_shared_with_every_child_on_find - m = Author.first - is = m.posts - is.each do |i| - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to child-owned instance" - end - end - - def test_parent_instance_should_be_shared_with_eager_loaded_children - m = Author.find(:first, :include => :posts) - is = m.posts - is.each do |i| - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to child-owned instance" - end - - m = Author.find(:first, :include => :posts, :order => 'posts.id') - is = m.posts - is.each do |i| - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to child-owned instance" - end - end - - def test_parent_instance_should_be_shared_with_newly_built_child - m = Author.first - i = m.posts.build(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to just-built-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_newly_block_style_built_child - m = Author.first - i = m.posts.build {|ii| ii.title = 'Industrial Revolution Re-enactment'; ii.body = 'Lorem ipsum'} - assert_not_nil i.title, "Child attributes supplied to build via blocks should be populated" - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to just-built-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_newly_created_child - m = Author.first - i = m.posts.create(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to newly-created-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child - m = Author.first - i = m.posts.create!(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to newly-created-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_newly_block_style_created_child - m = Author.first - i = m.posts.create {|ii| ii.title = 'Industrial Revolution Re-enactment'; ii.body = 'Lorem ipsum'} - assert_not_nil i.title, "Child attributes supplied to create via blocks should be populated" - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to newly-created-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_poked_in_child - m = Author.first - i = Post.create(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - m.posts << i - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to newly-created-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_replaced_via_accessor_children - m = Author.first - i = Post.new(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - m.posts = [i] - assert_same m, i.author - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to replaced-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_replaced_via_method_children - m = Author.first - i = Post.new(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - m.posts = [i] - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to replaced-child-owned instance" - end -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 68a1e62328..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 } - assert_equal real_count, Author.count(:joins => :posts), "plain inner join count should match the number of referenced posts records" + 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 } - assert_equal real_count, Author.calculate(:count, 'authors.id', :joins => :posts), "plain inner join count should match the number of referenced posts records" + 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.calculate(:count, 'authors.id', :joins => :posts, :distinct => true, :conditions => "posts.title like 'Welcome%'") + 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 76282213d8..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.find(:first, :conditions => {:name => 'Gordon'}, :include => :face) + 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.find(:first, :conditions => {:name => 'Gordon'}, :include => :face, :order => 'faces.id') + 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' @@ -114,7 +114,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_built_child - m = Man.find(:first) + m = Man.first f = m.build_face(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" @@ -125,7 +125,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_created_child - m = Man.find(:first) + m = Man.first f = m.create_face(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" @@ -136,7 +136,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method - m = Man.find(:first) + m = Man.first f = m.create_face!(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" @@ -147,7 +147,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_replaced_via_accessor_child - m = Man.find(:first) + m = Man.first f = Face.new(:description => 'haunted') m.face = f assert_not_nil f.man @@ -159,7 +159,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).dirty_face } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.dirty_face } end end @@ -179,7 +179,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_eager_loaded_children - m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests) + 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.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests, :order => 'interests.id') + 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" @@ -201,7 +201,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_block_style_built_child - m = Man.find(:first) + m = Man.first i = m.interests.build {|ii| ii.topic = 'Industrial Revolution Re-enactment'} assert_not_nil i.topic, "Child attributes supplied to build via blocks should be populated" assert_not_nil i.man @@ -213,7 +213,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child - m = Man.find(:first) + m = Man.first i = m.interests.create!(:topic => 'Industrial Revolution Re-enactment') assert_not_nil i.man assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -224,7 +224,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_block_style_created_child - m = Man.find(:first) + m = Man.first i = m.interests.create {|ii| ii.topic = 'Industrial Revolution Re-enactment'} assert_not_nil i.topic, "Child attributes supplied to create via blocks should be populated" assert_not_nil i.man @@ -248,7 +248,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_replaced_via_accessor_children - m = Man.find(:first) + m = Man.first i = Interest.new(:topic => 'Industrial Revolution Re-enactment') m.interests = [i] assert_not_nil i.man @@ -259,8 +259,14 @@ 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.find(:first).secret_interests } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests } end 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.find(:first, :include => :man, :conditions => {:description => 'trusting'}) + 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.find(:first, :include => :man, :order => 'men.id', :conditions => {:description => 'trusting'}) + 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' @@ -331,7 +337,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase end def test_child_instance_should_be_shared_with_replaced_via_accessor_parent - f = Face.find(:first) + f = Face.first m = Man.new(:name => 'Charles') f.man = m assert_not_nil m.face @@ -343,7 +349,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_man } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_man } end end @@ -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.find(:first, :conditions => {:description => 'confused'}) + 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.find(:first, :conditions => {:description => 'confused'}, :include => :man) + 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.find(:first, :conditions => {:description => 'confused'}, :include => :man, :order => 'men.id') + 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' @@ -421,19 +427,19 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error # Ideally this would, if only for symmetry's sake with other association types - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man } + assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man } end def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error # fails because no class has the correct inverse_of for horrible_polymorphic_man - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man = Man.first } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man = Man.first } end def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error # passes because Man does have the correct inverse_of - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Man.first } + assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Man.first } # fails because Interest does have the correct inverse_of - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Interest.first } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end end @@ -444,7 +450,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do - i = Interest.find(:first) + i = Interest.first i.zine i.man end @@ -452,7 +458,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do - i = Interest.find(:first) + i = Interest.first i.build_zine(:title => 'Get Some in Winter! 2008') i.build_man(:name => 'Gordon') i.save! diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 301755249c..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' @@ -46,16 +45,12 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert !authors(:mary).unique_categorized_posts.loaded? assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count } assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title) } - assert_queries(1) { assert_equal 0, author.unique_categorized_posts.count(:title, :conditions => "title is NULL") } + assert_queries(1) { assert_equal 0, author.unique_categorized_posts.where(title: nil).count(:title) } assert !authors(:mary).unique_categorized_posts.loaded? end def test_has_many_uniq_through_find - assert_equal 1, authors(:mary).unique_categorized_posts.find(:all).size - end - - def test_has_many_uniq_through_dynamic_find - assert_equal 1, authors(:mary).unique_categorized_posts.find_all_by_title("So I was thinking").size + assert_equal 1, authors(:mary).unique_categorized_posts.to_a.size end def test_polymorphic_has_many_going_through_join_model @@ -71,7 +66,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_polymorphic_has_many_going_through_join_model_with_find - assert_equal tags(:general), tag = posts(:welcome).tags.find(:first) + assert_equal tags(:general), tag = posts(:welcome).tags.first assert_no_queries do tag.tagging end @@ -85,7 +80,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection_with_find - assert_equal tags(:general), tag = posts(:welcome).funky_tags.find(:first) + assert_equal tags(:general), tag = posts(:welcome).funky_tags.first assert_no_queries do tag.tagging end @@ -179,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 @@ -190,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 @@ -201,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 @@ -212,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 @@ -223,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 @@ -237,8 +232,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_has_many_through - posts = Post.find(:all, :order => 'posts.id') - posts_with_authors = Post.find(:all, :include => :authors, :order => 'posts.id') + 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 } @@ -246,7 +241,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_one - post = Post.find_by_id(posts(:welcome).id, :include => :tagging) + post = Post.includes(:tagging).find posts(:welcome).id tagging = taggings(:welcome_general) assert_no_queries do assert_equal tagging, post.tagging @@ -254,7 +249,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_one_defined_in_abstract_parent - item = Item.find_by_id(items(:dvd).id, :include => :tagging) + item = Item.includes(:tagging).find items(:dvd).id tagging = taggings(:godfather) assert_no_queries do assert_equal tagging, item.tagging @@ -262,8 +257,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_many_through - posts = Post.find(:all, :order => 'posts.id') - posts_with_tags = Post.find(:all, :include => :tags, :order => 'posts.id') + 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 } @@ -271,8 +266,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_many - posts = Post.find(:all, :order => 'posts.id') - posts_with_taggings = Post.find(:all, :include => :taggings, :order => 'posts.id') + 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 } @@ -280,25 +275,20 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_find_all - assert_equal [categories(:general)], authors(:david).categories.find(:all) + assert_equal [categories(:general)], authors(:david).categories.to_a end def test_has_many_find_first - assert_equal categories(:general), authors(:david).categories.find(:first) + assert_equal categories(:general), authors(:david).categories.first end def test_has_many_with_hash_conditions - assert_equal categories(:general), authors(:david).categories_like_general.find(:first) + assert_equal categories(:general), authors(:david).categories_like_general.first end def test_has_many_find_conditions - assert_equal categories(:general), authors(:david).categories.find(:first, :conditions => "categories.name = 'General'") - assert_nil authors(:david).categories.find(:first, :conditions => "categories.name = 'Technology'") - end - - def test_has_many_class_methods_called_by_method_missing - assert_equal categories(:general), authors(:david).categories.find_all_by_name('General').first - assert_nil authors(:david).categories.find_by_name('Technology') + 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 @@ -327,14 +317,6 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_equal [authors(:david), authors(:bob)], posts(:thinking).authors_using_custom_pk.order('authors.id') end - def test_both_scoped_and_explicit_joins_should_be_respected - assert_nothing_raised do - Post.send(:with_scope, :find => {:joins => "left outer join comments on comments.id = posts.id"}) do - Post.find :all, :select => "comments.id, authors.id", :joins => "left outer join authors on authors.id = posts.author_id" - end - end - end - def test_belongs_to_polymorphic_with_counter_cache assert_equal 1, posts(:welcome)[:taggings_count] tagging = posts(:welcome).taggings.create(:tag => tags(:general)) @@ -362,7 +344,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end assert_raise ActiveRecord::EagerLoadPolymorphicError do - tags(:general).taggings.find(:all, :include => :taggable, :references => :bogus_table, :conditions => 'bogus_table.column = 1') + tags(:general).taggings.includes(:taggable).where('bogus_table.column = 1').references(:bogus_table).to_a end end @@ -372,7 +354,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_has_many_polymorphic_with_source_type - tag_with_include = Tag.find(tags(:general).id, :include => :tagged_posts) + 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 @@ -382,20 +364,20 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_has_many_find_all - assert_equal comments(:greetings), authors(:david).comments.find(:all, :order => 'comments.id').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.find(:all, :order => 'comments.id').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.find(:first, :order => 'comments.id') + assert_equal comments(:greetings), authors(:david).comments.order('comments.id').first end def test_has_many_through_has_many_find_conditions - options = { :conditions => "comments.#{QUOTED_TYPE}='SpecialComment'", :order => 'comments.id' } - assert_equal comments(:does_it_hurt), authors(:david).comments.find(:first, options) + options = { :where => "comments.#{QUOTED_TYPE}='SpecialComment'", :order => 'comments.id' } + assert_equal comments(:does_it_hurt), authors(:david).comments.merge(options).first end def test_has_many_through_has_many_find_by_id @@ -403,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 @@ -411,7 +393,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_has_many_through_polymorphic_has_many - author = Author.find_by_id(authors(:david).id, :include => :taggings) + author = Author.includes(:taggings).find authors(:david).id expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } @@ -419,7 +401,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_load_has_many_through_has_many - author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id' + 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) @@ -427,7 +409,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_load_has_many_through_has_many_with_conditions - post = Post.find(:first, :include => :invalid_tags) + post = Post.all.merge!(:includes => :invalid_tags).first assert_no_queries do post.invalid_tags end @@ -435,8 +417,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_eager_belongs_to_and_has_one_not_singularized assert_nothing_raised do - Author.find(:first, :include => :author_address) - AuthorAddress.find(:first, :include => :author) + Author.all.merge!(:includes => :author_address).first + AuthorAddress.all.merge!(:includes => :author).first end end @@ -452,7 +434,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_uses_conditions_specified_on_the_has_many_association - author = Author.find(:first) + author = Author.first assert_present author.comments assert_blank author.nonexistant_comments end @@ -471,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.") @@ -484,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? @@ -595,7 +577,27 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_deleting_junk_from_has_many_through_should_raise_type_mismatch - assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete("Uhh what now?") } + assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:thinking).tags.delete(Object.new) } + end + + def test_deleting_by_fixnum_id_from_has_many_through + post = posts(:thinking) + + assert_difference 'post.tags.count', -1 do + assert_equal 1, post.tags.delete(1).size + end + + assert_equal 0, post.tags.size + end + + def test_deleting_by_string_id_from_has_many_through + post = posts(:thinking) + + assert_difference 'post.tags.count', -1 do + assert_equal 1, post.tags.delete('1').size + end + + assert_equal 0, post.tags.size end def test_has_many_through_sum_uses_calculations @@ -622,7 +624,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_many expected = taggings(:welcome_general) - p = Post.find(posts(:welcome).id, :include => :taggings) + 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 @@ -630,18 +632,18 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_one expected = posts(:welcome) - tagging = Tagging.find(taggings(:welcome_general).id, :include => :taggable) + 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.find(posts(:welcome).id, :include => {:taggings => :taggable}) + 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.find(:all, :order => 'posts.id') - posts_with_tags = Post.find(:all, :include => :tags, :order => 'posts.id') + 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 } @@ -649,7 +651,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_preload_polymorph_many_types - taggings = Tagging.find :all, :include => :taggable, :conditions => ['taggable_type != ?', 'FakeModel'] + taggings = Tagging.all.merge!(:includes => :taggable, :where => ['taggable_type != ?', 'FakeModel']).to_a assert_no_queries do taggings.first.taggable.id taggings[1].taggable.id @@ -662,13 +664,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_preload_nil_polymorphic_belongs_to assert_nothing_raised do - Tagging.find(:all, :include => :taggable, :conditions => ['taggable_type IS NULL']) + Tagging.all.merge!(:includes => :taggable, :where => ['taggable_type IS NULL']).to_a end end def test_preload_polymorphic_has_many - posts = Post.find(:all, :order => 'posts.id') - posts_with_taggings = Post.find(:all, :include => :taggings, :order => 'posts.id') + 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 } @@ -676,7 +678,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_belongs_to_shared_parent - comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 1') + 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 @@ -684,7 +686,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_include_uses_array_include_after_loaded david = authors(:david) - david.categories.class # force load target + david.categories.load_target category = david.categories.first @@ -731,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 017905e0ac..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.find(:first, :order => 'id', :include => :firm)} - assert_nothing_raised {Account.find(:first, :order => :id, :include => :firm)} + 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 @@ -214,6 +214,17 @@ class AssociationProxyTest < ActiveRecord::TestCase david = developers(:david) assert_equal david.association(:projects), david.projects.proxy_association end + + def test_scoped_allows_conditions + 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 class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 764305459d..da5d9d8c2a 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -1,15 +1,10 @@ require "cases/helper" -require 'active_support/core_ext/object/inclusion' require 'thread' module ActiveRecord module AttributeMethods class ReadTest < ActiveRecord::TestCase class FakeColumn < Struct.new(:name) - def type_cast_code(var) - var - end - def type; :integer; end end @@ -19,6 +14,7 @@ module ActiveRecord def self.active_record_super; Base; end def self.base_class; self; end + extend ActiveRecord::Configuration include ActiveRecord::AttributeMethods def self.define_attribute_methods @@ -51,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 d6de668a17..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' @@ -30,9 +29,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase t = Topic.new t.title = "hello there!" t.written_on = Time.now + t.author_name = "" assert t.attribute_present?("title") assert t.attribute_present?("written_on") assert !t.attribute_present?("content") + assert !t.attribute_present?("author_name") end def test_attribute_present_with_booleans @@ -121,6 +122,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert keyboard.respond_to?('id') end + def test_id_before_type_cast_with_custom_primary_key + keyboard = Keyboard.create + keyboard.key_number = '10' + assert_equal '10', keyboard.id_before_type_cast + assert_equal nil, keyboard.read_attribute_before_type_cast('id') + assert_equal '10', keyboard.read_attribute_before_type_cast('key_number') + end + # Syck calls respond_to? before actually calling initialize def test_respond_to_with_allocated_object topic = Topic.allocate @@ -233,7 +242,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase # DB2 is not case-sensitive return true if current_adapter?(:DB2Adapter) - assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.find(:first).attributes + assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes end def test_hashes_not_mangled @@ -386,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 @@ -470,23 +479,23 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_typecast_attribute_from_select_to_false - topic = Topic.create(:title => 'Budget') + Topic.create(:title => 'Budget') # Oracle does not support boolean expressions in SELECT if current_adapter?(:OracleAdapter) - topic = Topic.find(:first, :select => "topics.*, 0 as is_test") + topic = Topic.all.merge!(:select => "topics.*, 0 as is_test").first else - topic = Topic.find(:first, :select => "topics.*, 1=2 as is_test") + topic = Topic.all.merge!(:select => "topics.*, 1=2 as is_test").first end assert !topic.is_test? end def test_typecast_attribute_from_select_to_true - topic = Topic.create(:title => 'Budget') + Topic.create(:title => 'Budget') # Oracle does not support boolean expressions in SELECT if current_adapter?(:OracleAdapter) - topic = Topic.find(:first, :select => "topics.*, 1 as is_test") + topic = Topic.all.merge!(:select => "topics.*, 1 as is_test").first else - topic = Topic.find(:first, :select => "topics.*, 2=2 as is_test") + topic = Topic.all.merge!(:select => "topics.*, 2=2 as is_test").first end assert topic.is_test? end @@ -533,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 @@ -617,6 +626,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_time_zone_aware_attribute_saved + in_time_zone 1 do + record = @target.create(:written_on => '2012-02-20 10:00') + + record.written_on = '2012-02-20 09:00' + record.save + assert_equal Time.zone.local(2012, 02, 20, 9), record.reload.written_on + end + end + def test_setting_time_zone_aware_attribute_to_blank_string_returns_nil in_time_zone "Pacific Time (US & Canada)" do record = @target.new @@ -710,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) @@ -771,12 +785,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase end private + def cached_columns - @cached_columns ||= time_related_columns_on_topic.map(&:name) + Topic.columns.find_all { |column| + !Topic.serialized_attributes.include? column.name + }.map(&:name) end def time_related_columns_on_topic - Topic.columns.select { |c| c.type.in?([:time, :date, :datetime, :timestamp]) } + Topic.columns.select { |c| [:time, :date, :datetime, :timestamp].include?(c.type) } end def in_time_zone(zone) @@ -792,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 1376810adf..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 @@ -87,7 +71,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end def test_save_fails_for_invalid_has_one - firm = Firm.find(:first) + firm = Firm.first assert firm.valid? firm.build_account @@ -99,7 +83,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end def test_save_succeeds_for_invalid_has_one_with_validate_false - firm = Firm.find(:first) + firm = Firm.first assert firm.valid? firm.build_unvalidated_account @@ -155,20 +139,20 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end def test_not_resaved_when_unchanged - firm = Firm.find(:first, :include => :account) + firm = Firm.all.merge!(:includes => :account).first firm.name += '-changed' assert_queries(1) { firm.save! } - firm = Firm.find(:first) - firm.account = Account.find(:first) + firm = Firm.first + firm.account = Account.first assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! } - firm = Firm.find(:first).dup - firm.account = Account.find(:first) + firm = Firm.first.dup + firm.account = Account.first assert_queries(2) { firm.save! } - firm = Firm.find(:first).dup - firm.account = Account.find(:first).dup + firm = Firm.first.dup + firm.account = Account.first.dup assert_queries(2) { firm.save! } end @@ -228,7 +212,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test end def test_assignment_before_parent_saved - client = Client.find(:first) + client = Client.first apple = Firm.new("name" => "Apple") client.firm = apple assert_equal apple, client.firm @@ -342,7 +326,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test end def test_build_and_then_save_parent_should_not_reload_target - client = Client.find(:first) + client = Client.first apple = client.build_firm(:name => "Apple") client.save! assert_no_queries { assert_equal apple, client.firm } @@ -384,7 +368,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa end def test_invalid_adding_with_validate_false - firm = Firm.find(:first) + firm = Firm.first client = Client.new firm.unvalidated_clients_of_firm << client @@ -397,7 +381,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_valid_adding_with_validate_false no_of_clients = Client.count - firm = Firm.find(:first) + firm = Firm.first client = Client.new("name" => "Apple") assert firm.valid? @@ -627,7 +611,9 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_a_child_marked_for_destruction_should_not_be_destroyed_twice @pirate.ship.mark_for_destruction assert @pirate.save - @pirate.ship.expects(:destroy).never + class << @pirate.ship + def destroy; raise "Should not be called" end + end assert @pirate.save end @@ -672,7 +658,9 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_a_parent_marked_for_destruction_should_not_be_destroyed_twice @ship.pirate.mark_for_destruction assert @ship.save - @ship.pirate.expects(:destroy).never + class << @ship.pirate + def destroy; raise "Should not be called" end + end assert @ship.save end @@ -767,6 +755,16 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase assert_equal before, @pirate.reload.birds end + def test_when_new_record_a_child_marked_for_destruction_should_not_affect_other_records_from_saving + @pirate = @ship.build_pirate(:catchphrase => "Arr' now I shall keep me eye on you matey!") # new record + + 3.times { |i| @pirate.birds.build(:name => "birds_#{i}") } + @pirate.birds[1].mark_for_destruction + @pirate.save! + + assert_equal 2, @pirate.birds.reload.length + end + # Add and remove callbacks tests for association collections. %w{ method proc }.each do |callback_type| define_method("test_should_run_add_callback_#{callback_type}s_for_has_many") do @@ -858,7 +856,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") } before = @pirate.parrots.map { |c| c.mark_for_destruction ; c } - class << @pirate.parrots + class << @pirate.association(:parrots) def destroy(*args) super raise 'Oh noes!' @@ -978,10 +976,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase values = [@pirate.reload.catchphrase, @pirate.ship.name, *@pirate.ship.parts.map(&:name)] # Oracle saves empty string as NULL if current_adapter?(:OracleAdapter) - expected = ActiveRecord::IdentityMap.enabled? ? - [nil, nil, '', ''] : - [nil, nil, nil, nil] - assert_equal expected, values + assert_equal [nil, nil, nil, nil], values else assert_equal ['', '', '', ''], values end @@ -1077,8 +1072,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase @ship.save(:validate => false) # Oracle saves empty string as NULL if current_adapter?(:OracleAdapter) - expected = ActiveRecord::IdentityMap.enabled? ? [nil, ''] : [nil, nil] - assert_equal expected, [@ship.reload.name, @ship.pirate.catchphrase] + assert_equal [nil, nil], [@ship.reload.name, @ship.pirate.catchphrase] else assert_equal ['', ''], [@ship.reload.name, @ship.pirate.catchphrase] end @@ -1267,7 +1261,7 @@ module AutosaveAssociationOnACollectionAssociationTests def test_should_not_load_the_associated_models_if_they_were_not_loaded_yet assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! } - @pirate.send(@association_name).class # hack to load the target + @pirate.send(@association_name).load_target assert_queries(3) do @pirate.catchphrase = 'Yarr' diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index dfa05990f9..b9d480d9ce 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 @@ -62,7 +61,11 @@ end class Weird < ActiveRecord::Base; end -class Boolean < ActiveRecord::Base; end +class Boolean < ActiveRecord::Base + def has_fun + super + end +end class LintTest < ActiveRecord::TestCase include ActiveModel::Lint::Tests @@ -77,6 +80,12 @@ end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts + def setup + ActiveRecord::Base.time_zone_aware_attributes = false + ActiveRecord::Base.default_timezone = :local + Time.zone = nil + end + def test_generated_methods_modules modules = Computer.ancestors assert modules.include?(Computer::GeneratedFeatureMethods) @@ -121,49 +130,42 @@ class BasicsTest < ActiveRecord::TestCase unless current_adapter?(:PostgreSQLAdapter,:OracleAdapter,:SQLServerAdapter) def test_limit_with_comma - assert_nothing_raised do - Topic.limit("1,2").all - end + assert Topic.limit("1,2").to_a end end def test_limit_without_comma - assert_nothing_raised do - assert_equal 1, Topic.limit("1").all.length - end - - assert_nothing_raised do - assert_equal 1, Topic.limit(1).all.length - end + 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 def test_select_symbol topic_ids = Topic.select(:id).map(&:id).sort - assert_equal Topic.all.map(&:id).sort, topic_ids + assert_equal Topic.pluck(:id).sort, topic_ids end def test_table_exists @@ -188,7 +190,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_previously_changed - topic = Topic.find :first + topic = Topic.first topic.title = '<3<3<3' assert_equal({}, topic.previous_changes) @@ -198,7 +200,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_previously_changed_dup - topic = Topic.find :first + topic = Topic.first topic.title = '<3<3<3' topic.save! @@ -224,10 +226,11 @@ class BasicsTest < ActiveRecord::TestCase ) # For adapters which support microsecond resolution. - if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLiteAdapter) + if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLite3Adapter) 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 @@ -346,13 +349,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_load - topics = Topic.find(:all, :order => 'id') + 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.find(:all, :conditions => "author_name = 'Mary'") + topics = Topic.all.merge!(:where => "author_name = 'Mary'").to_a assert_equal(1, topics.size) assert_equal(topics(:second).title, topics.first.title) @@ -463,18 +466,18 @@ 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) def test_update_all_with_order_and_limit - assert_equal 1, Topic.update_all("content = 'bulk updated!'", nil, :limit => 1, :order => 'id DESC') + assert_equal 1, Topic.limit(1).order('id DESC').update_all(:content => 'bulk updated!') end end @@ -600,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') @@ -689,7 +698,7 @@ class BasicsTest < ActiveRecord::TestCase } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on end def test_multiparameter_attributes_on_time_with_no_date @@ -796,8 +805,6 @@ class BasicsTest < ActiveRecord::TestCase topic = Topic.find(1) topic.attributes = attributes assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on - ensure - ActiveRecord::Base.default_timezone = :local end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes @@ -813,14 +820,9 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time assert_equal Time.zone, topic.written_on.time_zone - ensure - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false - ActiveRecord::Base.time_zone_aware_attributes = false Time.zone = ActiveSupport::TimeZone[-28800] attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", @@ -830,8 +832,6 @@ class BasicsTest < ActiveRecord::TestCase topic.attributes = attributes assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on assert_equal false, topic.written_on.respond_to?(:time_zone) - ensure - Time.zone = nil end def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes @@ -848,9 +848,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on assert_equal false, topic.written_on.respond_to?(:time_zone) ensure - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil Topic.skip_time_zone_conversion_for_attributes = [] end @@ -868,10 +865,6 @@ class BasicsTest < ActiveRecord::TestCase topic.attributes = attributes assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time assert topic.bonus_time.utc? - ensure - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil end end @@ -885,6 +878,41 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on end + def test_multiparameter_attributes_setting_time_attribute + return skip "Oracle does not have TIME data type" if current_adapter? :OracleAdapter + + topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" ) + assert_equal 1, topic.bonus_time.hour + assert_equal 5, topic.bonus_time.min + end + + def test_multiparameter_attributes_setting_date_attribute + topic = Topic.new( "written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11" ) + assert_equal 1952, topic.written_on.year + assert_equal 3, topic.written_on.month + assert_equal 11, topic.written_on.day + end + + def test_multiparameter_attributes_setting_date_and_time_attribute + topic = Topic.new( + "written_on(1i)" => "1952", + "written_on(2i)" => "3", + "written_on(3i)" => "11", + "written_on(4i)" => "13", + "written_on(5i)" => "55") + assert_equal 1952, topic.written_on.year + assert_equal 3, topic.written_on.month + assert_equal 11, topic.written_on.day + assert_equal 13, topic.written_on.hour + assert_equal 55, topic.written_on.min + end + + def test_multiparameter_attributes_setting_time_but_not_date_on_date_field + assert_raise( ActiveRecord::MultiparameterAssignmentErrors ) do + Topic.new( "written_on(4i)" => "13", "written_on(5i)" => "55" ) + end + end + def test_multiparameter_assignment_of_aggregation customer = Customer.new address = Address.new("The Street", "The City", "The Country") @@ -926,6 +954,7 @@ class BasicsTest < ActiveRecord::TestCase attributes = { "address(1)" => "The Street", "address(2)" => address.city, "address(3000)" => address.country } customer.attributes = attributes end + assert_equal("address", ex.errors[0].attribute) end @@ -938,7 +967,19 @@ class BasicsTest < ActiveRecord::TestCase } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time + assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time + end + + def test_attributes_on_dummy_time_with_invalid_time + # Oracle, and Sybase do not have a TIME datatype. + return true if current_adapter?(:OracleAdapter, :SybaseAdapter) + + attributes = { + "bonus_time" => "not a time" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_nil topic.bonus_time end def test_boolean @@ -957,6 +998,16 @@ class BasicsTest < ActiveRecord::TestCase assert b_true.value? end + def test_boolean_without_questionmark + b_true = Boolean.create({ "value" => true }) + true_id = b_true.id + + subclass = Class.new(Boolean).find true_id + superclass = Boolean.find true_id + + assert_equal superclass.read_attribute(:has_fun), subclass.read_attribute(:has_fun) + end + def test_boolean_cast_from_string b_blank = Boolean.create({ "value" => "" }) blank_id = b_blank.id @@ -1139,7 +1190,7 @@ class BasicsTest < ActiveRecord::TestCase assert g.save # Reload and check that we have all the geometric attributes. - h = ActiveRecord::IdentityMap.without { Geometric.find(g.id) } + h = Geometric.find(g.id) assert_equal '(5,6.1)', h.a_point assert_equal '[(2,3),(5.5,7)]', h.a_line_segment @@ -1150,7 +1201,8 @@ class BasicsTest < ActiveRecord::TestCase # use a geometric function to test for an open path objs = Geometric.find_by_sql ["select isopen(a_path) from geometrics where id = ?", g.id] - assert_equal objs[0].isopen, 't' + + assert_equal true, objs[0].isopen # test alternate formats when defining the geometric types @@ -1167,7 +1219,7 @@ class BasicsTest < ActiveRecord::TestCase assert g.save # Reload and check that we have all the geometric attributes. - h = ActiveRecord::IdentityMap.without { Geometric.find(g.id) } + h = Geometric.find(g.id) assert_equal '(5,6.1)', h.a_point assert_equal '[(2,3),(5.5,7)]', h.a_line_segment @@ -1178,7 +1230,8 @@ class BasicsTest < ActiveRecord::TestCase # use a geometric function to test for an closed path objs = Geometric.find_by_sql ["select isclosed(a_path) from geometrics where id = ?", g.id] - assert_equal objs[0].isclosed, 't' + + assert_equal true, objs[0].isclosed end end @@ -1248,193 +1301,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_quoting_arrays - replies = Reply.find(:all, :conditions => [ "id IN (?)", topics(:first).replies.collect(&:id) ]) + replies = Reply.all.merge!(:where => [ "id IN (?)", topics(:first).replies.collect(&:id) ]).to_a assert_equal topics(:first).replies.size, replies.size - replies = Reply.find(:all, :conditions => [ "id IN (?)", [] ]) + 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_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) @@ -1477,6 +1350,42 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_clear_cash_when_setting_table_name + Joke.table_name = "cold_jokes" + before_columns = Joke.columns + before_seq = Joke.sequence_name + + Joke.table_name = "funny_jokes" + after_columns = Joke.columns + after_seq = Joke.sequence_name + + assert_not_equal before_columns, after_columns + assert_not_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil? + end + + def test_dont_clear_sequence_name_when_setting_explicitly + Joke.sequence_name = "black_jokes_seq" + Joke.table_name = "cold_jokes" + before_seq = Joke.sequence_name + + Joke.table_name = "funny_jokes" + after_seq = Joke.sequence_name + + assert_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil? + ensure + Joke.reset_sequence_name + end + + def test_dont_clear_inheritnce_column_when_setting_explicitly + Joke.inheritance_column = "my_type" + before_inherit = Joke.inheritance_column + + Joke.reset_column_information + after_inherit = Joke.inheritance_column + + assert_equal before_inherit, after_inherit unless before_inherit.blank? && after_inherit.blank? + end + def test_set_table_name_symbol_converted_to_string Joke.table_name = :cold_jokes assert_equal 'cold_jokes', Joke.table_name @@ -1514,22 +1423,19 @@ class BasicsTest < ActiveRecord::TestCase def test_count_with_join res = Post.count_by_sql "SELECT COUNT(*) FROM posts LEFT JOIN comments ON posts.id=comments.post_id WHERE posts.#{QUOTED_TYPE} = 'Post'" - res2 = Post.count(:conditions => "posts.#{QUOTED_TYPE} = 'Post'", :joins => "LEFT JOIN comments ON posts.id=comments.post_id") + res2 = Post.where("posts.#{QUOTED_TYPE} = 'Post'").joins("LEFT JOIN comments ON posts.id=comments.post_id").count assert_equal res, res2 res3 = nil assert_nothing_raised do - res3 = Post.count(:conditions => "posts.#{QUOTED_TYPE} = 'Post'", - :joins => "LEFT JOIN comments ON posts.id=comments.post_id") + res3 = Post.where("posts.#{QUOTED_TYPE} = 'Post'").joins("LEFT JOIN comments ON posts.id=comments.post_id").count end assert_equal res, res3 res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" res5 = nil assert_nothing_raised do - res5 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", - :joins => "p, comments co", - :select => "p.id") + res5 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count end assert_equal res4, res5 @@ -1537,145 +1443,64 @@ class BasicsTest < ActiveRecord::TestCase res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" res7 = nil assert_nothing_raised do - res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", - :joins => "p, comments co", - :select => "p.id", - :distinct => true) + res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count(distinct: true) end assert_equal res6, res7 end - def test_scoped_find_conditions - scoped_developers = Developer.send(:with_scope, :find => { :conditions => 'salary > 90000' }) do - Developer.find(:all, :conditions => 'id < 5') - end - assert !scoped_developers.include?(developers(:david)) # David's salary is less than 90,000 - assert_equal 3, scoped_developers.size - end - def test_no_limit_offset assert_nothing_raised do - Developer.find(:all, :offset => 2) - end - end - - def test_scoped_find_limit_offset - scoped_developers = Developer.send(:with_scope, :find => { :limit => 3, :offset => 2 }) do - Developer.find(:all, :order => 'id') - end - assert !scoped_developers.include?(developers(:david)) - assert !scoped_developers.include?(developers(:jamis)) - assert_equal 3, scoped_developers.size - - # Test without scoped find conditions to ensure we get the whole thing - developers = Developer.find(:all, :order => 'id') - assert_equal Developer.count, developers.size - end - - def test_scoped_find_order - # Test order in scope - scoped_developers = Developer.send(:with_scope, :find => { :limit => 1, :order => 'salary DESC' }) do - Developer.find(:all) - end - assert_equal 'Jamis', scoped_developers.first.name - assert scoped_developers.include?(developers(:jamis)) - # Test scope without order and order in find - scoped_developers = Developer.send(:with_scope, :find => { :limit => 1 }) do - Developer.find(:all, :order => 'salary DESC') - end - # Test scope order + find order, order has priority - scoped_developers = Developer.send(:with_scope, :find => { :limit => 3, :order => 'id DESC' }) do - Developer.find(:all, :order => 'salary ASC') - end - assert scoped_developers.include?(developers(:poor_jamis)) - assert ! scoped_developers.include?(developers(:david)) - assert ! scoped_developers.include?(developers(:jamis)) - assert_equal 3, scoped_developers.size - - # Test without scoped find conditions to ensure we get the right thing - assert ! scoped_developers.include?(Developer.find(1)) - assert scoped_developers.include?(Developer.find(11)) - end - - def test_scoped_find_limit_offset_including_has_many_association - topics = Topic.send(:with_scope, :find => {:limit => 1, :offset => 1, :include => :replies}) do - Topic.find(:all, :order => "topics.id") + Developer.all.merge!(:offset => 2).to_a end - assert_equal 1, topics.size - assert_equal 2, topics.first.id - end - - def test_scoped_find_order_including_has_many_association - developers = Developer.send(:with_scope, :find => { :order => 'developers.salary DESC', :include => :projects }) do - Developer.find(:all) - end - assert developers.size >= 2 - (1...developers.size).each do |i| - assert developers[i-1].salary >= developers[i].salary - end - end - - def test_scoped_find_with_group_and_having - developers = Developer.send(:with_scope, :find => { :group => 'developers.salary', :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" }) do - Developer.find(:all) - end - assert_equal 3, developers.size end def test_find_last - last = Developer.find :last - assert_equal last, Developer.find(:first, :order => 'id desc') + last = Developer.last + assert_equal last, Developer.all.merge!(:order => 'id desc').first end def test_last - assert_equal Developer.find(:first, :order => 'id desc'), 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_equal Developer.find(:all), developers + assert_kind_of ActiveRecord::Relation, developers + assert_equal Developer.all, developers end def test_all_with_conditions - assert_equal Developer.find(:all, :order => 'id desc'), 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.find :last, :order => 'developers.salary ASC' - assert_equal last, Developer.find(:all, :order => 'developers.salary ASC').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.find :last, :order => 'developers.salary DESC' - assert_equal last, Developer.find(:all, :order => 'developers.salary DESC').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.find :last, :order => 'developers.name, developers.salary DESC' - assert_equal last, Developer.find(:all, :order => 'developers.name, developers.salary DESC').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.find(:all, :order => 'developers.name, developers.salary') - assert_equal combined, Developer.find(:all, :order => ['developers.name', 'developers.salary']) + 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.find(:all, :group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at') - assert_equal combined, Developer.find(:all, :group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at']) + 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.find :last, :order => :salary - assert_equal last, Developer.find(:all, :order => :salary).last - end - - def test_find_scoped_ordered_last - last_developer = Developer.send(:with_scope, :find => { :order => 'developers.salary ASC' }) do - Developer.find(:last) - end - assert_equal last_developer, Developer.find(:all, :order => 'developers.salary ASC').last + last = Developer.all.merge!(:order => :salary).last + assert_equal last, Developer.all.merge!(:order => :salary).to_a.last end def test_abstract_class @@ -1688,18 +1513,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? @@ -1750,7 +1563,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_to_param_should_return_string - assert_kind_of String, Client.find(:first).to_param + 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 @@ -1769,8 +1588,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_inspect_limited_select_instance - assert_equal %(#<Topic id: 1>), Topic.find(:first, :select => 'id', :conditions => 'id = 1').inspect - assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.find(:first, :select => 'id, title', :conditions => 'id = 1').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 @@ -1883,7 +1702,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] @@ -1918,8 +1737,17 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 1, post.comments.length end + def test_marshalling_new_record_round_trip_with_associations + post = Post.new + post.comments.build + + post = Marshal.load(Marshal.dump(post)) + + assert post.new_record?, "should be a new record" + end + def test_attribute_names - assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id"], + assert_equal ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"], Company.attribute_names end @@ -1941,24 +1769,22 @@ class BasicsTest < ActiveRecord::TestCase est_key = Developer.first.cache_key assert_equal utc_key, est_key - ensure - ActiveRecord::Base.time_zone_aware_attributes = false end def test_cache_key_format_for_existing_record_with_updated_at dev = Developer.first - assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:number)}", dev.cache_key + assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key end def test_cache_key_format_for_existing_record_with_nil_updated_at dev = Developer.first - dev.update_attribute(:updated_at, nil) + dev.update_columns(updated_at: nil) assert_match(/\/#{dev.id}$/, dev.cache_key) end def test_uniq_delegates_to_scoped scope = stub - Bird.stubs(:scoped).returns(mock(:uniq => scope)) + Bird.stubs(:all).returns(mock(:uniq => scope)) assert_equal scope, Bird.uniq end @@ -1974,4 +1800,65 @@ class BasicsTest < ActiveRecord::TestCase def test_table_name_with_2_abstract_subclasses assert_equal "photos", Photo.table_name end + + def test_column_types_typecast + topic = Topic.first + refute_equal 't.lo', topic.author_name + + attrs = topic.attributes.dup + attrs.delete 'id' + + typecast = Class.new { + def type_cast value + "t.lo" + end + } + + types = { 'author_name' => typecast.new } + topic = Topic.allocate.init_with 'attributes' => attrs, + 'column_types' => types + + assert_equal 't.lo', topic.author_name + end + + def test_typecasting_aliases + assert_equal 10, Topic.select('10 as tenderlove').first.tenderlove + end + + def test_slice + company = Company.new(:rating => 1, :name => "37signals", :firm_name => "37signals") + hash = company.slice(:name, :rating, "arbitrary_method") + assert_equal hash[:name], company.name + assert_equal hash['name'], company.name + assert_equal hash[:rating], company.rating + assert_equal hash['arbitrary_method'], company.arbitrary_method + assert_equal hash[:arbitrary_method], company.arbitrary_method + assert_nil hash[:firm_name] + assert_nil hash['firm_name'] + end + + def test_default_values_are_deeply_dupped + company = Company.new + company.description << "foo" + assert_equal "", Company.new.description + end + + ["find_by", "find_by!"].each do |meth| + test "#{meth} delegates to scoped" do + record = stub + + scope = mock + scope.expects(meth).with(:foo, :bar).returns(record) + + klass = Class.new(ActiveRecord::Base) + klass.stubs(:all => scope) + + assert_equal record, klass.public_send(meth, :foo, :bar) + end + end + + test "scoped can take a values hash" do + klass = Class.new(ActiveRecord::Base) + assert_equal ['foo'], klass.all.merge!(select: 'foo').select_values + end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 660098b9ad..cdd4b49042 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -27,30 +27,18 @@ class EachTest < ActiveRecord::TestCase def test_each_should_raise_if_select_is_set_without_id assert_raise(RuntimeError) do - Post.find_each(:select => :title, :batch_size => 1) { |post| post } + Post.select(:title).find_each(:batch_size => 1) { |post| post } end end def test_each_should_execute_if_id_is_in_select assert_queries(6) do - Post.find_each(:select => "id, title, type", :batch_size => 2) do |post| + Post.select("id, title, type").find_each(:batch_size => 2) do |post| assert_kind_of Post, post end end end - def test_each_should_raise_if_the_order_is_set - assert_raise(RuntimeError) do - Post.find_each(:order => "title") { |post| post } - end - end - - def test_each_should_raise_if_the_limit_is_set - assert_raise(RuntimeError) do - Post.find_each(:limit => 1) { |post| post } - end - end - def test_warn_if_limit_scope_is_set ActiveRecord::Base.logger.expects(:warn) Post.limit(1).find_each { |post| post } diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 3652255c38..03aa9fdb27 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -23,6 +23,8 @@ module ActiveRecord @listener = LogListener.new @pk = Topic.columns.find { |c| c.primary } ActiveSupport::Notifications.subscribe('sql.active_record', @listener) + + skip_if_prepared_statement_caching_is_not_supported end def teardown @@ -30,9 +32,6 @@ module ActiveRecord end def test_binds_are_logged - # FIXME: use skip with minitest - return unless @connection.supports_statement_cache? - sub = @connection.substitute_at(@pk, 0) binds = [[@pk, 1]] sql = "select * from topics where id = #{sub}" @@ -44,9 +43,6 @@ module ActiveRecord end def test_find_one_uses_binds - # FIXME: use skip with minitest - return unless @connection.supports_statement_cache? - Topic.find(1) binds = [[@pk, 1]] message = @listener.calls.find { |args| args[4][:binds] == binds } @@ -54,9 +50,6 @@ module ActiveRecord end def test_logs_bind_vars - # FIXME: use skip with minitest - return unless @connection.supports_statement_cache? - pk = Topic.columns.find { |x| x.primary } payload = { @@ -86,5 +79,11 @@ module ActiveRecord logger.sql event assert_match([[pk.name, 10]].inspect, logger.debugs.first) end + + private + + def skip_if_prepared_statement_caching_is_not_supported + skip('prepared statement caching is not supported') unless @connection.supports_statement_cache? + end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index e1544b3b00..6cb6c469d2 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -1,10 +1,11 @@ require "cases/helper" +require 'models/club' require 'models/company' require "models/contract" -require 'models/topic' require 'models/edge' -require 'models/club' require 'models/organization' +require 'models/possession' +require 'models/topic' Company.has_many :accounts @@ -40,8 +41,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 @@ -49,13 +50,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_get_maximum_of_field_with_include - assert_equal 55, Account.maximum(:credit_limit, :include => :firm, :references => :companies, :conditions => "companies.name != 'Summit'") - end - - def test_should_get_maximum_of_field_with_scoped_include - Account.send :with_scope, :find => { :include => :firm, :references => :companies, :conditions => "companies.name != 'Summit'" } do - assert_equal 55, Account.maximum(:credit_limit) - end + assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(:credit_limit) end def test_should_get_minimum_of_field @@ -63,12 +58,21 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_field - c = Account.sum(:credit_limit, :group => :firm_id) - [1,6,2].each { |firm_id| assert c.keys.include?(firm_id) } + c = Account.group(:firm_id).sum(:credit_limit) + [1,6,2].each do |firm_id| + assert c.keys.include?(firm_id), "Group #{c.inspect} does not contain firm_id #{firm_id}" + end + end + + def test_should_group_by_arel_attribute + c = Account.group(Account.arel_table[:firm_id]).sum(:credit_limit) + [1,6,2].each do |firm_id| + assert c.keys.include?(firm_id), "Group #{c.inspect} does not contain firm_id #{firm_id}" + end end def test_should_group_by_multiple_fields - c = Account.count(:all, :group => ['firm_id', :credit_limit]) + c = Account.group('firm_id', :credit_limit).count(:all) [ [nil, 50], [1, 50], [6, 50], [6, 55], [9, 53], [2, 60] ].each { |firm_and_limit| assert c.keys.include?(firm_and_limit) } end @@ -81,32 +85,32 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field - c = Account.sum(:credit_limit, :group => :firm_id) + c = Account.group(:firm_id).sum(:credit_limit) assert_equal 50, c[1] assert_equal 105, c[6] assert_equal 60, c[2] end def test_should_order_by_grouped_field - c = Account.sum(:credit_limit, :group => :firm_id, :order => "firm_id") + 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.sum(:credit_limit, :group => :firm_id, :order => "sum_credit_limit desc, firm_id") + 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.sum(:credit_limit, :conditions => "firm_id IS NOT NULL", - :group => :firm_id, :order => "firm_id", :limit => 2) + 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.sum(:credit_limit, :conditions => "firm_id IS NOT NULL", - :group => :firm_id, :order => "firm_id", :limit => 2, :offset => 1) + 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 @@ -156,16 +160,8 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_having_condition - c = Account.sum(:credit_limit, :group => :firm_id, - :having => 'sum(credit_limit) > 50') - assert_nil c[1] - assert_equal 105, c[6] - assert_equal 60, c[2] - end - - def test_should_group_by_summed_field_having_sanitized_condition - c = Account.sum(:credit_limit, :group => :firm_id, - :having => ['sum(credit_limit) > ?', 50]) + c = Account.all.merge!(:group => :firm_id, + :having => 'sum(credit_limit) > 50').sum(:credit_limit) assert_nil c[1] assert_equal 105, c[6] assert_equal 60, c[2] @@ -179,19 +175,19 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_association - c = Account.sum(:credit_limit, :group => :firm) + c = Account.group(:firm).sum(:credit_limit) assert_equal 50, c[companies(:first_firm)] assert_equal 105, c[companies(:rails_core)] assert_equal 60, c[companies(:first_client)] end def test_should_sum_field_with_conditions - assert_equal 105, Account.sum(:credit_limit, :conditions => 'firm_id = 6') + assert_equal 105, Account.where('firm_id = 6').sum(:credit_limit) end def test_should_return_zero_if_sum_conditions_return_nothing - assert_equal 0, Account.sum(:credit_limit, :conditions => '1 = 2') - assert_equal 0, companies(:rails_core).companies.sum(:id, :conditions => '1 = 2') + assert_equal 0, Account.where('1 = 2').sum(:credit_limit) + assert_equal 0, companies(:rails_core).companies.where('1 = 2').sum(:id) end def test_sum_should_return_valid_values_for_decimals @@ -200,24 +196,24 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_with_conditions - c = Account.sum(:credit_limit, :conditions => 'firm_id > 1', - :group => :firm_id) + c = Account.all.merge!(:where => 'firm_id > 1', + :group => :firm_id).sum(:credit_limit) assert_nil c[1] assert_equal 105, c[6] assert_equal 60, c[2] end def test_should_group_by_summed_field_with_conditions_and_having - c = Account.sum(:credit_limit, :conditions => 'firm_id > 1', - :group => :firm_id, - :having => 'sum(credit_limit) > 60') + c = Account.all.merge!(:where => 'firm_id > 1', + :group => :firm_id, + :having => 'sum(credit_limit) > 60').sum(:credit_limit) assert_nil c[1] assert_equal 105, c[6] assert_nil c[2] end def test_should_group_by_fields_with_table_alias - c = Account.sum(:credit_limit, :group => 'accounts.firm_id') + c = Account.group('accounts.firm_id').sum(:credit_limit) assert_equal 50, c[1] assert_equal 105, c[6] assert_equal 60, c[2] @@ -229,14 +225,14 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_calculate_grouped_with_invalid_field - c = Account.count(:all, :group => 'accounts.firm_id') + c = Account.group('accounts.firm_id').count(:all) assert_equal 1, c[1] assert_equal 2, c[6] assert_equal 1, c[2] end def test_should_calculate_grouped_association_with_invalid_field - c = Account.count(:all, :group => :firm) + c = Account.group(:firm).count(:all) assert_equal 1, c[companies(:first_firm)] assert_equal 2, c[companies(:rails_core)] assert_equal 1, c[companies(:first_client)] @@ -255,7 +251,7 @@ class CalculationsTest < ActiveRecord::TestCase column.expects(:type_cast).with("ABC").returns("ABC") Account.expects(:columns).at_least_once.returns([column]) - c = Account.count(:all, :group => :firm) + c = Account.group(:firm).count(:all) first_key = c.keys.first assert_equal Firm, first_key.class assert_equal 1, c[first_key] @@ -263,22 +259,14 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_calculate_grouped_association_with_foreign_key_option Account.belongs_to :another_firm, :class_name => 'Firm', :foreign_key => 'firm_id' - c = Account.count(:all, :group => :another_firm) + c = Account.group(:another_firm).count(:all) assert_equal 1, c[companies(:first_firm)] assert_equal 2, c[companies(:rails_core)] assert_equal 1, c[companies(:first_client)] end - def test_should_not_modify_options_when_using_includes - options = {:conditions => 'companies.id > 1', :include => :firm, :references => :companies} - options_copy = options.dup - - Account.count(:all, options) - assert_equal options_copy, options - end - def test_should_calculate_grouped_by_function - c = Company.count(:all, :group => "UPPER(#{QUOTED_TYPE})") + c = Company.group("UPPER(#{QUOTED_TYPE})").count(:all) assert_equal 2, c[nil] assert_equal 1, c['DEPENDENTFIRM'] assert_equal 4, c['CLIENT'] @@ -286,7 +274,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_calculate_grouped_by_function_with_table_alias - c = Company.count(:all, :group => "UPPER(companies.#{QUOTED_TYPE})") + c = Company.group("UPPER(companies.#{QUOTED_TYPE})").count(:all) assert_equal 2, c[nil] assert_equal 1, c['DEPENDENTFIRM'] assert_equal 4, c['CLIENT'] @@ -306,25 +294,24 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_sum_scoped_field_with_conditions - assert_equal 8, companies(:rails_core).companies.sum(:id, :conditions => 'id > 7') + assert_equal 8, companies(:rails_core).companies.where('id > 7').sum(:id) end def test_should_group_by_scoped_field - c = companies(:rails_core).companies.sum(:id, :group => :name) + c = companies(:rails_core).companies.group(:name).sum(:id) assert_equal 7, c['Leetsoft'] assert_equal 8, c['Jadedpixel'] end def test_should_group_by_summed_field_through_association_and_having - c = companies(:rails_core).companies.sum(:id, :group => :name, - :having => 'sum(id) > 7') + c = companies(:rails_core).companies.group(:name).having('sum(id) > 7').sum(:id) assert_nil c['Leetsoft'] assert_equal 8, c['Jadedpixel'] end def test_should_count_selected_field_with_include - assert_equal 6, Account.count(:distinct => true, :include => :firm) - assert_equal 4, Account.count(:distinct => true, :include => :firm, :select => :credit_limit) + assert_equal 6, Account.includes(:firm).count(:distinct => true) + assert_equal 4, Account.includes(:firm).select(:credit_limit).count(:distinct => true) end def test_should_not_perform_joined_include_by_default @@ -340,19 +327,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").count(:conditions => ['credit_limit >= 50']) + 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.count(:select => "DISTINCT accounts.id", :include => :firm) + assert_equal 6, Account.all.merge!(:select => "DISTINCT accounts.id", :includes => :firm).count end def test_count_with_column_parameter @@ -360,16 +347,16 @@ class CalculationsTest < ActiveRecord::TestCase end def test_count_with_column_and_options_parameter - assert_equal 2, Account.count(:firm_id, :conditions => "credit_limit = 50 AND firm_id IS NOT NULL") + assert_equal 2, Account.where("credit_limit = 50 AND firm_id IS NOT NULL").count(:firm_id) end def test_should_count_field_in_joined_table - assert_equal 5, Account.count('companies.id', :joins => :firm) - assert_equal 4, Account.count('companies.id', :joins => :firm, :distinct => true) + assert_equal 5, Account.joins(:firm).count('companies.id') + assert_equal 4, Account.joins(:firm).count('companies.id', :distinct => true) end def test_should_count_field_in_joined_table_with_group_by - c = Account.count('companies.id', :group => 'accounts.firm_id', :joins => :firm) + 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 @@ -392,17 +379,33 @@ class CalculationsTest < ActiveRecord::TestCase end def test_count_with_from_option - assert_equal Company.count(:all), Company.count(:all, :from => 'companies') - assert_equal Account.count(:all, :conditions => "credit_limit = 50"), - Account.count(:all, :from => 'accounts', :conditions => "credit_limit = 50") - assert_equal Company.count(:type, :conditions => {:type => "Firm"}), - Company.count(:type, :conditions => {:type => "Firm"}, :from => 'companies') + assert_equal Company.count(:all), Company.from('companies').count(:all) + assert_equal Account.where("credit_limit = 50").count(:all), + Account.from('accounts').where("credit_limit = 50").count(:all) + assert_equal Company.where(:type => "Firm").count(:type), + Company.where(:type => "Firm").from('companies').count(:type) + end + + def test_count_with_block_acts_as_array + accounts = Account.where('id > 0') + assert_equal Account.count, accounts.count { true } + assert_equal 0, accounts.count { false } + assert_equal Account.where('credit_limit > 50').size, accounts.count { |account| account.credit_limit > 50 } + assert_equal Account.count, Account.count { true } + assert_equal 0, Account.count { false } + end + + def test_sum_with_block_acts_as_array + accounts = Account.where('id > 0') + assert_equal Account.sum(:credit_limit), accounts.sum { |account| account.credit_limit } + assert_equal Account.sum(:credit_limit) + Account.count, accounts.sum{ |account| account.credit_limit + 1 } + assert_equal 0, accounts.sum { |account| 0 } end def test_sum_with_from_option - assert_equal Account.sum(:credit_limit), Account.sum(:credit_limit, :from => 'accounts') - assert_equal Account.sum(:credit_limit, :conditions => "credit_limit > 50"), - Account.sum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.sum(:credit_limit), Account.from('accounts').sum(:credit_limit) + assert_equal Account.where("credit_limit > 50").sum(:credit_limit), + Account.where("credit_limit > 50").from('accounts').sum(:credit_limit) end def test_sum_array_compatibility @@ -410,33 +413,67 @@ class CalculationsTest < ActiveRecord::TestCase end def test_average_with_from_option - assert_equal Account.average(:credit_limit), Account.average(:credit_limit, :from => 'accounts') - assert_equal Account.average(:credit_limit, :conditions => "credit_limit > 50"), - Account.average(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.average(:credit_limit), Account.from('accounts').average(:credit_limit) + assert_equal Account.where("credit_limit > 50").average(:credit_limit), + Account.where("credit_limit > 50").from('accounts').average(:credit_limit) end def test_minimum_with_from_option - assert_equal Account.minimum(:credit_limit), Account.minimum(:credit_limit, :from => 'accounts') - assert_equal Account.minimum(:credit_limit, :conditions => "credit_limit > 50"), - Account.minimum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.minimum(:credit_limit), Account.from('accounts').minimum(:credit_limit) + assert_equal Account.where("credit_limit > 50").minimum(:credit_limit), + Account.where("credit_limit > 50").from('accounts').minimum(:credit_limit) end def test_maximum_with_from_option - assert_equal Account.maximum(:credit_limit), Account.maximum(:credit_limit, :from => 'accounts') - assert_equal Account.maximum(:credit_limit, :conditions => "credit_limit > 50"), - Account.maximum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.maximum(:credit_limit), Account.from('accounts').maximum(:credit_limit) + assert_equal Account.where("credit_limit > 50").maximum(:credit_limit), + Account.where("credit_limit > 50").from('accounts').maximum(:credit_limit) + end + + def test_maximum_with_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + + # TODO: Investigate why PG isn't being typecast + if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:MysqlAdapter) + assert_equal "7", Company.includes(:contracts).maximum(:developer_id) + else + assert_equal 7, Company.includes(:contracts).maximum(:developer_id) + end end + def test_minimum_with_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + + # TODO: Investigate why PG isn't being typecast + if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:MysqlAdapter) + assert_equal "7", Company.includes(:contracts).minimum(:developer_id) + else + assert_equal 7, Company.includes(:contracts).minimum(:developer_id) + end + end + + def test_sum_with_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + + # TODO: Investigate why PG isn't being typecast + if current_adapter?(:MysqlAdapter) || current_adapter?(:PostgreSQLAdapter) + assert_equal "7", Company.includes(:contracts).sum(:developer_id) + else + assert_equal 7, Company.includes(:contracts).sum(:developer_id) + end + end + + def test_from_option_with_specified_index if Edge.connection.adapter_name == 'MySQL' or Edge.connection.adapter_name == 'Mysql2' - assert_equal Edge.count(:all), Edge.count(:all, :from => 'edges USE INDEX(unique_edge_index)') - assert_equal Edge.count(:all, :conditions => 'sink_id < 5'), - Edge.count(:all, :from => 'edges USE INDEX(unique_edge_index)', :conditions => 'sink_id < 5') + assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all) + assert_equal Edge.where('sink_id < 5').count(:all), + Edge.from('edges USE INDEX(unique_edge_index)').where('sink_id < 5').count(:all) end end def test_from_option_with_table_different_than_class - assert_equal Account.count(:all), Company.count(:all, :from => 'accounts') + assert_equal Account.count(:all), Company.from('accounts').count(:all) end def test_distinct_is_honored_when_used_with_count_operation_after_group @@ -484,13 +521,66 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [c.id], Company.joins(:contracts).pluck(:id) end + def test_pluck_if_table_included + c = Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + assert_equal [c.id], Company.includes(:contracts).where("contracts.id" => c.contracts.first).pluck(:id) + end + def test_pluck_not_auto_table_name_prefix_if_column_joined Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) - # FIXME: PostgreSQL should works in the same way of the other adapters - if current_adapter?(:PostgreSQLAdapter) - assert_equal ["7"], Company.joins(:contracts).pluck(:developer_id) - else - assert_equal [7], Company.joins(:contracts).pluck(:developer_id) - end + assert_equal [7], Company.joins(:contracts).pluck(:developer_id) + end + + def test_pluck_with_selection_clause + assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT credit_limit').sort + assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT accounts.credit_limit').sort + assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT(credit_limit)').sort + + # MySQL returns "SUM(DISTINCT(credit_limit))" as the column name unless + # an alias is provided. Without the alias, the column cannot be found + # and properly typecast. + assert_equal [50 + 53 + 55 + 60], Account.pluck('SUM(DISTINCT(credit_limit)) as credit_limit') + end + + def test_plucks_with_ids + assert_equal Company.all.map(&:id).sort, Company.ids.sort + end + + def test_pluck_not_auto_table_name_prefix_if_column_included + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + ids = Company.includes(:contracts).pluck(:developer_id) + assert_equal Company.count, ids.length + assert_equal [7], ids.compact + end + + def test_pluck_multiple_columns + assert_equal [ + [1, "The First Topic"], [2, "The Second Topic of the day"], + [3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"] + ], Topic.order(:id).pluck(:id, :title) + assert_equal [ + [1, "The First Topic", "David"], [2, "The Second Topic of the day", "Mary"], + [3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"] + ], Topic.order(:id).pluck(:id, :title, :author_name) + end + + def test_pluck_with_multiple_columns_and_selection_clause + assert_equal [[1, 50], [2, 50], [3, 50], [4, 60], [5, 55], [6, 53]], + Account.pluck('id, credit_limit') + end + + def test_pluck_with_multiple_columns_and_includes + Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) + companies_and_developers = Company.order('companies.id').includes(:contracts).pluck(:name, :developer_id) + + assert_equal Company.count, companies_and_developers.length + assert_equal ["37signals", nil], companies_and_developers.first + assert_equal ["test", 7], companies_and_developers.last + end + + def test_pluck_with_reserved_words + Possession.create!(:where => "Over There") + + assert_equal ["Over There"], Possession.pluck(:where) end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 7690769226..deeef3a3fd 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -426,11 +426,13 @@ class CallbacksTest < ActiveRecord::TestCase def test_before_destroy_returning_false david = ImmutableDeveloper.find(1) assert !david.destroy + assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackCancellationDeveloper.find(1) someone.cancel_before_destroy = true assert !someone.destroy + assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } assert !someone.after_destroy_called end diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb index c7dcc21809..b874adc081 100644 --- a/activerecord/test/cases/coders/yaml_column_test.rb +++ b/activerecord/test/cases/coders/yaml_column_test.rb @@ -9,6 +9,13 @@ module ActiveRecord assert_equal Object, coder.object_class end + def test_type_mismatch_on_different_classes_on_dump + coder = YAMLColumn.new(Array) + assert_raises(SerializationTypeMismatch) do + coder.dump("a") + end + end + def test_type_mismatch_on_different_classes coder = YAMLColumn.new(Array) assert_raises(SerializationTypeMismatch) do diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index 14884e42af..bd2fbaa7db 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -126,19 +126,16 @@ module ActiveRecord if current_adapter?(:PostgreSQLAdapter) def test_bigint_column_should_map_to_integer - bigint_column = PostgreSQLColumn.new('number', nil, "bigint") + oid = PostgreSQLAdapter::OID::Identity.new + bigint_column = PostgreSQLColumn.new('number', nil, oid, "bigint") assert_equal :integer, bigint_column.type end def test_smallint_column_should_map_to_integer - smallint_column = PostgreSQLColumn.new('number', nil, "smallint") + oid = PostgreSQLAdapter::OID::Identity.new + smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint") assert_equal :integer, smallint_column.type end - - def test_uuid_column_should_map_to_string - uuid_column = PostgreSQLColumn.new('unique_id', nil, "uuid") - assert_equal :string, uuid_column.type - end end end end diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb index ccc57cb876..a7b63d15c9 100644 --- a/activerecord/test/cases/column_test.rb +++ b/activerecord/test/cases/column_test.rb @@ -24,6 +24,64 @@ module ActiveRecord assert !column.type_cast('off') assert !column.type_cast('OFF') end + + def test_type_cast_integer + column = Column.new("field", nil, "integer") + assert_equal 1, column.type_cast(1) + assert_equal 1, column.type_cast('1') + assert_equal 1, column.type_cast('1ignore') + assert_equal 0, column.type_cast('bad1') + assert_equal 0, column.type_cast('bad') + assert_equal 1, column.type_cast(1.7) + assert_nil column.type_cast(nil) + end + + def test_type_cast_non_integer_to_integer + column = Column.new("field", nil, "integer") + assert_raises(NoMethodError) do + column.type_cast([]) + end + assert_raises(NoMethodError) do + column.type_cast(true) + end + assert_raises(NoMethodError) do + column.type_cast(false) + end + end + + def test_type_cast_time + column = Column.new("field", nil, "time") + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, column.type_cast(time_string).strftime("%T") + end + + def test_type_cast_datetime_and_timestamp + [Column.new("field", nil, "datetime"), Column.new("field", nil, "timestamp")].each do |column| + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T") + end + end + + def test_type_cast_date + column = Column.new("field", nil, "date") + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, column.type_cast(date_string).strftime("%F") + end + + def test_type_cast_duration_to_integer + column = Column.new("field", nil, "integer") + assert_equal 1800, column.type_cast(30.minutes) + assert_equal 7200, column.type_cast(2.hours) + end end end end diff --git a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb index 7dc6e8afcb..3e3d6e2769 100644 --- a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb +++ b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb @@ -36,7 +36,7 @@ module ActiveRecord def test_close pool = ConnectionPool.new(ConnectionSpecification.new({}, nil)) - pool.connections << adapter + pool.insert_connection_for_test! adapter adapter.pool = pool # Make sure the pool marks the connection in use diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index dc99ac665c..e1579d037f 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -4,17 +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 - def self.name; 'america'; end - class << self - alias active_record_super superclass - end - end - @subklass = Class.new(@klass) do - def self.name; 'north america'; end - end + @handler.establish_connection @klass, Base.connection_pool.spec end def test_retrieve_connection @@ -43,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/connection_adapters/quoting_test.rb b/activerecord/test/cases/connection_adapters/quoting_test.rb new file mode 100644 index 0000000000..59dcb96ebc --- /dev/null +++ b/activerecord/test/cases/connection_adapters/quoting_test.rb @@ -0,0 +1,13 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + module Quoting + class QuotingTest < ActiveRecord::TestCase + def test_quoting_classes + assert_equal "'Object'", AbstractAdapter.new(nil).quote(Object) + end + end + end + end +end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 42e39d534c..541e983758 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -39,6 +39,21 @@ module ActiveRecord assert_equal 0, @cache.tables.size assert_equal 0, @cache.primary_keys.size end + + def test_dump_and_load + @cache.columns['posts'] + @cache.columns_hash['posts'] + @cache.tables['posts'] + @cache.primary_keys['posts'] + + @cache = Marshal.load(Marshal.dump(@cache)) + + assert_equal 12, @cache.columns['posts'].size + assert_equal 12, @cache.columns_hash['posts'].size + assert @cache.tables['posts'] + assert_equal 'id', @cache.primary_keys['posts'] + end + end end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index 496cd78136..fe1b40d884 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -26,6 +26,27 @@ module ActiveRecord assert ActiveRecord::Base.connection_handler.active_connections? end + def test_connection_pool_per_pid + return skip('must support fork') unless Process.respond_to?(:fork) + + object_id = ActiveRecord::Base.connection.object_id + + rd, wr = IO.pipe + + pid = fork { + rd.close + wr.write Marshal.dump ActiveRecord::Base.connection.object_id + wr.close + exit! + } + + wr.close + + Process.waitpid pid + assert_not_equal object_id, Marshal.load(rd.read) + rd.close + end + def test_app_delegation manager = ConnectionManagement.new(@app) diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 2c69bfde5b..8287b35aaf 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -3,6 +3,8 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class ConnectionPoolTest < ActiveRecord::TestCase + attr_reader :pool + def setup super @@ -25,6 +27,67 @@ module ActiveRecord @pool.disconnect! end + def active_connections(pool) + pool.connections.find_all(&:in_use?) + end + + def test_checkout_after_close + connection = pool.connection + assert connection.in_use? + + connection.close + assert !connection.in_use? + + assert pool.connection.in_use? + end + + def test_released_connection_moves_between_threads + thread_conn = nil + + Thread.new { + pool.with_connection do |conn| + thread_conn = conn + end + }.join + + assert thread_conn + + Thread.new { + pool.with_connection do |conn| + assert_equal thread_conn, conn + end + }.join + end + + def test_with_connection + assert_equal 0, active_connections(pool).size + + main_thread = pool.connection + assert_equal 1, active_connections(pool).size + + Thread.new { + pool.with_connection do |conn| + assert conn + assert_equal 2, active_connections(pool).size + end + assert_equal 1, active_connections(pool).size + }.join + + main_thread.close + assert_equal 0, active_connections(pool).size + end + + def test_active_connection_in_use + assert !pool.active_connection? + main_thread = pool.connection + + assert pool.active_connection? + + main_thread.close + + assert !pool.active_connection? + end + def test_full_pool_exception assert_raises(PoolFullError) do (@pool.size + 1).times do @@ -33,11 +96,35 @@ module ActiveRecord end end + def test_full_pool_blocks + cs = @pool.size.times.map { @pool.checkout } + t = Thread.new { @pool.checkout } + + # make sure our thread is in the timeout section + Thread.pass until t.status == "sleep" + + connection = cs.first + connection.close + assert_equal connection, t.join.value + end + + def test_removing_releases_latch + cs = @pool.size.times.map { @pool.checkout } + t = Thread.new { @pool.checkout } + + # make sure our thread is in the timeout section + Thread.pass until t.status == "sleep" + + connection = cs.first + @pool.remove connection + assert_respond_to t.join.value, :execute + end + def test_reap_and_active @pool.checkout @pool.checkout @pool.checkout - @pool.timeout = 0 + @pool.dead_connection_timeout = 0 connections = @pool.connections.dup @@ -50,7 +137,7 @@ module ActiveRecord @pool.checkout @pool.checkout @pool.checkout - @pool.timeout = 0 + @pool.dead_connection_timeout = 0 connections = @pool.connections.dup connections.each do |conn| @@ -113,6 +200,110 @@ module ActiveRecord end.join end + # The connection pool is "fair" if threads waiting for + # connections receive them the order in which they began + # waiting. This ensures that we don't timeout one HTTP request + # even while well under capacity in a multi-threaded environment + # such as a Java servlet container. + # + # We don't need strict fairness: if two connections become + # available at the same time, it's fine of two threads that were + # waiting acquire the connections out of order. + # + # Thus this test prepares waiting threads and then trickles in + # available connections slowly, ensuring the wakeup order is + # correct in this case. + def test_checkout_fairness + @pool.instance_variable_set(:@size, 10) + expected = (1..@pool.size).to_a.freeze + # check out all connections so our threads start out waiting + conns = expected.map { @pool.checkout } + mutex = Mutex.new + order = [] + errors = [] + + threads = expected.map do |i| + t = Thread.new { + begin + @pool.checkout # never checked back in + mutex.synchronize { order << i } + rescue => e + mutex.synchronize { errors << e } + end + } + Thread.pass until t.status == "sleep" + t + end + + # this should wake up the waiting threads one by one in order + conns.each { |conn| @pool.checkin(conn); sleep 0.1 } + + threads.each(&:join) + + raise errors.first if errors.any? + + assert_equal(expected, order) + end + + # As mentioned in #test_checkout_fairness, we don't care about + # strict fairness. This test creates two groups of threads: + # group1 whose members all start waiting before any thread in + # group2. Enough connections are checked in to wakeup all + # group1 threads, and the fact that only group1 and no group2 + # threads acquired a connection is enforced. + def test_checkout_fairness_by_group + @pool.instance_variable_set(:@size, 10) + # take all the connections + conns = (1..10).map { @pool.checkout } + mutex = Mutex.new + successes = [] # threads that successfully got a connection + errors = [] + + make_thread = proc do |i| + t = Thread.new { + begin + @pool.checkout # never checked back in + mutex.synchronize { successes << i } + rescue => e + mutex.synchronize { errors << e } + end + } + Thread.pass until t.status == "sleep" + t + end + + # all group1 threads start waiting before any in group2 + group1 = (1..5).map(&make_thread) + group2 = (6..10).map(&make_thread) + + # checkin n connections back to the pool + checkin = proc do |n| + n.times do + c = conns.pop + @pool.checkin(c) + end + end + + checkin.call(group1.size) # should wake up all group1 + + loop do + sleep 0.1 + break if mutex.synchronize { (successes.size + errors.size) == group1.size } + end + + winners = mutex.synchronize { successes.dup } + checkin.call(group2.size) # should wake up everyone remaining + + group1.each(&:join) + group2.each(&:join) + + assert_equal((1..group1.size).to_a, winners.sort) + + if errors.any? + raise errors.first + end + end + def test_automatic_reconnect= pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec assert pool.automatic_reconnect diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index e6cb1b9521..673a2b2b88 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -9,6 +9,7 @@ module ActiveRecord end def test_url_host_no_db + skip "only if mysql is available" unless defined?(MysqlAdapter) spec = resolve 'mysql://foo?encoding=utf8' assert_equal({ :adapter => "mysql", @@ -18,6 +19,7 @@ module ActiveRecord end def test_url_host_db + skip "only if mysql is available" unless defined?(MysqlAdapter) spec = resolve 'mysql://foo/bar?encoding=utf8' assert_equal({ :adapter => "mysql", @@ -27,6 +29,7 @@ module ActiveRecord end def test_url_port + skip "only if mysql is available" unless defined?(MysqlAdapter) spec = resolve 'mysql://foo:123?encoding=utf8' assert_equal({ :adapter => "mysql", diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index cd3d19e783..ee443741ca 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -8,9 +8,11 @@ require 'models/category' require 'models/categorization' require 'models/dog' require 'models/dog_lover' +require 'models/person' +require 'models/friendship' class CounterCacheTest < ActiveRecord::TestCase - fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers + fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers, :people, :friendships class ::SpecialTopic < ::Topic has_many :special_replies, :foreign_key => 'parent_id' @@ -109,4 +111,11 @@ class CounterCacheTest < ActiveRecord::TestCase Topic.update_counters([t1.id, t2.id], :replies_count => 2) end end + + test "reset the right counter if two have the same foreign key" do + michael = people(:michael) + assert_nothing_raised(ActiveRecord::StatementInvalid) do + Person.reset_counters(michael.id, :followers) + end + end end diff --git a/activerecord/test/cases/custom_locking_test.rb b/activerecord/test/cases/custom_locking_test.rb index d63ecdbcc5..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.find(1, :lock => 'LOCK IN SHARE MODE') + 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 new file mode 100644 index 0000000000..392f5f4cd5 --- /dev/null +++ b/activerecord/test/cases/deprecated_dynamic_methods_test.rb @@ -0,0 +1,608 @@ +# 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 +# finders so it is useful to keep this around to guard against regressions if +# we need to change the code. + +require 'cases/helper' +require 'models/topic' +require 'models/reply' +require 'models/customer' +require 'models/post' +require 'models/company' +require 'models/author' +require 'models/category' +require 'models/comment' +require 'models/person' +require 'models/reader' + +class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase + fixtures :topics, :customers, :companies, :accounts, :posts, :categories, :categories_posts, :authors, :people, :comments, :readers + + def setup + @deprecation_behavior = ActiveSupport::Deprecation.behavior + ActiveSupport::Deprecation.behavior = :silence + end + + def teardown + ActiveSupport::Deprecation.behavior = @deprecation_behavior + end + + def test_find_all_by_one_attribute + topics = Topic.find_all_by_content("Have a nice day") + assert_equal 2, topics.size + assert topics.include?(topics(:first)) + + assert_equal [], Topic.find_all_by_title("The First Topic!!") + end + + def test_find_all_by_one_attribute_which_is_a_symbol + topics = Topic.find_all_by_content("Have a nice day".to_sym) + assert_equal 2, topics.size + assert topics.include?(topics(:first)) + + 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 + + topics = Topic.find_all_by_content("Have a nice day", :order => "id") + assert_equal topics(:first), topics.first + end + + def test_find_all_by_array_attribute + assert_equal 2, Topic.find_all_by_title(["The First Topic", "The Second Topic of the day"]).size + end + + def test_find_all_by_boolean_attribute + topics = Topic.find_all_by_approved(false) + assert_equal 1, topics.size + assert topics.include?(topics(:first)) + + topics = Topic.find_all_by_approved(true) + assert_equal 3, topics.size + assert topics.include?(topics(:second)) + end + + def test_find_all_by_nil_and_not_nil_attributes + topics = Topic.find_all_by_last_read_and_author_name nil, "Mary" + assert_equal 1, topics.size + assert_equal "Mary", topics[0].author_name + end + + def test_find_or_create_from_one_attribute + number_of_companies = Company.count + sig38 = Company.find_or_create_by_name("38signals") + assert_equal number_of_companies + 1, Company.count + assert_equal sig38, Company.find_or_create_by_name("38signals") + assert sig38.persisted? + end + + def test_find_or_create_from_two_attributes + number_of_topics = Topic.count + another = Topic.find_or_create_by_title_and_author_name("Another topic","John") + assert_equal number_of_topics + 1, Topic.count + assert_equal another, Topic.find_or_create_by_title_and_author_name("Another topic", "John") + assert another.persisted? + end + + def test_find_or_create_from_one_attribute_bang + number_of_companies = Company.count + assert_raises(ActiveRecord::RecordInvalid) { Company.find_or_create_by_name!("") } + assert_equal number_of_companies, Company.count + sig38 = Company.find_or_create_by_name!("38signals") + assert_equal number_of_companies + 1, Company.count + assert_equal sig38, Company.find_or_create_by_name!("38signals") + assert sig38.persisted? + end + + def test_find_or_create_from_two_attributes_bang + number_of_companies = Company.count + assert_raises(ActiveRecord::RecordInvalid) { Company.find_or_create_by_name_and_firm_id!("", 17) } + assert_equal number_of_companies, Company.count + sig38 = Company.find_or_create_by_name_and_firm_id!("38signals", 17) + assert_equal number_of_companies + 1, Company.count + assert_equal sig38, Company.find_or_create_by_name_and_firm_id!("38signals", 17) + assert sig38.persisted? + assert_equal "38signals", sig38.name + 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}) + assert_equal number_of_companies + 1, Company.count + assert_equal sig38, Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) + assert sig38.persisted? + assert_equal "38signals", sig38.name + assert_equal 17, sig38.firm_id + assert_equal 23, sig38.client_of + end + + def test_find_or_create_from_two_attributes_and_hash + number_of_companies = Company.count + sig38 = Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23}) + assert_equal number_of_companies + 1, Company.count + assert_equal sig38, Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23}) + assert sig38.persisted? + assert_equal "38signals", sig38.name + assert_equal 17, sig38.firm_id + 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 + assert_not_equal 1000, c.rating + assert c.valid? + assert !c.persisted? + end + + def test_find_or_create_from_one_attribute_should_not_set_attribute_even_when_protected + c = Company.find_or_create_by_name({:name => "Fortune 1000", :rating => 1000}) + assert_equal "Fortune 1000", c.name + assert_not_equal 1000, c.rating + assert c.valid? + assert c.persisted? + end + + def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected + c = Company.find_or_initialize_by_name_and_rating("Fortune 1000", 1000) + assert_equal "Fortune 1000", c.name + assert_equal 1000, c.rating + assert c.valid? + assert !c.persisted? + end + + def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected + c = Company.find_or_create_by_name_and_rating("Fortune 1000", 1000) + assert_equal "Fortune 1000", c.name + assert_equal 1000, c.rating + assert c.valid? + assert c.persisted? + end + + def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash + c = Company.find_or_initialize_by_rating(1000, {:name => "Fortune 1000"}) + assert_equal "Fortune 1000", c.name + assert_equal 1000, c.rating + assert c.valid? + assert !c.persisted? + end + + def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash + c = Company.find_or_create_by_rating(1000, {:name => "Fortune 1000"}) + assert_equal "Fortune 1000", c.name + assert_equal 1000, c.rating + assert c.valid? + assert c.persisted? + end + + def test_find_or_initialize_should_set_protected_attributes_if_given_as_block + c = Company.find_or_initialize_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } + assert_equal "Fortune 1000", c.name + assert_equal 1000.to_f, c.rating.to_f + assert c.valid? + assert !c.persisted? + end + + def test_find_or_create_should_set_protected_attributes_if_given_as_block + c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } + assert_equal "Fortune 1000", c.name + assert_equal 1000.to_f, c.rating.to_f + assert c.valid? + assert c.persisted? + end + + def test_find_or_create_should_work_with_block_on_first_call + class << Company + undef_method(:find_or_create_by_name) if method_defined?(:find_or_create_by_name) + end + c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } + assert_equal "Fortune 1000", c.name + assert_equal 1000.to_f, c.rating.to_f + assert c.valid? + assert c.persisted? + end + + def test_find_or_initialize_from_two_attributes + another = Topic.find_or_initialize_by_title_and_author_name("Another topic","John") + assert_equal "Another topic", another.title + assert_equal "John", another.author_name + assert !another.persisted? + end + + def test_find_or_initialize_from_two_attributes_but_passing_only_one + 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 + assert_equal 17, sig38.firm_id + assert_equal 23, sig38.client_of + 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") + end + + def test_find_last_by_invalid_method_syntax + assert_raise(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") } + assert_raise(NoMethodError) { Topic.find_last_by_title?("The First Topic") } + end + + def test_find_last_by_one_attribute_with_several_options + assert_equal accounts(:signals37), Account.order('id DESC').where('id != ?', 3).find_last_by_credit_limit(50) + end + + def test_find_last_by_one_missing_attribute + assert_raise(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") } + end + + def test_find_last_by_two_attributes + topic = Topic.last + assert_equal topic, Topic.find_last_by_title_and_author_name(topic.title, topic.author_name) + assert_nil Topic.find_last_by_title_and_author_name(topic.title, "Anonymous") + end + + def test_find_last_with_limit_gives_same_result_when_loaded_and_unloaded + scope = Topic.limit(2) + unloaded_last = scope.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.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.to_a.last + assert_equal loaded_last, unloaded_last + end + + def test_find_all_by_nil_attribute + topics = Topic.find_all_by_last_read nil + assert_equal 3, topics.size + assert topics.collect(&:last_read).all?(&:nil?) + end + + def test_forwarding_to_dynamic_finders + welcome = Post.find(1) + assert_equal 4, Category.find_all_by_type('SpecialCategory').size + assert_equal 0, welcome.categories.find_all_by_type('SpecialCategory').size + assert_equal 2, welcome.categories.find_all_by_type('Category').size + end + + def test_dynamic_find_all_should_respect_association_order + 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.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.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 + + def test_dynamic_find_last_without_specified_order + assert_equal companies(:second_client), companies(:first_firm).unsorted_clients.find_last_by_type('Client') + end + + def test_dynamic_find_or_create_from_two_attributes_using_an_association + author = authors(:david) + number_of_posts = Post.count + another = author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") + assert_equal number_of_posts + 1, Post.count + assert_equal another, author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") + assert another.persisted? + 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.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.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.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.includes(:people).to_a.length + end + + def test_find_or_create_by_resets_cached_counters + person = Person.create! :first_name => 'tenderlove' + post = Post.first + + assert_equal [], person.readers + assert_nil person.readers.find_by_post_id(post.id) + + person.readers.find_or_create_by_post_id(post.id) + + assert_equal 1, person.readers.count + assert_equal 1, person.readers.length + assert_equal post, person.readers.first.post + assert_equal person, person.readers.first.person + end + + def test_find_or_initialize + the_client = companies(:first_firm).clients.find_or_initialize_by_name("Yet another client") + assert_equal companies(:first_firm).id, the_client.firm_id + assert_equal "Yet another client", the_client.name + assert !the_client.persisted? + end + + def test_find_or_create_updates_size + number_of_clients = companies(:first_firm).clients.size + the_client = companies(:first_firm).clients.find_or_create_by_name("Yet another client") + assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size + assert_equal the_client, companies(:first_firm).clients.find_or_create_by_name("Yet another client") + assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size + end + + def test_find_or_initialize_updates_collection_size + number_of_clients = companies(:first_firm).clients_of_firm.size + companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client") + assert_equal number_of_clients + 1, companies(:first_firm).clients_of_firm.size + end + + def test_find_or_initialize_returns_the_instantiated_object + client = companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client") + assert_equal client, companies(:first_firm).clients_of_firm[-1] + end + + def test_find_or_initialize_only_instantiates_a_single_object + number_of_clients = Client.count + companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client").save! + companies(:first_firm).save! + assert_equal number_of_clients+1, Client.count + end + + def test_find_or_create_with_hash + post = authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody') + assert_equal post, authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody') + assert post.persisted? + end + + def test_find_or_create_with_one_attribute_followed_by_hash + post = authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody') + assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody') + assert post.persisted? + end + + def test_find_or_create_should_work_with_block + post = authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'} + assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'} + assert post.persisted? + end + + def test_forwarding_to_dynamic_finders_2 + welcome = Post.find(1) + assert_equal 4, Comment.find_all_by_type('Comment').size + assert_equal 2, welcome.comments.find_all_by_type('Comment').size + end + + def test_dynamic_find_all_by_attributes + authors = Author.all + + davids = authors.find_all_by_name('David') + assert_kind_of Array, davids + assert_equal [authors(:david)], davids + end + + def test_dynamic_find_or_initialize_by_attributes + authors = Author.all + + lifo = authors.find_or_initialize_by_name('Lifo') + assert_equal "Lifo", lifo.name + assert !lifo.persisted? + + assert_equal authors(:david), authors.find_or_initialize_by_name(:name => 'David') + end + + def test_dynamic_find_or_create_by_attributes + authors = Author.all + + lifo = authors.find_or_create_by_name('Lifo') + assert_equal "Lifo", lifo.name + assert lifo.persisted? + + assert_equal authors(:david), authors.find_or_create_by_name(:name => 'David') + end + + def test_dynamic_find_or_create_by_attributes_bang + authors = Author.all + + assert_raises(ActiveRecord::RecordInvalid) { authors.find_or_create_by_name!('') } + + lifo = authors.find_or_create_by_name!('Lifo') + assert_equal "Lifo", lifo.name + assert lifo.persisted? + + assert_equal authors(:david), authors.find_or_create_by_name!(:name => 'David') + end + + def test_finder_block + t = Topic.first + found = nil + Topic.find_by_id(t.id) { |f| found = f } + assert_equal t, found + end + + def test_finder_block_nothing_found + bad_id = Topic.maximum(:id) + 1 + assert_nil Topic.find_by_id(bad_id) { |f| raise } + end + + def test_find_returns_block_value + t = Topic.first + x = Topic.find_by_id(t.id) { |f| "hi mom!" } + assert_equal "hi mom!", x + end + + def test_dynamic_finder_with_invalid_params + assert_raise(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" } + end + + def test_find_by_one_attribute_with_order_option + assert_equal accounts(:signals37), Account.find_by_credit_limit(50, :order => 'id') + assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :order => 'id DESC') + end + + def test_dynamic_find_by_attributes_should_yield_found_object + david = authors(:david) + yielded_value = nil + Author.find_by_name(david.name) do |author| + yielded_value = author + end + assert_equal david, yielded_value + end +end + +class DynamicScopeTest < ActiveRecord::TestCase + fixtures :posts + + def setup + @test_klass = Class.new(Post) do + def self.name; "Post"; end + end + @deprecation_behavior = ActiveSupport::Deprecation.behavior + ActiveSupport::Deprecation.behavior = :silence + end + + def teardown + ActiveSupport::Deprecation.behavior = @deprecation_behavior + end + + 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.all.merge!(:where => { :author_id => 1, :title => "Welcome to the weblog"}).first + end + + def test_dynamic_scope_should_create_methods_after_hitting_method_missing + assert_blank @test_klass.methods.grep(/scoped_by_type/) + @test_klass.scoped_by_type(nil) + assert_present @test_klass.methods.grep(/scoped_by_type/) + end + + def test_dynamic_scope_with_less_number_of_arguments + assert_raise(ArgumentError){ @test_klass.scoped_by_author_id_and_title(1) } + end +end + +class DynamicScopeMatchTest < ActiveRecord::TestCase + def test_scoped_by_no_match + assert_nil ActiveRecord::DynamicMatchers::Method.match(nil, "not_scoped_at_all") + end + + def test_scoped_by + model = stub(attribute_aliases: {}) + match = ActiveRecord::DynamicMatchers::Method.match(model, "scoped_by_age_and_sex_and_location") + assert_not_nil match + assert_equal %w(age sex location), match.attribute_names + end +end diff --git a/activerecord/test/cases/deprecated_finder_test.rb b/activerecord/test/cases/deprecated_finder_test.rb deleted file mode 100644 index 2afc91b769..0000000000 --- a/activerecord/test/cases/deprecated_finder_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "cases/helper" -require 'models/entrant' - -class DeprecatedFinderTest < ActiveRecord::TestCase - fixtures :entrants - - def test_deprecated_find_all_was_removed - assert_raise(NoMethodError) { Entrant.find_all } - end - - def test_deprecated_find_first_was_removed - assert_raise(NoMethodError) { Entrant.find_first } - end - - def test_deprecated_find_on_conditions_was_removed - assert_raise(NoMethodError) { Entrant.find_on_conditions } - end - - def test_count - assert_equal(0, Entrant.count(:conditions => "id > 3")) - assert_equal(1, Entrant.count(:conditions => ["id > ?", 2])) - assert_equal(2, Entrant.count(:conditions => ["id > ?", 1])) - end - - def test_count_by_sql - assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3")) - assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2])) - assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1])) - end -end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index b1ce846218..9a2a5a4e3c 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -79,6 +79,17 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_setting_time_attributes_with_time_zone_field_to_itself_should_not_be_marked_as_a_change + in_time_zone 'Paris' do + target = Class.new(ActiveRecord::Base) + target.table_name = 'pirates' + + pirate = target.create + pirate.created_on = pirate.created_on + assert !pirate.created_on_changed? + end + end + def test_time_attributes_changes_without_time_zone_by_skip in_time_zone 'Paris' do target = Class.new(ActiveRecord::Base) @@ -190,7 +201,7 @@ class DirtyTest < ActiveRecord::TestCase end end - def test_nullable_integer_zero_to_string_zero_not_marked_as_changed + def test_integer_zero_to_string_zero_not_marked_as_changed pirate = Pirate.new pirate.parrot_id = 0 pirate.catchphrase = 'arrr' @@ -202,6 +213,19 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.changed? end + def test_integer_zero_to_integer_zero_not_marked_as_changed + pirate = Pirate.new + pirate.parrot_id = 0 + pirate.catchphrase = 'arrr' + assert pirate.save! + + assert !pirate.changed? + + pirate.parrot_id = 0 + assert !pirate.changed? + end + + def test_zero_to_blank_marked_as_changed pirate = Pirate.new pirate.catchphrase = "Yarrrr, me hearties" @@ -288,7 +312,7 @@ class DirtyTest < ActiveRecord::TestCase with_partial_updates Pirate, false do assert_queries(2) { 2.times { pirate.save! } } - Pirate.update_all({ :updated_on => old_updated_on }, :id => pirate.id) + Pirate.where(id: pirate.id).update_all(:updated_on => old_updated_on) end with_partial_updates Pirate, true do @@ -306,7 +330,7 @@ class DirtyTest < ActiveRecord::TestCase with_partial_updates Person, false do assert_queries(2) { 2.times { person.save! } } - Person.update_all({ :first_name => 'baz' }, :id => person.id) + Person.where(id: person.id).update_all(:first_name => 'baz') end with_partial_updates Person, true do @@ -413,7 +437,7 @@ class DirtyTest < ActiveRecord::TestCase with_partial_updates(Topic) do Topic.create!(:author_name => 'Bill', :content => {:a => "a"}) topic = Topic.select('id, author_name').first - topic.update_column :author_name, 'John' + topic.update_columns author_name: 'John' topic = Topic.first assert_not_nil topic.content end @@ -497,6 +521,35 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.previous_changes.key?('created_on') end + if ActiveRecord::Base.connection.supports_migrations? + class Testings < ActiveRecord::Base; end + def test_field_named_field + ActiveRecord::Base.connection.create_table :testings do |t| + t.string :field + end + assert_nothing_raised do + Testings.new.attributes + end + ensure + ActiveRecord::Base.connection.drop_table :testings rescue nil + 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/dynamic_finder_match_test.rb b/activerecord/test/cases/dynamic_finder_match_test.rb deleted file mode 100644 index e576870317..0000000000 --- a/activerecord/test/cases/dynamic_finder_match_test.rb +++ /dev/null @@ -1,98 +0,0 @@ -require "cases/helper" - -module ActiveRecord - class DynamicFinderMatchTest < ActiveRecord::TestCase - def test_find_or_create_by - match = DynamicFinderMatch.match("find_or_create_by_age_and_sex_and_location") - assert_not_nil match - assert !match.finder? - assert match.instantiator? - assert_equal :first, match.finder - assert_equal :create, match.instantiator - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_or_initialize_by - match = DynamicFinderMatch.match("find_or_initialize_by_age_and_sex_and_location") - assert_not_nil match - assert !match.finder? - assert match.instantiator? - assert_equal :first, match.finder - assert_equal :new, match.instantiator - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_no_match - assert_nil DynamicFinderMatch.match("not_a_finder") - end - - def find_by_bang - match = DynamicFinderMatch.match("find_by_age_and_sex_and_location!") - assert_not_nil match - assert match.finder? - assert match.bang? - assert_equal :first, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_by - match = DynamicFinderMatch.match("find_by_age_and_sex_and_location") - assert_not_nil match - assert match.finder? - assert_equal :first, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_by_with_symbol - m = DynamicFinderMatch.match(:find_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_all_by_with_symbol - m = DynamicFinderMatch.match(:find_all_by_foo) - assert_equal :all, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_all_by - match = DynamicFinderMatch.match("find_all_by_age_and_sex_and_location") - assert_not_nil match - assert match.finder? - assert_equal :all, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_last_by - m = DynamicFinderMatch.match(:find_last_by_foo) - assert_equal :last, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_by! - m = DynamicFinderMatch.match(:find_by_foo!) - assert_equal :first, m.finder - assert m.bang?, 'should be banging' - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_or_create - m = DynamicFinderMatch.match(:find_or_create_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - assert_equal :create, m.instantiator - end - - def test_find_or_initialize - m = DynamicFinderMatch.match(:find_or_initialize_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - assert_equal :new, m.instantiator - end - - def test_garbage - assert !DynamicFinderMatch.match(:fooo), 'should be false' - assert !DynamicFinderMatch.match(:find_by), 'should be false' - end - end -end diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index e118add44c..91e1df91cd 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -6,14 +6,14 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_if_available_queries_for_explain_is_nil with_queries(nil) do - SUBSCRIBER.call + SUBSCRIBER.finish(nil, nil, {}) assert_nil Thread.current[:available_queries_for_explain] end end def test_collects_nothing_if_the_payload_has_an_exception with_queries([]) do |queries| - SUBSCRIBER.call(:exception => Exception.new) + SUBSCRIBER.finish(nil, nil, :exception => Exception.new) assert queries.empty? end end @@ -21,7 +21,7 @@ if ActiveRecord::Base.connection.supports_explain? def test_collects_nothing_for_ignored_payloads with_queries([]) do |queries| ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip| - SUBSCRIBER.call(:name => ip) + SUBSCRIBER.finish(nil, nil, :name => ip) end assert queries.empty? end @@ -31,7 +31,7 @@ if ActiveRecord::Base.connection.supports_explain? sql = 'select 1 from users' binds = [1, 2] with_queries([]) do |queries| - SUBSCRIBER.call(:name => 'SQL', :sql => sql, :binds => binds) + SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => sql, :binds => binds) assert_equal 1, queries.size assert_equal sql, queries[0][0] assert_equal binds, queries[0][1] @@ -45,4 +45,4 @@ if ActiveRecord::Base.connection.supports_explain? Thread.current[:available_queries_for_explain] = nil end end -end
\ No newline at end of file +end diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 83c9b6e107..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 @@ -28,7 +28,9 @@ if ActiveRecord::Base.connection.supports_explain? original = base.logger base.logger = nil - base.logger.expects(:warn).never + class << base.logger + def warn; raise "Should not be called" end + end with_threshold(0) do car = Car.where(:name => 'honda').first @@ -43,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] @@ -56,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] @@ -66,6 +68,16 @@ if ActiveRecord::Base.connection.supports_explain? assert_equal [cars(:honda)], result end + def test_logging_query_plan_when_counting_by_sql + base.logger.expects(:warn).with do |message| + message.starts_with?('EXPLAIN for:') + end + + with_threshold(0) do + Car.count_by_sql "SELECT COUNT(*) FROM cars WHERE name = 'honda'" + end + end + def test_exec_explain_with_no_binds sqls = %w(foo bar) binds = [[], []] diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 235805a67c..9440cd429a 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -36,6 +36,11 @@ class FinderRespondToTest < ActiveRecord::TestCase assert_respond_to Topic, :find_by_title_and_author_name end + def test_should_respond_to_find_all_by_an_aliased_attribute + ensure_topic_method_is_not_cached(:find_by_heading) + assert_respond_to Topic, :find_by_heading + end + def test_should_respond_to_find_or_initialize_from_one_attribute ensure_topic_method_is_not_cached(:find_or_initialize_by_title) assert_respond_to Topic, :find_or_initialize_by_title @@ -56,6 +61,16 @@ class FinderRespondToTest < ActiveRecord::TestCase assert_respond_to Topic, :find_or_create_by_title_and_author_name end + def test_should_respond_to_find_or_create_from_one_attribute_bang + ensure_topic_method_is_not_cached(:find_or_create_by_title!) + assert_respond_to Topic, :find_or_create_by_title! + end + + def test_should_respond_to_find_or_create_from_two_attributes_bang + ensure_topic_method_is_not_cached(:find_or_create_by_title_and_author_name!) + assert_respond_to Topic, :find_or_create_by_title_and_author_name! + end + def test_should_not_respond_to_find_by_one_missing_attribute assert !Topic.respond_to?(:find_by_undertitle) end @@ -70,7 +85,6 @@ class FinderRespondToTest < ActiveRecord::TestCase private def ensure_topic_method_is_not_cached(method_id) - class << Topic; self; end.send(:remove_method, method_id) if Topic.public_methods.any? { |m| m.to_s == method_id.to_s } + class << Topic; self; end.send(:remove_method, method_id) if Topic.public_methods.include? method_id end - end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 76c041397a..20c8e8894d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -32,6 +32,7 @@ class FinderTest < ActiveRecord::TestCase assert Topic.exists?(:author_name => "Mary", :approved => true) assert Topic.exists?(["parent_id = ?", 1]) assert !Topic.exists?(45) + assert !Topic.exists?(Topic.new) begin assert !Topic.exists?("foo") @@ -44,10 +45,20 @@ class FinderTest < ActiveRecord::TestCase assert_raise(NoMethodError) { Topic.exists?([1,2]) } end + def test_exists_does_not_select_columns_without_alias + assert_sql(/SELECT\W+1 AS one FROM ["`]topics["`]/i) do + Topic.exists? + end + end + def test_exists_returns_true_with_one_record_and_no_args assert Topic.exists? end + def test_exists_returns_false_with_false_arg + assert !Topic.exists?(false) + end + # exists? should handle nil for id's that come from URLs and always return false # (example: Topic.exists?(params[:id])) where params[:id] is nil def test_exists_with_nil_arg @@ -62,7 +73,12 @@ class FinderTest < ActiveRecord::TestCase assert Topic.order(:id).uniq.exists? end - def test_does_not_exist_with_empty_table_and_no_args_given + def test_exists_with_includes_limit_and_empty_result + assert !Topic.includes(:replies).limit(0).exists? + assert !Topic.includes(:replies).limit(1).where('0 = 1').exists? + end + + def test_exists_with_empty_table_and_no_args_given Topic.delete_all assert !Topic.exists? end @@ -82,12 +98,6 @@ class FinderTest < ActiveRecord::TestCase Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) end - def test_exists_with_scoped_include - Developer.send(:with_scope, :find => { :include => :projects, :order => "projects.name" }) do - assert Developer.exists? - end - end - def test_exists_does_not_instantiate_records Developer.expects(:instantiate).never Developer.exists? @@ -104,14 +114,14 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_ids_with_limit_and_offset - assert_equal 2, Entrant.find([1,3,2], :limit => 2).size - assert_equal 1, Entrant.find([1,3,2], :limit => 3, :offset => 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.find :all - last_devs = Developer.find devs.map(&:id), :limit => 3, :offset => 9 + devs = Developer.all + last_devs = Developer.all.merge!(:limit => 3, :offset => 9).find devs.map(&:id) assert_equal 2, last_devs.size end @@ -119,62 +129,16 @@ class FinderTest < ActiveRecord::TestCase assert_equal [], Topic.find([]) end - def test_find_by_ids_missing_one - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) } - end - - def test_find_all_with_limit - assert_equal(2, Entrant.find(:all, :limit => 2).size) - assert_equal(0, Entrant.find(:all, :limit => 0).size) - end - - def test_find_all_with_prepared_limit_and_offset - entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 1) - - assert_equal(2, entrants.size) - assert_equal(entrants(:second).name, entrants.first.name) - - assert_equal 3, Entrant.count - entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 2) - assert_equal(1, entrants.size) - assert_equal(entrants(:third).name, entrants.first.name) - end - - def test_find_all_with_limit_and_offset_and_multiple_order_clauses - first_three_posts = Post.find :all, :order => 'author_id, id', :limit => 3, :offset => 0 - second_three_posts = Post.find :all, :order => ' author_id,id ', :limit => 3, :offset => 3 - third_three_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6 - last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 9 - - assert_equal [[0,3],[1,1],[1,2]], first_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[2,7],[2,9],[2,11]], third_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[3,8],[3,10]], last_posts.map { |p| [p.author_id, p.id] } - end - - - def test_find_with_group - developers = Developer.find(:all, :group => "salary", :select => "salary") - assert_equal 4, developers.size - assert_equal 4, developers.map(&:salary).uniq.size - end - - def test_find_with_group_and_having - developers = Developer.find(:all, :group => "salary", :having => "sum(salary) > 10000", :select => "salary") - assert_equal 3, developers.size - assert_equal 3, developers.map(&:salary).uniq.size - assert developers.all? { |developer| developer.salary > 10000 } + def test_find_doesnt_have_implicit_ordering + assert_sql(/^((?!ORDER).)*$/) { Topic.find(1) } end - def test_find_with_group_and_sanitized_having - developers = Developer.find(:all, :group => "salary", :having => ["sum(salary) > ?", 10000], :select => "salary") - assert_equal 3, developers.size - assert_equal 3, developers.map(&:salary).uniq.size - assert developers.all? { |developer| developer.salary > 10000 } + def test_find_by_ids_missing_one + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) } 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 } @@ -199,14 +163,24 @@ class FinderTest < ActiveRecord::TestCase assert_equal [Account], accounts.collect(&:class).uniq end - def test_find_first - first = Topic.find(:first, :conditions => "title = 'The First Topic'") - assert_equal(topics(:first).title, first.title) + def test_take + assert_equal topics(:first), Topic.take + end + + def test_take_failing + assert_nil Topic.where("title = 'This title does not exist'").take + end + + def test_take_bang_present + assert_nothing_raised do + assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").take! + end end - def test_find_first_failing - first = Topic.find(:first, :conditions => "title = 'The First Topic!'") - assert_nil(first) + def test_take_bang_missing + assert_raises ActiveRecord::RecordNotFound do + Topic.where("title = 'This title does not exist'").take! + end end def test_first @@ -229,6 +203,12 @@ class FinderTest < ActiveRecord::TestCase end end + def test_first_have_primary_key_order_by_default + expected = topics(:first) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.first + end + def test_model_class_responds_to_first_bang assert Topic.first! Topic.delete_all @@ -257,7 +237,8 @@ class FinderTest < ActiveRecord::TestCase end end - def test_first_and_last_with_integer_should_use_sql_limit + def test_take_and_first_and_last_with_integer_should_use_sql_limit + assert_sql(/LIMIT 3|ROWNUM <= 3/) { Topic.take(3).entries } assert_sql(/LIMIT 2|ROWNUM <= 2/) { Topic.first(2).entries } assert_sql(/LIMIT 5|ROWNUM <= 5/) { Topic.last(5).entries } end @@ -278,7 +259,8 @@ class FinderTest < ActiveRecord::TestCase assert_no_match(/LIMIT/, query.first) end - def test_first_and_last_with_integer_should_return_an_array + def test_take_and_first_and_last_with_integer_should_return_an_array + assert_kind_of Array, Topic.take(5) assert_kind_of Array, Topic.first(5) assert_kind_of Array, Topic.last(5) end @@ -292,7 +274,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_only_some_columns - topic = Topic.find(1, :select => "author_name") + 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 @@ -302,36 +284,24 @@ class FinderTest < ActiveRecord::TestCase assert_respond_to topic, "author_name" end - def test_find_on_blank_conditions - [nil, " ", [], {}].each do |blank| - assert_nothing_raised { Topic.find(:first, :conditions => blank) } - end - end - - def test_find_on_blank_bind_conditions - [ [""], ["",{}] ].each do |blank| - assert_nothing_raised { Topic.find(:first, :conditions => blank) } - end - end - def test_find_on_array_conditions - assert Topic.find(1, :conditions => ["approved = ?", false]) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) } + 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.find(1, :conditions => { :approved => false }) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) } + 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.find(1, :conditions => { 'topics.approved' => false }) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) } + 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.find(1, :conditions => {:topics => { :approved => false }}) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) } + 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 @@ -341,89 +311,89 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_hash_conditions_on_joined_table_and_with_range - firms = DependentFirm.all :joins => :account, :conditions => {: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.find(david.id, :conditions => { 'customers.name' => david.name, :address => david.address }) + assert Customer.where('customers.name' => david.name, :address => david.address).find(david.id) assert_raise(ActiveRecord::RecordNotFound) { - Customer.find(david.id, :conditions => { 'customers.name' => david.name + "1", :address => david.address }) + 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.find_all_by_post_id(authors(:david).posts).map(&:id).sort + 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.find(:all, :conditions => { :id => 1..2 }).map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :id => 2..3 }) } + 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.find(:all, :conditions => { :id => 1..3 }).map(&:id).sort - assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1...3 }).map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(3, :conditions => { :id => 2...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.find(:all, :conditions => { :id => 1..3, :post_id => 1..2 }).map(&:id).sort - assert_equal [1], Comment.find(:all, :conditions => { :id => 1..1, :post_id => 1..10 }).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.find(:all, :conditions => {:id => [1..2, 3, 5, 6..8, 9]}).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.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) } + 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.find(:first, :conditions => ["name = '%s'", "37signals"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).written_on + assert_kind_of Firm, Company.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_array_interpolation - assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).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.find(:first, :conditions => { :name => "37signals"}) - assert_nil Company.find(:first, :conditions => { :name => "37signals!"}) - assert_kind_of Time, Topic.find(:first, :conditions => {:id => 1}).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.find(:first, :conditions => { :id => 2, :dhh => true }) + 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.find(:first, :conditions => { :name => "Ain't noth'n like' \#stuff" }) + assert Company.all.merge!(:where => { :name => "Ain't noth'n like' \#stuff" }).first end def test_hash_condition_find_with_array - p1, p2 = Post.find(:all, :limit => 2, :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => { :id => [p1, p2] }, :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => { :id => [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 => [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.find(:first, :conditions => { :last_read => nil } ) + topic = Topic.all.merge!(:where => { :last_read => nil } ).first assert_not_nil topic assert_nil topic.last_read end @@ -431,42 +401,42 @@ class FinderTest < ActiveRecord::TestCase def test_hash_condition_find_with_aggregate_having_one_mapping balance = customers(:david).balance assert_kind_of Money, balance - found_customer = Customer.find(:first, :conditions => {:balance => 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.find(:first, :conditions => {:gps_location => 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.find(:first, :conditions => {:balance => balance.amount}) + 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.find(:first, :conditions => {:gps_location => gps_location.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.find(:first, :conditions => {: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.find(:first, :conditions => {:address => address, :name => customers(:david).name}) + found_customer = Customer.where(:address => address, :name => customers(:david).name).first assert_equal customers(:david), found_customer end @@ -474,7 +444,7 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :local do topic = Topic.first - assert_equal topic, Topic.find(:first, :conditions => ['written_on = ?', topic.written_on.getutc]) + assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getutc]).first end end end @@ -483,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.find(:first, :conditions => {:written_on => topic.written_on.getutc}) + assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getutc}).first end end end @@ -492,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.find(:first, :conditions => ['written_on = ?', topic.written_on.getlocal]) + assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getlocal]).first end end end @@ -501,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.find(:first, :conditions => {:written_on => topic.written_on.getlocal}) + 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.find(:first, :conditions => ["name = ?", "37signals"]) - assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"]) - assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!' OR 1=1"]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = ?", 1]).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.find(:first, :conditions => ["id=? AND name = ?", 2]) + Company.all.merge!(:where => ["id=? AND name = ?", 2]).first } assert_raise(ActiveRecord::PreparedStatementInvalid) { - Company.find(:first, :conditions => ["id=?", 2, 3, 4]) + 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.find(:first, :conditions => ["name = ?", "37signals' go'es agains"]) + 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.find(:first, :conditions => ["name = :name", {:name => "37signals' go'es agains"}]) + assert Company.all.merge!(:where => ["name = :name", {:name => "37signals' go'es agains"}]).first end def test_bind_arity @@ -544,10 +514,10 @@ class FinderTest < ActiveRecord::TestCase assert_nothing_raised { bind("'+00:00'", :foo => "bar") } - assert_kind_of Firm, Company.find(:first, :conditions => ["name = :name", { :name => "37signals" }]) - assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!" }]) - assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!' OR 1=1" }]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = :id", { :id => 1 }]).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 @@ -618,12 +588,6 @@ class FinderTest < ActiveRecord::TestCase assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table") end - def test_count - assert_equal(0, Entrant.count(:conditions => "id > 3")) - assert_equal(1, Entrant.count(:conditions => ["id > ?", 2])) - assert_equal(2, Entrant.count(:conditions => ["id > ?", 1])) - end - def test_count_by_sql assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3")) assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2])) @@ -640,13 +604,13 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") } end - def test_find_by_one_attribute_with_order_option - assert_equal accounts(:signals37), Account.find_by_credit_limit(50, :order => 'id') - assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :order => 'id DESC') + def test_find_by_one_attribute_that_is_an_alias + assert_equal topics(:first), Topic.find_by_heading("The First Topic") + assert_nil Topic.find_by_heading("The First Topic!") end def test_find_by_one_attribute_with_conditions - assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) + 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 @@ -685,13 +649,13 @@ class FinderTest < ActiveRecord::TestCase def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching # ensure this test can run independently of order - class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' } - a = Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) - assert_equal a, Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) # find_by_credit_limit has been cached + class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.include?(:find_by_credit_limit) + a = Account.where('firm_id = ?', 6).find_by_credit_limit(50) + assert_equal a, Account.where('firm_id = ?', 6).find_by_credit_limit(50) # find_by_credit_limit has been cached end def test_find_by_one_attribute_with_several_options - assert_equal accounts(:unknown), Account.find_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3]) + assert_equal accounts(:unknown), Account.order('id DESC').where('id != ?', 3).find_by_credit_limit(50) end def test_find_by_one_missing_attribute @@ -714,345 +678,26 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") } 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") - end - - def test_find_last_by_invalid_method_syntax - assert_raise(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") } - assert_raise(NoMethodError) { Topic.find_last_by_title?("The First Topic") } - end - - def test_find_last_by_one_attribute_with_several_options - assert_equal accounts(:signals37), Account.find_last_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3]) - end - - def test_find_last_by_one_missing_attribute - assert_raise(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") } - end - - def test_find_last_by_two_attributes - topic = Topic.last - assert_equal topic, Topic.find_last_by_title_and_author_name(topic.title, topic.author_name) - assert_nil Topic.find_last_by_title_and_author_name(topic.title, "Anonymous") - end - - 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 - 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 - 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 - assert_equal loaded_last, unloaded_last - end - - def test_find_all_by_one_attribute - topics = Topic.find_all_by_content("Have a nice day") - assert_equal 2, topics.size - assert topics.include?(topics(:first)) - - assert_equal [], Topic.find_all_by_title("The First Topic!!") - end - - def test_find_all_by_one_attribute_which_is_a_symbol - topics = Topic.find_all_by_content("Have a nice day".to_sym) - assert_equal 2, topics.size - assert topics.include?(topics(:first)) - - 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 - - topics = Topic.find_all_by_content("Have a nice day", :order => "id") - assert_equal topics(:first), topics.first - end - - def test_find_all_by_array_attribute - assert_equal 2, Topic.find_all_by_title(["The First Topic", "The Second Topic of the day"]).size - end - - def test_find_all_by_boolean_attribute - topics = Topic.find_all_by_approved(false) - assert_equal 1, topics.size - assert topics.include?(topics(:first)) - - topics = Topic.find_all_by_approved(true) - assert_equal 3, topics.size - assert topics.include?(topics(:second)) - end - def test_find_by_nil_attribute topic = Topic.find_by_last_read nil assert_not_nil topic assert_nil topic.last_read end - def test_find_all_by_nil_attribute - topics = Topic.find_all_by_last_read nil - assert_equal 3, topics.size - assert topics.collect(&:last_read).all?(&:nil?) - end - def test_find_by_nil_and_not_nil_attributes topic = Topic.find_by_last_read_and_author_name nil, "Mary" assert_equal "Mary", topic.author_name end - def test_find_all_by_nil_and_not_nil_attributes - topics = Topic.find_all_by_last_read_and_author_name nil, "Mary" - assert_equal 1, topics.size - assert_equal "Mary", topics[0].author_name - end - - def test_find_or_create_from_one_attribute - number_of_companies = Company.count - sig38 = Company.find_or_create_by_name("38signals") - assert_equal number_of_companies + 1, Company.count - assert_equal sig38, Company.find_or_create_by_name("38signals") - assert sig38.persisted? - end - - def test_find_or_create_from_two_attributes - number_of_topics = Topic.count - another = Topic.find_or_create_by_title_and_author_name("Another topic","John") - assert_equal number_of_topics + 1, Topic.count - assert_equal another, Topic.find_or_create_by_title_and_author_name("Another topic", "John") - assert another.persisted? - 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}) - assert_equal number_of_companies + 1, Company.count - assert_equal sig38, Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) - assert sig38.persisted? - assert_equal "38signals", sig38.name - assert_equal 17, sig38.firm_id - assert_equal 23, sig38.client_of - end - - def test_find_or_create_from_two_attributes_and_hash - number_of_companies = Company.count - sig38 = Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23}) - assert_equal number_of_companies + 1, Company.count - assert_equal sig38, Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23}) - assert sig38.persisted? - assert_equal "38signals", sig38.name - assert_equal 17, sig38.firm_id - 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 - assert_not_equal 1000, c.rating - assert c.valid? - assert !c.persisted? - end - - def test_find_or_create_from_one_attribute_should_not_set_attribute_even_when_protected - c = Company.find_or_create_by_name({:name => "Fortune 1000", :rating => 1000}) - assert_equal "Fortune 1000", c.name - assert_not_equal 1000, c.rating - assert c.valid? - assert c.persisted? - end - - def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected - c = Company.find_or_initialize_by_name_and_rating("Fortune 1000", 1000) - assert_equal "Fortune 1000", c.name - assert_equal 1000, c.rating - assert c.valid? - assert !c.persisted? - end - - def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected - c = Company.find_or_create_by_name_and_rating("Fortune 1000", 1000) - assert_equal "Fortune 1000", c.name - assert_equal 1000, c.rating - assert c.valid? - assert c.persisted? - end - - def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash - c = Company.find_or_initialize_by_rating(1000, {:name => "Fortune 1000"}) - assert_equal "Fortune 1000", c.name - assert_equal 1000, c.rating - assert c.valid? - assert !c.persisted? - end - - def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash - c = Company.find_or_create_by_rating(1000, {:name => "Fortune 1000"}) - assert_equal "Fortune 1000", c.name - assert_equal 1000, c.rating - assert c.valid? - assert c.persisted? - end - - def test_find_or_initialize_should_set_protected_attributes_if_given_as_block - c = Company.find_or_initialize_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } - assert_equal "Fortune 1000", c.name - assert_equal 1000.to_f, c.rating.to_f - assert c.valid? - assert !c.persisted? - end - - def test_find_or_create_should_set_protected_attributes_if_given_as_block - c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } - assert_equal "Fortune 1000", c.name - assert_equal 1000.to_f, c.rating.to_f - assert c.valid? - assert c.persisted? - end - - def test_find_or_create_should_work_with_block_on_first_call - class << Company - undef_method(:find_or_create_by_name) if method_defined?(:find_or_create_by_name) - end - c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } - assert_equal "Fortune 1000", c.name - assert_equal 1000.to_f, c.rating.to_f - assert c.valid? - assert c.persisted? - end - - def test_find_or_initialize_from_two_attributes - another = Topic.find_or_initialize_by_title_and_author_name("Another topic","John") - assert_equal "Another topic", another.title - assert_equal "John", another.author_name - assert !another.persisted? - end - - def test_find_or_initialize_from_two_attributes_but_passing_only_one - 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 - assert_equal 17, sig38.firm_id - assert_equal 23, sig38.client_of - 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_with_bad_sql assert_raise(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" } end - def test_find_with_invalid_params - assert_raise(ArgumentError) { Topic.find :first, :join => "It should be `joins'" } - assert_raise(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" } - end - - def test_dynamic_finder_with_invalid_params - assert_raise(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" } - end - def test_find_all_with_join - developers_on_project_one = Developer.find( - :all, + developers_on_project_one = Developer.all.merge!( :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', - :conditions => 'project_id=1' - ) + :where => 'project_id=1' + ).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') @@ -1060,17 +705,15 @@ class FinderTest < ActiveRecord::TestCase end def test_joins_dont_clobber_id - first = Firm.find( - :first, + first = Firm.all.merge!( :joins => 'INNER JOIN companies clients ON clients.firm_id = companies.id', - :conditions => 'companies.id = 1' - ) + :where => 'companies.id = 1' + ).first assert_equal 1, first.id end def test_joins_with_string_array - person_with_reader_and_post = Post.find( - :all, + 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'" @@ -1081,15 +724,14 @@ class FinderTest < ActiveRecord::TestCase def test_find_by_id_with_conditions_with_or assert_nothing_raised do - Post.find([1,2,3], - :conditions => "posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'") + Post.where("posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'").find([1,2,3]) end end # http://dev.rubyonrails.org/ticket/6778 def test_find_ignores_previously_inserted_record Post.create!(:title => 'test', :body => 'it out') - assert_equal [], Post.find_all_by_id(nil) + assert_equal [], Post.where(id: nil) end def test_find_by_empty_ids @@ -1097,13 +739,13 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_empty_in_condition - assert_equal [], Post.find(:all, :conditions => ['id in (?)', []]) + assert_equal [], Post.where('id in (?)', []) end def test_find_by_records - p1, p2 = Post.find(:all, :limit => 2, :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => ['id in (?)', [p1, p2]], :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => ['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 @@ -1130,16 +772,15 @@ 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.find(:all, :include => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).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.find(:all, :include => { :author => :author_address, :authors => :author_address}, - :order => 'author_addresses_authors.id DESC ', :limit => 3).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.find( - :all, - :conditions => { + client_of = Company.all.merge!( + :where => { :client_of => [2, 1, nil], :name => ['37signals', 'Summit', 'Microsoft'] }, :order => 'client_of DESC' @@ -1150,9 +791,8 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_nil_inside_set_passed_for_attribute - client_of = Company.find( - :all, - :conditions => { :client_of => [nil] }, + client_of = Company.all.merge!( + :where => { :client_of => [nil] }, :order => 'client_of DESC' ).map { |x| x.client_of } @@ -1160,22 +800,14 @@ class FinderTest < ActiveRecord::TestCase end def test_with_limiting_with_custom_select - posts = Post.find( - :all, :include => :author, :select => ' posts.*, authors.id as "author_id"', - :references => :authors, :limit => 3, :order => 'posts.id' - ) + posts = Post.references(:authors).merge( + :includes => :author, :select => ' posts.*, authors.id as "author_id"', + :limit => 3, :order => 'posts.id' + ).to_a assert_equal 3, posts.size assert_equal [0, 1, 1], posts.map(&:author_id).sort end - def test_finder_with_scoped_from - all_topics = Topic.find(:all) - - Topic.send(:with_scope, :find => { :from => 'fake_topics' }) do - assert_equal all_topics, Topic.from('topics').to_a - end - end - def test_find_one_message_with_custom_primary_key Toy.primary_key = :name begin @@ -1185,6 +817,10 @@ class FinderTest < ActiveRecord::TestCase end end + def test_finder_with_offset_string + assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.all.merge!(:offset => "3").to_a } + end + protected def bind(statement, *vars) if vars.first.is_a?(Hash) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 562b370c97..c28f8de682 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -514,7 +514,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase self.use_transactional_fixtures = false def test_raises_error - assert_raise FixtureClassNotFound do + assert_raise ActiveRecord::FixtureClassNotFound do funny_jokes(:a_joke) end end @@ -716,7 +716,7 @@ class FoxyFixturesTest < ActiveRecord::TestCase end def test_only_generates_a_pk_if_necessary - m = Matey.find(:first) + m = Matey.first m.pirate = pirates(:blackbeard) m.target = pirates(:redbeard) end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 9f5f012073..4c6d4666ed 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -2,12 +2,12 @@ require File.expand_path('../../../../load_paths', __FILE__) require 'config' +gem 'minitest' require 'minitest/autorun' require 'stringio' -require 'mocha' -require 'cases/test_case' require 'active_record' +require 'cases/test_case' require 'active_support/dependencies' require 'active_support/logger' @@ -19,12 +19,6 @@ require 'support/connection' # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true -# Enable Identity Map only when ENV['IM'] is set to "true" -ActiveRecord::IdentityMap.enabled = (ENV['IM'] == "true") - -# 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 @@ -39,7 +33,7 @@ def current_adapter?(*types) end def in_memory_db? - current_adapter?(:SQLiteAdapter) && + current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:" end @@ -83,8 +77,8 @@ class ActiveSupport::TestCase self.use_instantiated_fixtures = false self.use_transactional_fixtures = true - def create_fixtures(*table_names, &block) - ActiveRecord::Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, table_names, fixture_class_names, &block) + def create_fixtures(*fixture_set_names, &block) + ActiveRecord::Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block) end end @@ -123,3 +117,19 @@ class << Time @now = nil end end + +module LogIntercepter + attr_accessor :logged, :intercepted + def self.extended(base) + base.logged = [] + end + def log(sql, name, binds = [], &block) + if @intercepted + @logged << [sql, name, binds] + yield + else + super(sql, name,binds, &block) + end + end +end + diff --git a/activerecord/test/cases/identity_map/middleware_test.rb b/activerecord/test/cases/identity_map/middleware_test.rb deleted file mode 100644 index 5b32a1c6d2..0000000000 --- a/activerecord/test/cases/identity_map/middleware_test.rb +++ /dev/null @@ -1,74 +0,0 @@ -require "cases/helper" -require "rack" - -module ActiveRecord - module IdentityMap - class MiddlewareTest < ActiveRecord::TestCase - def setup - super - @enabled = IdentityMap.enabled - IdentityMap.enabled = false - end - - def teardown - super - IdentityMap.enabled = @enabled - IdentityMap.clear - end - - def test_delegates - called = false - mw = Middleware.new lambda { |env| - called = true - [200, {}, nil] - } - mw.call({}) - assert called, 'middleware delegated' - end - - def test_im_enabled_during_delegation - mw = Middleware.new lambda { |env| - assert IdentityMap.enabled?, 'identity map should be enabled' - [200, {}, nil] - } - mw.call({}) - end - - class Enum < Struct.new(:iter) - def each(&b) - iter.call(&b) - end - end - - def test_im_enabled_during_body_each - mw = Middleware.new lambda { |env| - [200, {}, Enum.new(lambda { |&b| - assert IdentityMap.enabled?, 'identity map should be enabled' - b.call "hello" - })] - } - body = mw.call({}).last - body.each { |x| assert_equal 'hello', x } - end - - def test_im_disabled_after_body_close - mw = Middleware.new lambda { |env| [200, {}, []] } - body = mw.call({}).last - assert IdentityMap.enabled?, 'identity map should be enabled' - body.close - assert !IdentityMap.enabled?, 'identity map should be disabled' - end - - def test_im_cleared_after_body_close - mw = Middleware.new lambda { |env| [200, {}, []] } - body = mw.call({}).last - - IdentityMap.repository['hello'] = 'world' - assert !IdentityMap.repository.empty?, 'repo should not be empty' - - body.close - assert IdentityMap.repository.empty?, 'repo should be empty' - end - end - end -end diff --git a/activerecord/test/cases/identity_map_test.rb b/activerecord/test/cases/identity_map_test.rb deleted file mode 100644 index 3efc8bf559..0000000000 --- a/activerecord/test/cases/identity_map_test.rb +++ /dev/null @@ -1,439 +0,0 @@ -require "cases/helper" - -require 'models/developer' -require 'models/project' -require 'models/company' -require 'models/topic' -require 'models/reply' -require 'models/computer' -require 'models/customer' -require 'models/order' -require 'models/post' -require 'models/author' -require 'models/tag' -require 'models/tagging' -require 'models/comment' -require 'models/sponsor' -require 'models/member' -require 'models/essay' -require 'models/subscriber' -require "models/pirate" -require "models/bird" -require "models/parrot" - -if ActiveRecord::IdentityMap.enabled? -class IdentityMapTest < ActiveRecord::TestCase - fixtures :accounts, :companies, :developers, :projects, :topics, - :developers_projects, :computers, :authors, :author_addresses, - :posts, :tags, :taggings, :comments, :subscribers - - ############################################################################## - # Basic tests checking if IM is functioning properly on basic find operations# - ############################################################################## - - def test_find_id - assert_same(Client.find(3), Client.find(3)) - end - - def test_find_id_without_identity_map - ActiveRecord::IdentityMap.without do - assert_not_same(Client.find(3), Client.find(3)) - end - end - - def test_find_id_use_identity_map - ActiveRecord::IdentityMap.enabled = false - ActiveRecord::IdentityMap.use do - assert_same(Client.find(3), Client.find(3)) - end - ActiveRecord::IdentityMap.enabled = true - end - - def test_find_pkey - assert_same( - Subscriber.find('swistak'), - Subscriber.find('swistak') - ) - end - - def test_find_by_id - assert_same( - Client.find_by_id(3), - Client.find_by_id(3) - ) - end - - def test_find_by_string_and_numeric_id - assert_same( - Client.find_by_id("3"), - Client.find_by_id(3) - ) - end - - def test_find_by_pkey - assert_same( - Subscriber.find_by_nick('swistak'), - Subscriber.find_by_nick('swistak') - ) - end - - def test_find_first_id - assert_same( - Client.find(:first, :conditions => {:id => 1}), - Client.find(:first, :conditions => {:id => 1}) - ) - end - - def test_find_first_pkey - assert_same( - Subscriber.find(:first, :conditions => {:nick => 'swistak'}), - Subscriber.find(:first, :conditions => {:nick => 'swistak'}) - ) - end - - def test_queries_are_not_executed_when_finding_by_id - Post.find 1 - assert_no_queries do - Post.find 1 - end - end - - ############################################################################## - # Tests checking if IM is functioning properly on more advanced finds # - # and associations # - ############################################################################## - - def test_owner_object_is_associated_from_identity_map - post = Post.find(1) - comment = post.comments.first - - assert_no_queries do - comment.post - end - assert_same post, comment.post - end - - def test_associated_object_are_assigned_from_identity_map - post = Post.find(1) - - post.comments.each do |comment| - assert_same post, comment.post - assert_equal post.object_id, comment.post.object_id - end - end - - def test_creation - t1 = Topic.create("title" => "t1") - t2 = Topic.find(t1.id) - assert_same(t1, t2) - end - - ############################################################################## - # Tests checking if IM is functioning properly on classes with multiple # - # types of inheritance # - ############################################################################## - - def test_inherited_without_type_attribute_without_identity_map - ActiveRecord::IdentityMap.without do - p1 = DestructivePirate.create!(:catchphrase => "I'm not a regular Pirate") - p2 = Pirate.find(p1.id) - assert_not_same(p1, p2) - end - end - - def test_inherited_with_type_attribute_without_identity_map - ActiveRecord::IdentityMap.without do - c = comments(:sub_special_comment) - c1 = SubSpecialComment.find(c.id) - c2 = Comment.find(c.id) - assert_same(c1.class, c2.class) - end - end - - def test_inherited_without_type_attribute - p1 = DestructivePirate.create!(:catchphrase => "I'm not a regular Pirate") - p2 = Pirate.find(p1.id) - assert_not_same(p1, p2) - end - - def test_inherited_with_type_attribute - c = comments(:sub_special_comment) - c1 = SubSpecialComment.find(c.id) - c2 = Comment.find(c.id) - assert_same(c1, c2) - end - - ############################################################################## - # Tests checking dirty attribute behavior with IM # - ############################################################################## - - def test_loading_new_instance_should_not_update_dirty_attributes - swistak = Subscriber.find(:first, :conditions => {:nick => 'swistak'}) - swistak.name = "Swistak Sreberkowiec" - assert_equal(["name"], swistak.changed) - assert_equal({"name" => ["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) - - assert swistak.name_changed? - assert_equal("Swistak Sreberkowiec", swistak.name) - end - - def test_loading_new_instance_should_change_dirty_attribute_original_value - swistak = Subscriber.find(:first, :conditions => {:nick => 'swistak'}) - swistak.name = "Swistak Sreberkowiec" - - Subscriber.update_all({:name => "Raczkowski Marcin"}, {:name => "Marcin Raczkowski"}) - - assert_equal({"name"=>["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) - assert_equal("Swistak Sreberkowiec", swistak.name) - end - - def test_loading_new_instance_should_remove_dirt - swistak = Subscriber.find(:first, :conditions => {:nick => 'swistak'}) - swistak.name = "Swistak Sreberkowiec" - - assert_equal({"name" => ["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) - - Subscriber.update_all({:name => "Swistak Sreberkowiec"}, {:name => "Marcin Raczkowski"}) - - assert_equal("Swistak Sreberkowiec", swistak.name) - assert_equal({"name"=>["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) - assert swistak.name_changed? - end - - def test_has_many_associations - pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?") - pirate.birds.create!(:name => 'Posideons Killer') - pirate.birds.create!(:name => 'Killer bandita Dionne') - - posideons, _ = pirate.birds - - pirate.reload - - pirate.birds_attributes = [{ :id => posideons.id, :name => 'Grace OMalley' }] - assert_equal 'Grace OMalley', pirate.birds.to_a.find { |r| r.id == posideons.id }.name - end - - def test_changing_associations - post1 = Post.create("title" => "One post", "body" => "Posting...") - post2 = Post.create("title" => "Another post", "body" => "Posting... Again...") - comment = Comment.new("body" => "comment") - - comment.post = post1 - assert comment.save - - assert_same(post1.comments.first, comment) - - comment.post = post2 - assert comment.save - - assert_same(post2.comments.first, comment) - assert_equal(0, post1.comments.size) - end - - def test_im_with_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins - tag = posts(:welcome).tags.first - tag_with_joins_and_select = posts(:welcome).tags.add_joins_and_select.first - assert_same(tag, tag_with_joins_and_select) - assert_nothing_raised(NoMethodError, "Joins/select was not loaded") { tag.author_id } - end - - ############################################################################## - # Tests checking Identity Map behavior with preloaded associations, joins, # - # includes etc. # - ############################################################################## - - def test_find_with_preloaded_associations - assert_queries(2) do - posts = Post.preload(:comments).order('posts.id') - assert posts.first.comments.first - end - - # With IM we'll retrieve post object from previous query, it'll have comments - # already preloaded from first call - assert_queries(1) do - posts = Post.preload(:comments).order('posts.id') - assert posts.first.comments.first - end - - assert_queries(2) do - posts = Post.preload(:author).order('posts.id') - assert posts.first.author - end - - # With IM we'll retrieve post object from previous query, it'll have comments - # already preloaded from first call - assert_queries(1) do - posts = Post.preload(:author).order('posts.id') - assert posts.first.author - end - - assert_queries(1) do - posts = Post.preload(:author, :comments).order('posts.id') - assert posts.first.author - assert posts.first.comments.first - end - end - - def test_find_with_included_associations - assert_queries(2) do - posts = Post.includes(:comments).order('posts.id') - assert posts.first.comments.first - end - - assert_queries(1) do - posts = Post.scoped.includes(:comments).order('posts.id') - assert posts.first.comments.first - end - - assert_queries(2) do - posts = Post.includes(:author).order('posts.id') - assert posts.first.author - end - - assert_queries(1) do - posts = Post.includes(:author, :comments).order('posts.id') - assert posts.first.author - assert posts.first.comments.first - end - end - - def test_eager_loading_with_conditions_on_joined_table_preloads - posts = Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} - assert_same posts.first.author, Author.first - - posts = Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} - assert_same posts.first.author, Author.first - - posts = Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'", :order => 'posts.id') - assert_equal posts(:welcome, :thinking), posts - assert_same posts.first.author, Author.first - - posts = Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2", :order => 'posts.id') - assert_equal posts(:welcome, :thinking), posts - assert_same posts.first.author, Author.first - end - - def test_eager_loading_with_conditions_on_string_joined_table_preloads - posts = assert_queries(2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') - end - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} - - posts = assert_queries(1) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') - end - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} - end - - ############################################################################## - # Behaviour related to saving failures - ############################################################################## - - def test_reload_object_if_save_failed - developer = Developer.first - developer.salary = 0 - - assert !developer.save - - same_developer = Developer.first - - assert_not_same developer, same_developer - assert_not_equal 0, same_developer.salary - assert_not_equal developer.salary, same_developer.salary - end - - def test_reload_object_if_forced_save_failed - developer = Developer.first - developer.salary = 0 - - assert_raise(ActiveRecord::RecordInvalid) { developer.save! } - - same_developer = Developer.first - - assert_not_same developer, same_developer - assert_not_equal 0, same_developer.salary - assert_not_equal developer.salary, same_developer.salary - end - - def test_reload_object_if_update_attributes_fails - developer = Developer.first - developer.salary = 0 - - assert !developer.update_attributes(:salary => 0) - - same_developer = Developer.first - - assert_not_same developer, same_developer - assert_not_equal 0, same_developer.salary - assert_not_equal developer.salary, same_developer.salary - end - - ############################################################################## - # Behaviour of readonly, frozen, destroyed - ############################################################################## - - def test_find_using_identity_map_respects_readonly_when_loading_associated_object_first - author = Author.first - readonly_comment = author.readonly_comments.first - - comment = Comment.first - assert !comment.readonly? - - assert readonly_comment.readonly? - - assert_raise(ActiveRecord::ReadOnlyRecord) {readonly_comment.save} - assert comment.save - end - - def test_find_using_identity_map_respects_readonly - comment = Comment.first - assert !comment.readonly? - - author = Author.first - readonly_comment = author.readonly_comments.first - - assert readonly_comment.readonly? - - assert_raise(ActiveRecord::ReadOnlyRecord) {readonly_comment.save} - assert comment.save - end - - def test_find_using_select_and_identity_map - author_id, author = Author.select('id').first, Author.first - - assert_equal author_id, author - assert_same author_id, author - assert_not_nil author.name - - post, post_id = Post.first, Post.select('id').first - - assert_equal post_id, post - assert_same post_id, post - assert_not_nil post.title - end - -# Currently AR is not allowing changing primary key (see Persistence#update) -# So we ignore it. If this changes, this test needs to be uncommented. -# def test_updating_of_pkey -# assert client = Client.find(3), -# client.update_attribute(:id, 666) -# -# assert Client.find(666) -# assert_same(client, Client.find(666)) -# -# s = Subscriber.find_by_nick('swistak') -# assert s.update_attribute(:nick, 'swistakTheJester') -# assert_equal('swistakTheJester', s.nick) -# -# assert stj = Subscriber.find_by_nick('swistakTheJester') -# assert_same(s, stj) -# end - -end -end diff --git a/activerecord/test/cases/inclusion_test.rb b/activerecord/test/cases/inclusion_test.rb index 8726ba5e51..8f095e4953 100644 --- a/activerecord/test/cases/inclusion_test.rb +++ b/activerecord/test/cases/inclusion_test.rb @@ -4,7 +4,7 @@ require 'models/teapot' class BasicInclusionModelTest < ActiveRecord::TestCase def test_basic_model Teapot.create!(:name => "Ronnie Kemper") - assert_equal "Ronnie Kemper", Teapot.find(1).name + assert_equal "Ronnie Kemper", Teapot.first.name end def test_initialization @@ -84,8 +84,10 @@ class InclusionUnitTest < ActiveRecord::TestCase end def test_deprecation_proxy - assert_equal ActiveRecord::Model.name, ActiveRecord::Model::DeprecationProxy.name - assert_equal ActiveRecord::Base.superclass, assert_deprecated { ActiveRecord::Model::DeprecationProxy.superclass } + proxy = ActiveRecord::Model::DeprecationProxy.new + + assert_equal ActiveRecord::Model.name, proxy.name + assert_equal ActiveRecord::Base.superclass, assert_deprecated { proxy.superclass } sup, sup2 = nil, nil ActiveSupport.on_load(:__test_active_record_model_deprecation) do @@ -93,11 +95,29 @@ class InclusionUnitTest < ActiveRecord::TestCase sup2 = send(:superclass) end assert_deprecated do - ActiveSupport.run_load_hooks(:__test_active_record_model_deprecation, ActiveRecord::Model::DeprecationProxy) + ActiveSupport.run_load_hooks(:__test_active_record_model_deprecation, proxy) end assert_equal ActiveRecord::Base.superclass, sup assert_equal ActiveRecord::Base.superclass, sup2 end + + test "including in deprecation proxy" do + model, base = ActiveRecord::Model.dup, ActiveRecord::Base.dup + proxy = ActiveRecord::Model::DeprecationProxy.new(model, base) + + mod = Module.new + proxy.include mod + assert model < mod + end + + test "extending in deprecation proxy" do + model, base = ActiveRecord::Model.dup, ActiveRecord::Base.dup + proxy = ActiveRecord::Model::DeprecationProxy.new(model, base) + + mod = Module.new + assert_deprecated { proxy.extend mod } + assert base.singleton_class < mod + end end class InclusionFixturesTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 02df464469..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 @@ -15,7 +19,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_class_with_blank_sti_name - company = Company.find(:first) + company = Company.first company = company.dup company.extend(Module.new { def read_attribute(name) @@ -24,7 +28,7 @@ class InheritanceTest < ActiveRecord::TestCase end }) company.save! - company = Company.find(: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.find(:all, :order => 'id') + 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.find(: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.find(:all, :order => "id").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.find(:first, :conditions => "name = '37signals'") - assert_kind_of Firm, Firm.find(:first, :conditions => "name = '37signals'") - assert_nil Client.find(:first, :conditions => "name = '37signals'") + 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.find(:first, :conditions => "name = 'veryspecial'") - assert_equal very_special_client, Company.find(:first, :conditions => "name = 'veryspecial'") - assert_equal very_special_client, Client.find(:first, :conditions => "name = 'veryspecial'") - assert_equal 1, Client.find(:all, :conditions => "name = 'Summit'").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.find(1, :include => :firm) + 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.find(1, :include => :firm) + 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.find(:all, :order => 'id').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 @@ -259,8 +287,8 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase def test_instantiation_doesnt_try_to_require_corresponding_file ActiveRecord::Base.store_full_sti_class = false - foo = Firm.find(:first).clone - foo.ruby_type = foo.type = 'FirmOnTheFly' + foo = Firm.first.clone + foo.type = 'FirmOnTheFly' foo.save! # Should fail without FirmOnTheFly in the type condition. diff --git a/activerecord/test/cases/invalid_date_test.rb b/activerecord/test/cases/invalid_date_test.rb index 98cda010ae..426a350379 100644 --- a/activerecord/test/cases/invalid_date_test.rb +++ b/activerecord/test/cases/invalid_date_test.rb @@ -7,8 +7,6 @@ class InvalidDateTest < ActiveRecord::TestCase invalid_dates = [[2007, 11, 31], [1993, 2, 29], [2007, 2, 29]] - topic = Topic.new - valid_dates.each do |date_src| topic = Topic.new("last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s) # Oracle DATE columns are datetime columns and Oracle adapter returns Time value diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index d9e350abc0..a86b165c78 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -83,6 +83,53 @@ class JsonSerializationTest < ActiveRecord::TestCase assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json end + def test_uses_serializable_hash_with_only_option + def @contact.serializable_hash(options=nil) + super(only: %w(name)) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{awesome}, json + assert_no_match %r{age}, json + end + + def test_uses_serializable_hash_with_except_option + def @contact.serializable_hash(options=nil) + super(except: %w(age)) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"awesome":true}, json + assert_no_match %r{age}, json + end + + def test_does_not_include_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{type}, json + assert_no_match %r{ContactSti}, json + end + + def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + def @contact.serializable_hash(options={}) + super({ except: %w(age) }.merge!(options)) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{age}, json + assert_no_match %r{type}, json + assert_no_match %r{ContactSti}, json + end + def test_serializable_hash_should_not_modify_options_in_argument options = { :only => :name } @contact.serializable_hash(options) diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb index 75e5dfa49b..0b78f2e46b 100644 --- a/activerecord/test/cases/lifecycle_test.rb +++ b/activerecord/test/cases/lifecycle_test.rb @@ -137,7 +137,7 @@ class LifecycleTest < ActiveRecord::TestCase def test_auto_observer topic_observer = TopicaAuditor.instance assert_nil TopicaAuditor.observed_class - assert_equal [Topic], TopicaAuditor.instance.observed_classes.to_a + assert_equal [Topic], TopicaAuditor.observed_classes.to_a topic = Topic.find(1) assert_equal topic.title, topic_observer.topic.title diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 807274ca67..2392516395 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -3,12 +3,14 @@ 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' require 'models/car' require 'models/engine' require 'models/wheel' +require 'models/treasure' class LockWithoutDefault < ActiveRecord::Base; end @@ -17,12 +19,12 @@ 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 - fixtures :people, :legacy_things, :references, :string_key_objects + fixtures :people, :legacy_things, :references, :string_key_objects, :peoples_treasures def test_non_integer_lock_existing s1 = StringKeyObject.find("record1") @@ -199,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 @@ -230,15 +232,24 @@ class OptimisticLockingTest < ActiveRecord::TestCase def test_polymorphic_destroy_with_dependencies_and_lock_version car = Car.create! - + assert_difference 'car.wheels.count' do car.wheels << Wheel.create! - end + end assert_difference 'car.wheels.count', -1 do car.destroy end assert car.destroyed? end + + def test_removing_has_and_belongs_to_many_associations_upon_destroy + p = RichPerson.create! first_name: 'Jon' + p.treasures.create! + assert !p.treasures.empty? + p.destroy + assert p.treasures.empty? + assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty? + end end class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase @@ -313,7 +324,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase def counter_test(model, expected_count) add_counter_column_to(model) - object = model.find(:first) + object = model.first assert_equal 0, object.test_count assert_equal 0, object.send(model.locking_column) yield object.id @@ -348,18 +359,7 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? def test_sane_find_with_lock assert_nothing_raised do Person.transaction do - Person.find 1, :lock => true - end - end - end - - # Test scoped lock. - def test_sane_find_with_scoped_lock - assert_nothing_raised do - Person.transaction do - Person.send(:with_scope, :find => { :lock => true }) do - Person.find 1 - end + Person.lock.find(1) end end end @@ -370,7 +370,7 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? def test_eager_find_with_lock assert_nothing_raised do Person.transaction do - Person.find 1, :include => :readers, :lock => true + Person.includes(:readers).lock.find(1) end end end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index d1f0ace184..70d00aecf9 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -11,8 +11,6 @@ class LogSubscriberTest < ActiveRecord::TestCase def setup @old_logger = ActiveRecord::Base.logger - @using_identity_map = ActiveRecord::IdentityMap.enabled? - ActiveRecord::IdentityMap.enabled = false Developer.primary_key super ActiveRecord::LogSubscriber.attach_to(:active_record) @@ -22,7 +20,6 @@ class LogSubscriberTest < ActiveRecord::TestCase super ActiveRecord::LogSubscriber.log_subscribers.pop ActiveRecord::Base.logger = @old_logger - ActiveRecord::IdentityMap.enabled = @using_identity_map end def set_logger(logger) @@ -57,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) @@ -74,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 @@ -85,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 @@ -93,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 @@ -103,13 +100,4 @@ class LogSubscriberTest < ActiveRecord::TestCase def test_initializes_runtime Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join end - - def test_log - ActiveRecord::IdentityMap.use do - Post.find 1 - Post.find 1 - end - wait - assert_match(/From Identity Map/, @logger.logged(:debug).last) - end end diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb index 8122857f52..a36b2c2506 100644 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -95,7 +95,11 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase end def test_mass_assigning_does_not_choke_on_nil - Firm.new.assign_attributes(nil) + assert_nil Firm.new.assign_attributes(nil) + end + + def test_mass_assigning_does_not_choke_on_empty_hash + assert_nil Firm.new.assign_attributes({}) end def test_assign_attributes_uses_default_role_when_no_role_is_provided @@ -248,6 +252,82 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase end end + test "ActiveRecord::Model.whitelist_attributes works for models which include Model" do + begin + prev, ActiveRecord::Model.whitelist_attributes = ActiveRecord::Model.whitelist_attributes, true + + klass = Class.new { include ActiveRecord::Model } + assert_equal ActiveModel::MassAssignmentSecurity::WhiteList, klass.active_authorizers[:default].class + assert_equal [], klass.active_authorizers[:default].to_a + ensure + ActiveRecord::Model.whitelist_attributes = prev + end + end + + test "ActiveRecord::Model.whitelist_attributes works for models which inherit Base" do + begin + prev, ActiveRecord::Model.whitelist_attributes = ActiveRecord::Model.whitelist_attributes, true + + klass = Class.new(ActiveRecord::Base) + assert_equal ActiveModel::MassAssignmentSecurity::WhiteList, klass.active_authorizers[:default].class + assert_equal [], klass.active_authorizers[:default].to_a + + klass.attr_accessible 'foo' + assert_equal ['foo'], Class.new(klass).active_authorizers[:default].to_a + ensure + ActiveRecord::Model.whitelist_attributes = prev + end + end + + test "ActiveRecord::Model.mass_assignment_sanitizer works for models which include Model" do + begin + sanitizer = Object.new + prev, ActiveRecord::Model.mass_assignment_sanitizer = ActiveRecord::Model.mass_assignment_sanitizer, sanitizer + + klass = Class.new { include ActiveRecord::Model } + assert_equal sanitizer, klass._mass_assignment_sanitizer + + ActiveRecord::Model.mass_assignment_sanitizer = nil + klass = Class.new { include ActiveRecord::Model } + assert_not_nil klass._mass_assignment_sanitizer + ensure + ActiveRecord::Model.mass_assignment_sanitizer = prev + end + end + + test "ActiveRecord::Model.mass_assignment_sanitizer works for models which inherit Base" do + begin + sanitizer = Object.new + prev, ActiveRecord::Model.mass_assignment_sanitizer = ActiveRecord::Model.mass_assignment_sanitizer, sanitizer + + klass = Class.new(ActiveRecord::Base) + assert_equal sanitizer, klass._mass_assignment_sanitizer + + sanitizer2 = Object.new + klass.mass_assignment_sanitizer = sanitizer2 + assert_equal sanitizer2, Class.new(klass)._mass_assignment_sanitizer + ensure + ActiveRecord::Model.mass_assignment_sanitizer = prev + end + end +end + + +# This class should be deleted when we remove activerecord-deprecated_finders as a +# dependency. +class MassAssignmentSecurityDeprecatedFindersTest < ActiveRecord::TestCase + include MassAssignmentTestHelpers + + def setup + super + @deprecation_behavior = ActiveSupport::Deprecation.behavior + ActiveSupport::Deprecation.behavior = :silence + end + + def teardown + ActiveSupport::Deprecation.behavior = @deprecation_behavior + end + def test_find_or_initialize_by_with_attr_accessible_attributes p = TightPerson.find_or_initialize_by_first_name('Josh', attributes_hash) @@ -861,4 +941,26 @@ class MassAssignmentSecurityNestedAttributesTest < ActiveRecord::TestCase assert_all_attributes(person.best_friends.first) end + def test_mass_assignment_options_are_reset_after_exception + person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin) + person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin) + + attributes = { :best_friend_attributes => { :comments => 'rides a sweet bike' } } + assert_raises(RuntimeError) { person.assign_attributes(attributes, :as => :admin) } + assert_equal 'm', person.best_friend.gender + + person.best_friend_attributes = { :gender => 'f' } + assert_equal 'm', person.best_friend.gender + end + + def test_mass_assignment_options_are_nested_correctly + person = NestedPerson.create!({ :first_name => 'David', :gender => 'm' }, :as => :admin) + person.create_best_friend!({ :first_name => 'Jeremy', :gender => 'm' }, :as => :admin) + + attributes = { :best_friend_first_name => 'Josh', :best_friend_attributes => { :gender => 'f' } } + person.assign_attributes(attributes, :as => :admin) + assert_equal 'Josh', person.best_friend.first_name + assert_equal 'f', person.best_friend.gender + end + end diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb deleted file mode 100644 index ebf6e26385..0000000000 --- a/activerecord/test/cases/method_scoping_test.rb +++ /dev/null @@ -1,558 +0,0 @@ -# This file can be removed once with_exclusive_scope and with_scope are removed. -# All the tests were already ported to relation_scoping_test.rb when the new -# relation scoping API was added. - -require "cases/helper" -require 'models/post' -require 'models/author' -require 'models/developer' -require 'models/project' -require 'models/comment' - -class MethodScopingTest < ActiveRecord::TestCase - fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects - - def test_set_conditions - Developer.send(:with_scope, :find => { :conditions => 'just a test...' }) do - assert_match '(just a test...)', Developer.scoped.to_sql - end - end - - def test_scoped_find - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - assert_nothing_raised { Developer.find(1) } - end - end - - def test_scoped_find_first - Developer.send(:with_scope, :find => { :conditions => "salary = 100000" }) do - assert_equal Developer.find(10), Developer.find(:first, :order => 'name') - end - end - - def test_scoped_find_last - highest_salary = Developer.find(:first, :order => "salary DESC") - - Developer.send(:with_scope, :find => { :order => "salary" }) do - assert_equal highest_salary, Developer.last - end - end - - def test_scoped_find_last_preserves_scope - lowest_salary = Developer.find(:first, :order => "salary ASC") - highest_salary = Developer.find(:first, :order => "salary DESC") - - Developer.send(:with_scope, :find => { :order => "salary" }) do - assert_equal highest_salary, Developer.last - assert_equal lowest_salary, Developer.first - end - end - - def test_scoped_find_combines_conditions - Developer.send(:with_scope, :find => { :conditions => "salary = 9000" }) do - assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => "name = 'Jamis'") - end - end - - def test_scoped_find_sanitizes_conditions - Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do - assert_equal developers(:poor_jamis), Developer.find(:first) - end - end - - def test_scoped_find_combines_and_sanitizes_conditions - Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do - assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis']) - end - end - - def test_scoped_find_all - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - assert_equal [developers(:david)], Developer.all - end - end - - def test_scoped_find_select - Developer.send(:with_scope, :find => { :select => "id, name" }) do - developer = Developer.find(:first, :conditions => "name = 'David'") - assert_equal "David", developer.name - assert !developer.has_attribute?(:salary) - end - end - - def test_scope_select_concatenates - Developer.send(:with_scope, :find => { :select => "name" }) do - developer = Developer.find(:first, :select => 'id, salary', :conditions => "name = 'David'") - assert_equal 80000, developer.salary - assert developer.has_attribute?(:id) - assert developer.has_attribute?(:name) - assert developer.has_attribute?(:salary) - end - end - - def test_scoped_count - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - assert_equal 1, Developer.count - end - - Developer.send(:with_scope, :find => { :conditions => 'salary = 100000' }) do - assert_equal 8, Developer.count - assert_equal 1, Developer.count(:conditions => "name LIKE 'fixture_1%'") - end - end - - def test_scoped_find_include - # with the include, will retrieve only developers for the given project - scoped_developers = Developer.send(:with_scope, :find => { :include => :projects }) do - Developer.find(:all, :conditions => { 'projects.id' => 2 }) - end - assert scoped_developers.include?(developers(:david)) - assert !scoped_developers.include?(developers(:jamis)) - assert_equal 1, scoped_developers.size - end - - def test_scoped_find_joins - scoped_developers = Developer.send(:with_scope, :find => { :joins => 'JOIN developers_projects ON id = developer_id' } ) do - Developer.find(:all, :conditions => 'developers_projects.project_id = 2') - end - assert scoped_developers.include?(developers(:david)) - assert !scoped_developers.include?(developers(:jamis)) - assert_equal 1, scoped_developers.size - assert_equal developers(:david).attributes, scoped_developers.first.attributes - end - - def test_scoped_find_using_new_style_joins - scoped_developers = Developer.send(:with_scope, :find => { :joins => :projects }) do - Developer.find(:all, :conditions => 'projects.id = 2') - end - assert scoped_developers.include?(developers(:david)) - assert !scoped_developers.include?(developers(:jamis)) - assert_equal 1, scoped_developers.size - assert_equal developers(:david).attributes, scoped_developers.first.attributes - end - - def test_scoped_find_merges_old_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id ' }) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'INNER JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_merges_new_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => :comments, :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_merges_new_and_old_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_merges_string_array_style_and_string_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => ["INNER JOIN posts ON posts.author_id = authors.id"]}) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'INNER JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_merges_string_array_style_and_hash_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => :posts}) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => ['INNER JOIN comments ON posts.id = comments.post_id'], :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_merges_joins_and_eliminates_duplicate_string_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => 'INNER JOIN posts ON posts.author_id = authors.id'}) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => ["INNER JOIN posts ON posts.author_id = authors.id", "INNER JOIN comments ON posts.id = comments.post_id"], :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_strips_spaces_from_string_joins_and_eliminates_duplicate_string_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => ' INNER JOIN posts ON posts.author_id = authors.id '}) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => ['INNER JOIN posts ON posts.author_id = authors.id'], :conditions => 'posts.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_count_include - # with the include, will retrieve only developers for the given project - Developer.send(:with_scope, :find => { :include => :projects }) do - assert_equal 1, Developer.count(:conditions => { 'projects.id' => 2 }) - end - end - - def test_scope_for_create_only_uses_equal - table = VerySpecialComment.arel_table - relation = VerySpecialComment.scoped - relation.where_values << table[:id].not_eq(1) - assert_equal({:type => "VerySpecialComment"}, relation.send(:scope_for_create)) - end - - def test_scoped_create - new_comment = nil - - VerySpecialComment.send(:with_scope, :create => { :post_id => 1 }) do - assert_equal({:post_id => 1, :type => 'VerySpecialComment' }, VerySpecialComment.scoped.send(:scope_for_create)) - new_comment = VerySpecialComment.create :body => "Wonderful world" - end - - assert Post.find(1).comments.include?(new_comment) - end - - def test_scoped_create_with_join_and_merge - Comment.where(:body => "but Who's Buying?").joins(:post).merge(Post.where(:body => 'Peace Sells...')).with_scope do - assert_equal({:body => "but Who's Buying?"}, Comment.scoped.scope_for_create) - end - end - - def test_immutable_scope - options = { :conditions => "name = 'David'" } - Developer.send(:with_scope, :find => options) do - assert_equal %w(David), Developer.all.map(&:name) - options[:conditions] = "name != 'David'" - assert_equal %w(David), Developer.all.map(&:name) - end - - scope = { :find => { :conditions => "name = 'David'" }} - Developer.send(:with_scope, scope) do - assert_equal %w(David), Developer.all.map(&:name) - scope[:find][:conditions] = "name != 'David'" - assert_equal %w(David), Developer.all.map(&:name) - end - end - - def test_scoped_with_duck_typing - scoping = Struct.new(:current_scope).new(:find => { :conditions => ["name = ?", 'David'] }) - Developer.send(:with_scope, scoping) do - assert_equal %w(David), Developer.all.map(&:name) - end - end - - def test_ensure_that_method_scoping_is_correctly_restored - begin - Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do - raise "an exception" - end - rescue - end - - assert !Developer.scoped.where_values.include?("name = 'Jamis'") - end -end - -class NestedScopingTest < ActiveRecord::TestCase - fixtures :authors, :developers, :projects, :comments, :posts - - def test_merge_options - Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do - Developer.send(:with_scope, :find => { :limit => 10 }) do - devs = Developer.scoped - assert_match '(salary = 80000)', devs.to_sql - assert_equal 10, devs.taken - end - end - end - - def test_merge_inner_scope_has_priority - Developer.send(:with_scope, :find => { :limit => 5 }) do - Developer.send(:with_scope, :find => { :limit => 10 }) do - assert_equal 10, Developer.scoped.taken - end - end - end - - def test_replace_options - Developer.send(:with_scope, :find => { :conditions => {:name => 'David'} }) do - Developer.send(:with_exclusive_scope, :find => { :conditions => {:name => 'Jamis'} }) do - assert_equal 'Jamis', Developer.scoped.send(:scope_for_create)[:name] - end - - assert_equal 'David', Developer.scoped.send(:scope_for_create)[:name] - end - end - - def test_with_exclusive_scope_with_relation - assert_raise(ArgumentError) do - Developer.all_johns - end - end - - def test_append_conditions - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do - devs = Developer.scoped - assert_match "(name = 'David') AND (salary = 80000)", devs.to_sql - assert_equal(1, Developer.count) - end - Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do - assert_equal(0, Developer.count) - end - end - end - - def test_merge_and_append_options - Developer.send(:with_scope, :find => { :conditions => 'salary = 80000', :limit => 10 }) do - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - devs = Developer.scoped - assert_match "(salary = 80000) AND (name = 'David')", devs.to_sql - assert_equal 10, devs.taken - end - end - end - - def test_nested_scoped_find - Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do - Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'David'" }) do - assert_nothing_raised { Developer.find(1) } - assert_equal('David', Developer.find(:first).name) - end - assert_equal('Jamis', Developer.find(:first).name) - end - end - - def test_nested_scoped_find_include - Developer.send(:with_scope, :find => { :include => :projects }) do - Developer.send(:with_scope, :find => { :conditions => { 'projects.id' => 2 } }) do - assert_nothing_raised { Developer.find(1) } - assert_equal('David', Developer.find(:first).name) - end - end - end - - def test_nested_scoped_find_merged_include - # :include's remain unique and don't "double up" when merging - Developer.send(:with_scope, :find => { :include => :projects, :conditions => { 'projects.id' => 2 } }) do - Developer.send(:with_scope, :find => { :include => :projects }) do - assert_equal 1, Developer.scoped.includes_values.uniq.length - assert_equal 'David', Developer.find(:first).name - end - end - - # the nested scope doesn't remove the first :include - Developer.send(:with_scope, :find => { :include => :projects, :conditions => { 'projects.id' => 2 } }) do - Developer.send(:with_scope, :find => { :include => [] }) do - assert_equal 1, Developer.scoped.includes_values.uniq.length - assert_equal('David', Developer.find(:first).name) - end - end - - # mixing array and symbol include's will merge correctly - Developer.send(:with_scope, :find => { :include => [:projects], :conditions => { 'projects.id' => 2 } }) do - Developer.send(:with_scope, :find => { :include => :projects }) do - assert_equal 1, Developer.scoped.includes_values.uniq.length - assert_equal('David', Developer.find(:first).name) - end - end - end - - def test_nested_scoped_find_replace_include - Developer.send(:with_scope, :find => { :include => :projects }) do - Developer.send(:with_exclusive_scope, :find => { :include => [] }) do - assert_equal 0, Developer.scoped.includes_values.length - end - end - end - - def test_three_level_nested_exclusive_scoped_find - Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do - assert_equal('Jamis', Developer.find(:first).name) - - Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'David'" }) do - assert_equal('David', Developer.find(:first).name) - - Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'Maiha'" }) do - assert_equal(nil, Developer.find(:first)) - end - - # ensure that scoping is restored - assert_equal('David', Developer.find(:first).name) - end - - # ensure that scoping is restored - assert_equal('Jamis', Developer.find(:first).name) - end - end - - def test_merged_scoped_find - poor_jamis = developers(:poor_jamis) - Developer.send(:with_scope, :find => { :conditions => "salary < 100000" }) do - Developer.send(:with_scope, :find => { :offset => 1, :order => 'id asc' }) do - # Oracle adapter does not generated space after asc therefore trailing space removed from regex - assert_sql(/ORDER BY\s+id asc/) do - assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc')) - end - end - end - end - - def test_merged_scoped_find_sanitizes_conditions - Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do - Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do - assert_raise(ActiveRecord::RecordNotFound) { developers(:poor_jamis) } - end - end - end - - def test_nested_scoped_find_combines_and_sanitizes_conditions - Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do - Developer.send(:with_exclusive_scope, :find => { :conditions => ['salary = ?', 9000] }) do - assert_equal developers(:poor_jamis), Developer.find(:first) - assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis']) - end - end - end - - def test_merged_scoped_find_combines_and_sanitizes_conditions - Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do - Developer.send(:with_scope, :find => { :conditions => ['salary > ?', 9000] }) do - assert_equal %w(David), Developer.all.map(&:name) - end - end - end - - def test_nested_scoped_create - comment = nil - Comment.send(:with_scope, :create => { :post_id => 1}) do - Comment.send(:with_scope, :create => { :post_id => 2}) do - assert_equal({:post_id => 2}, Comment.scoped.send(:scope_for_create)) - comment = Comment.create :body => "Hey guys, nested scopes are broken. Please fix!" - end - end - assert_equal 2, comment.post_id - end - - def test_nested_exclusive_scope_for_create - comment = nil - - Comment.send(:with_scope, :create => { :body => "Hey guys, nested scopes are broken. Please fix!" }) do - Comment.send(:with_exclusive_scope, :create => { :post_id => 1 }) do - assert_equal({:post_id => 1}, Comment.scoped.send(:scope_for_create)) - assert_blank Comment.new.body - comment = Comment.create :body => "Hey guys" - end - end - assert_equal 1, comment.post_id - assert_equal 'Hey guys', comment.body - end - - def test_merged_scoped_find_on_blank_conditions - [nil, " ", [], {}].each do |blank| - Developer.send(:with_scope, :find => {:conditions => blank}) do - Developer.send(:with_scope, :find => {:conditions => blank}) do - assert_nothing_raised { Developer.find(:first) } - end - end - end - end - - def test_merged_scoped_find_on_blank_bind_conditions - [ [""], ["",{}] ].each do |blank| - Developer.send(:with_scope, :find => {:conditions => blank}) do - Developer.send(:with_scope, :find => {:conditions => blank}) do - assert_nothing_raised { Developer.find(:first) } - end - end - end - end - - def test_immutable_nested_scope - options1 = { :conditions => "name = 'Jamis'" } - options2 = { :conditions => "name = 'David'" } - Developer.send(:with_scope, :find => options1) do - Developer.send(:with_exclusive_scope, :find => options2) do - assert_equal %w(David), Developer.all.map(&:name) - options1[:conditions] = options2[:conditions] = nil - assert_equal %w(David), Developer.all.map(&:name) - end - end - end - - def test_immutable_merged_scope - options1 = { :conditions => "name = 'Jamis'" } - options2 = { :conditions => "salary > 10000" } - Developer.send(:with_scope, :find => options1) do - Developer.send(:with_scope, :find => options2) do - assert_equal %w(Jamis), Developer.all.map(&:name) - options1[:conditions] = options2[:conditions] = nil - assert_equal %w(Jamis), Developer.all.map(&:name) - end - end - end - - def test_ensure_that_method_scoping_is_correctly_restored - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - begin - Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do - raise "an exception" - end - rescue - end - - assert Developer.scoped.where_values.include?("name = 'David'") - assert !Developer.scoped.where_values.include?("name = 'Maiha'") - end - end - - def test_nested_scoped_find_merges_old_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id' }) do - Author.send(:with_scope, :find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do - Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1') - end - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_nested_scoped_find_merges_new_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do - Author.send(:with_scope, :find => { :joins => :comments }) do - Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1') - end - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_nested_scoped_find_merges_new_and_old_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do - Author.send(:with_scope, :find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => '', :conditions => 'comments.id = 1') - end - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end -end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index f0b1f74bd3..ec4c554abb 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -83,7 +83,6 @@ module ActiveRecord t.column :one_int, :integer, :limit => 1 t.column :four_int, :integer, :limit => 4 t.column :eight_int, :integer, :limit => 8 - t.column :eleven_int, :integer, :limit => 11 end columns = connection.columns(:testings) @@ -94,20 +93,17 @@ module ActiveRecord one = columns.detect { |c| c.name == "one_int" } four = columns.detect { |c| c.name == "four_int" } eight = columns.detect { |c| c.name == "eight_int" } - eleven = columns.detect { |c| c.name == "eleven_int" } if current_adapter?(:PostgreSQLAdapter) assert_equal 'integer', default.sql_type assert_equal 'smallint', one.sql_type assert_equal 'integer', four.sql_type assert_equal 'bigint', eight.sql_type - assert_equal 'integer', eleven.sql_type elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) assert_match 'int(11)', default.sql_type assert_match 'tinyint', one.sql_type assert_match 'int', four.sql_type assert_match 'bigint', eight.sql_type - assert_match 'int(11)', eleven.sql_type elsif current_adapter?(:OracleAdapter) assert_equal 'NUMBER(38)', default.sql_type assert_equal 'NUMBER(1)', one.sql_type @@ -145,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 @@ -295,14 +291,20 @@ module ActiveRecord def test_column_exists_with_definition connection.create_table :testings do |t| - t.column :foo, :string, :limit => 100 - t.column :bar, :decimal, :precision => 8, :scale => 2 + t.column :foo, :string, limit: 100 + t.column :bar, :decimal, precision: 8, scale: 2 + t.column :taggable_id, :integer, null: false + t.column :taggable_type, :string, default: 'Photo' end - assert connection.column_exists?(:testings, :foo, :string, :limit => 100) - refute connection.column_exists?(:testings, :foo, :string, :limit => 50) - assert connection.column_exists?(:testings, :bar, :decimal, :precision => 8, :scale => 2) - refute connection.column_exists?(:testings, :bar, :decimal, :precision => 10, :scale => 2) + assert connection.column_exists?(:testings, :foo, :string, limit: 100) + refute connection.column_exists?(:testings, :foo, :string, limit: nil) + assert connection.column_exists?(:testings, :bar, :decimal, precision: 8, scale: 2) + refute connection.column_exists?(:testings, :bar, :decimal, precision: nil, scale: nil) + assert connection.column_exists?(:testings, :taggable_id, :integer, null: false) + refute connection.column_exists?(:testings, :taggable_id, :integer, null: true) + assert connection.column_exists?(:testings, :taggable_type, :string, default: 'Photo') + refute connection.column_exists?(:testings, :taggable_type, :string, default: nil) end def test_column_exists_on_table_with_no_options_parameter_supplied diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb new file mode 100644 index 0000000000..4614be9650 --- /dev/null +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -0,0 +1,217 @@ +require "cases/migration/helper" + +module ActiveRecord + class Migration + class TableTest < ActiveRecord::TestCase + class MockConnection < MiniTest::Mock + def native_database_types + { + :string => 'varchar(255)', + :integer => 'integer', + } + end + + def type_to_sql(type, limit, precision, scale) + native_database_types[type] + end + end + + def setup + @connection = MockConnection.new + end + + def teardown + assert @connection.verify + end + + def with_change_table + yield ConnectionAdapters::Table.new(:delete_me, @connection) + end + + def test_references_column_type_adds_id + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :customer, {}] + t.references :customer + end + end + + def test_remove_references_column_type_removes_id + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :customer, {}] + t.remove_references :customer + end + end + + def test_add_belongs_to_works_like_add_references + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :customer, {}] + t.belongs_to :customer + end + end + + def test_remove_belongs_to_works_like_remove_references + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :customer, {}] + t.remove_belongs_to :customer + end + end + + def test_references_column_type_with_polymorphic_adds_type + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true] + t.references :taggable, polymorphic: true + end + end + + def test_remove_references_column_type_with_polymorphic_removes_type + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true] + t.remove_references :taggable, polymorphic: true + end + end + + def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true, null: false] + t.references :taggable, polymorphic: true, null: false + end + end + + def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true, null: false] + t.remove_references :taggable, polymorphic: true, null: false + end + end + + def test_timestamps_creates_updated_at_and_created_at + with_change_table do |t| + @connection.expect :add_timestamps, nil, [:delete_me] + t.timestamps + end + end + + def test_remove_timestamps_creates_updated_at_and_created_at + with_change_table do |t| + @connection.expect :remove_timestamps, nil, [:delete_me] + t.remove_timestamps + end + end + + def string_column + @connection.native_database_types[:string] + end + + def integer_column + @connection.native_database_types[:integer] + end + + def test_integer_creates_integer_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, integer_column, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, integer_column, {}] + t.integer :foo, :bar + end + end + + def test_string_creates_string_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, string_column, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, string_column, {}] + t.string :foo, :bar + end + end + + def test_column_creates_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}] + t.column :bar, :integer + end + end + + def test_column_creates_column_with_options + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {:null => false}] + t.column :bar, :integer, :null => false + end + end + + def test_index_creates_index + with_change_table do |t| + @connection.expect :add_index, nil, [:delete_me, :bar, {}] + t.index :bar + end + end + + def test_index_creates_index_with_options + with_change_table do |t| + @connection.expect :add_index, nil, [:delete_me, :bar, {:unique => true}] + t.index :bar, :unique => true + end + end + + def test_index_exists + with_change_table do |t| + @connection.expect :index_exists?, nil, [:delete_me, :bar, {}] + t.index_exists?(:bar) + end + end + + def test_index_exists_with_options + with_change_table do |t| + @connection.expect :index_exists?, nil, [:delete_me, :bar, {:unique => true}] + t.index_exists?(:bar, :unique => true) + end + end + + def test_change_changes_column + with_change_table do |t| + @connection.expect :change_column, nil, [:delete_me, :bar, :string, {}] + t.change :bar, :string + end + end + + def test_change_changes_column_with_options + with_change_table do |t| + @connection.expect :change_column, nil, [:delete_me, :bar, :string, {:null => true}] + t.change :bar, :string, :null => true + end + end + + def test_change_default_changes_column + with_change_table do |t| + @connection.expect :change_column_default, nil, [:delete_me, :bar, :string] + t.change_default :bar, :string + end + end + + def test_remove_drops_single_column + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, :bar] + t.remove :bar + end + end + + def test_remove_drops_multiple_columns + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, :bar, :baz] + t.remove :bar, :baz + end + end + + def test_remove_index_removes_index_with_options + with_change_table do |t| + @connection.expect :remove_index, nil, [:delete_me, {:unique => true}] + t.remove_index :unique => true + end + end + + def test_rename_renames_column + with_change_table do |t| + @connection.expect :rename_column, nil, [:delete_me, :bar, :baz] + t.rename :bar, :baz + end + end + end + end +end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 040445ef12..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,17 +62,17 @@ 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 - row = TestModel.find(:first) + row = TestModel.first assert_kind_of BigDecimal, row.wealth # If this assert fails, that means the SELECT is broken! @@ -79,7 +87,7 @@ module ActiveRecord TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") # SELECT - row = TestModel.find(:first) + row = TestModel.first assert_kind_of BigDecimal, row.wealth # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! @@ -132,7 +140,7 @@ module ActiveRecord :birthday => 18.years.ago, :favorite_day => 10.days.ago, :moment_of_truth => "1782-10-10 21:40:18", :male => true - bob = TestModel.find(:first) + bob = TestModel.first assert_equal 'bob', bob.first_name assert_equal 'bobsen', bob.last_name assert_equal "I was born ....", bob.bio @@ -174,7 +182,7 @@ module ActiveRecord assert_not_equal "Z", bob.moment_of_truth.zone # US/Eastern is -5 hours from GMT assert_equal Rational(-5, 24), bob.moment_of_truth.offset - assert_match(/\A-05:?00\Z/, bob.moment_of_truth.zone) #ruby 1.8.6 uses HH:MM, prior versions use HHMM + assert_match(/\A-05:00\Z/, bob.moment_of_truth.zone) assert_equal DateTime::ITALY, bob.moment_of_truth.start end end @@ -183,6 +191,16 @@ module ActiveRecord assert_instance_of TrueClass, bob.male? assert_kind_of BigDecimal, bob.wealth end + + def test_out_of_range_limit_should_raise + skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + + assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 } + + unless current_adapter?(:PostgreSQLAdapter) + assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff } + end + end end end end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 7d026961be..f2213ee6aa 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -110,9 +110,9 @@ module ActiveRecord end def test_invert_add_index_with_name - @recorder.record :add_index, [:table, [:one, :two], {:name => "new_index"}] - remove = @recorder.inverse.first - assert_equal [:remove_index, [:table, {:name => "new_index"}]], remove + @recorder.record :add_index, [:table, [:one, :two], {:name => "new_index"}] + remove = @recorder.inverse.first + assert_equal [:remove_index, [:table, {:name => "new_index"}]], remove end def test_invert_add_index_with_no_options @@ -138,6 +138,30 @@ module ActiveRecord add = @recorder.inverse.first assert_equal [:add_timestamps, [:table]], add end + + def test_invert_add_reference + @recorder.record :add_reference, [:table, :taggable, { polymorphic: true }] + remove = @recorder.inverse.first + assert_equal [:remove_reference, [:table, :taggable, { polymorphic: true }]], remove + end + + def test_invert_add_belongs_to_alias + @recorder.record :add_belongs_to, [:table, :user] + remove = @recorder.inverse.first + assert_equal [:remove_reference, [:table, :user]], remove + end + + def test_invert_remove_reference + @recorder.record :remove_reference, [:table, :taggable, { polymorphic: true }] + add = @recorder.inverse.first + assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }]], add + end + + def test_invert_remove_belongs_to_alias + @recorder.record :remove_belongs_to, [:table, :user] + add = @recorder.inverse.first + assert_equal [:add_reference, [:table, :user]], add + end end end end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 0428d9ba76..cd1b0e8b47 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -10,60 +10,67 @@ module ActiveRecord @connection = ActiveRecord::Base.connection end + def teardown + super + %w(artists_musics musics_videos catalog).each do |table_name| + connection.drop_table table_name if connection.tables.include?(table_name) + end + end + def test_create_join_table connection.create_join_table :artists, :musics assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort - ensure - connection.drop_table :artists_musics end def test_create_join_table_set_not_null_by_default connection.create_join_table :artists, :musics assert_equal [false, false], connection.columns(:artists_musics).map(&:null) - ensure - connection.drop_table :artists_musics end def test_create_join_table_with_strings connection.create_join_table 'artists', 'musics' assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort - ensure - connection.drop_table :artists_musics end def test_create_join_table_with_the_proper_order connection.create_join_table :videos, :musics assert_equal %w(music_id video_id), connection.columns(:musics_videos).map(&:name).sort - ensure - connection.drop_table :musics_videos end def test_create_join_table_with_the_table_name - connection.create_join_table :artists, :musics, :table_name => :catalog + connection.create_join_table :artists, :musics, table_name: :catalog assert_equal %w(artist_id music_id), connection.columns(:catalog).map(&:name).sort - ensure - connection.drop_table :catalog end def test_create_join_table_with_the_table_name_as_string - connection.create_join_table :artists, :musics, :table_name => 'catalog' + connection.create_join_table :artists, :musics, table_name: 'catalog' assert_equal %w(artist_id music_id), connection.columns(:catalog).map(&:name).sort - ensure - connection.drop_table :catalog end def test_create_join_table_with_column_options - connection.create_join_table :artists, :musics, :column_options => {:null => true} + connection.create_join_table :artists, :musics, column_options: {null: true} assert_equal [true, true], connection.columns(:artists_musics).map(&:null) - ensure - connection.drop_table :artists_musics + end + + def test_create_join_table_without_indexes + connection.create_join_table :artists, :musics + + assert connection.indexes(:artists_musics).blank? + end + + def test_create_join_table_with_index + connection.create_join_table :artists, :musics do |t| + t.index [:artist_id, :music_id] + end + + assert_equal [%w(artist_id music_id)], connection.indexes(:artists_musics).map(&:columns) end end end diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb index fe53510ba2..768ebc5861 100644 --- a/activerecord/test/cases/migration/helper.rb +++ b/activerecord/test/cases/migration/helper.rb @@ -14,8 +14,10 @@ module ActiveRecord module TestHelper attr_reader :connection, :table_name + CONNECTION_METHODS = %w[add_column remove_column rename_column add_index change_column rename_table column_exists? index_exists? add_reference add_belongs_to remove_reference remove_references remove_belongs_to] + class TestModel < ActiveRecord::Base - self.table_name = 'test_models' + self.table_name = :test_models end def setup @@ -36,29 +38,8 @@ module ActiveRecord end private - def add_column(*args) - connection.add_column(*args) - end - - def remove_column(*args) - connection.remove_column(*args) - end - - def rename_column(*args) - connection.rename_column(*args) - end - def add_index(*args) - connection.add_index(*args) - end - - def change_column(*args) - connection.change_column(*args) - end - - def rename_table(*args) - connection.rename_table(*args) - end + delegate(*CONNECTION_METHODS, to: :connection) end end end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 89cf0f5e93..dd9492924c 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -171,6 +171,15 @@ module ActiveRecord end end + def test_add_partial_index + skip 'only on pg' unless current_adapter?(:PostgreSQLAdapter) + + connection.add_index("testings", "last_name", :where => "first_name = 'john doe'") + assert connection.index_exists?("testings", "last_name") + + connection.remove_index("testings", "last_name") + assert !connection.index_exists?("testings", "last_name") + end end end end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb new file mode 100644 index 0000000000..264a99f9ce --- /dev/null +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -0,0 +1,102 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class ReferencesIndexTest < ActiveRecord::TestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + def teardown + super + connection.drop_table :testings rescue nil + end + + def test_creates_index + connection.create_table table_name do |t| + t.references :foo, :index => true + end + + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_does_not_create_index + connection.create_table table_name do |t| + t.references :foo + end + + refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_does_not_create_index_explicit + connection.create_table table_name do |t| + t.references :foo, :index => false + end + + refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_creates_index_with_options + connection.create_table table_name do |t| + t.references :foo, :index => {:name => :index_testings_on_yo_momma} + t.references :bar, :index => {:unique => true} + end + + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_yo_momma) + assert connection.index_exists?(table_name, :bar_id, :name => :index_testings_on_bar_id, :unique => true) + end + + def test_creates_polymorphic_index + return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter + + connection.create_table table_name do |t| + t.references :foo, :polymorphic => true, :index => true + end + + assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + end + + def test_creates_index_for_existing_table + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo, :index => true + end + + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_does_not_create_index_for_existing_table + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo + end + + refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_does_not_create_index_for_existing_table_explicit + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo, :index => false + end + + refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_creates_polymorphic_index_for_existing_table + return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo, :polymorphic => true, :index => true + end + + assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + end + + end + end +end diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb new file mode 100644 index 0000000000..144302bd4a --- /dev/null +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -0,0 +1,111 @@ +require "cases/migration/helper" + +module ActiveRecord + class Migration + class ReferencesStatementsTest < ActiveRecord::TestCase + include ActiveRecord::Migration::TestHelper + + self.use_transactional_fixtures = false + + def setup + super + @table_name = :test_models + + add_column table_name, :supplier_id, :integer + add_index table_name, :supplier_id + end + + def test_creates_reference_id_column + add_reference table_name, :user + assert column_exists?(table_name, :user_id, :integer) + end + + def test_does_not_create_reference_type_column + add_reference table_name, :taggable + refute column_exists?(table_name, :taggable_type, :string) + end + + def test_creates_reference_type_column + add_reference table_name, :taggable, polymorphic: true + assert column_exists?(table_name, :taggable_type, :string) + end + + def test_creates_reference_id_index + add_reference table_name, :user, index: true + assert index_exists?(table_name, :user_id) + end + + def test_does_not_create_reference_id_index + add_reference table_name, :user + refute index_exists?(table_name, :user_id) + end + + def test_creates_polymorphic_index + add_reference table_name, :taggable, polymorphic: true, index: true + assert index_exists?(table_name, [:taggable_id, :taggable_type]) + end + + def test_creates_reference_type_column_with_default + add_reference table_name, :taggable, polymorphic: { default: 'Photo' }, index: true + assert column_exists?(table_name, :taggable_type, :string, default: 'Photo') + end + + def test_creates_named_index + add_reference table_name, :tag, index: { name: 'index_taggings_on_tag_id' } + assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id') + end + + def test_deletes_reference_id_column + remove_reference table_name, :supplier + refute column_exists?(table_name, :supplier_id, :integer) + end + + def test_deletes_reference_id_index + remove_reference table_name, :supplier + refute index_exists?(table_name, :supplier_id) + end + + def test_does_not_delete_reference_type_column + with_polymorphic_column do + remove_reference table_name, :supplier + + refute column_exists?(table_name, :supplier_id, :integer) + assert column_exists?(table_name, :supplier_type, :string) + end + end + + def test_deletes_reference_type_column + with_polymorphic_column do + remove_reference table_name, :supplier, polymorphic: true + refute column_exists?(table_name, :supplier_type, :string) + end + end + + def test_deletes_polymorphic_index + with_polymorphic_column do + remove_reference table_name, :supplier, polymorphic: true + refute index_exists?(table_name, [:supplier_id, :supplier_type]) + end + end + + def test_add_belongs_to_alias + add_belongs_to table_name, :user + assert column_exists?(table_name, :user_id, :integer) + end + + def test_remove_belongs_to_alias + remove_belongs_to table_name, :supplier + refute column_exists?(table_name, :supplier_id, :integer) + end + + private + + def with_polymorphic_column + add_column table_name, :supplier_type, :string + add_index table_name, [:supplier_id, :supplier_type] + + yield + end + end + end +end diff --git a/activerecord/test/cases/migration/rename_column_test.rb b/activerecord/test/cases/migration/rename_column_test.rb index 16e09fd80e..d1a85ee5e4 100644 --- a/activerecord/test/cases/migration/rename_column_test.rb +++ b/activerecord/test/cases/migration/rename_column_test.rb @@ -18,7 +18,7 @@ module ActiveRecord rename_column "test_models", "girlfriend", "exgirlfriend" TestModel.reset_column_information - bob = TestModel.find(:first) + bob = TestModel.first assert_equal "bobette", bob.exgirlfriend end @@ -33,7 +33,7 @@ module ActiveRecord rename_column :test_models, :first_name, :nick_name TestModel.reset_column_information assert TestModel.column_names.include?("nick_name") - assert_equal ['foo'], TestModel.find(:all).map(&:nick_name) + assert_equal ['foo'], TestModel.all.map(&:nick_name) end # FIXME: another integration test. We should decouple this from the @@ -46,7 +46,7 @@ module ActiveRecord rename_column "test_models", "first_name", "nick_name" TestModel.reset_column_information assert TestModel.column_names.include?("nick_name") - assert_equal ['foo'], TestModel.find(:all).map(&:nick_name) + assert_equal ['foo'], TestModel.all.map(&:nick_name) end def test_rename_column_preserves_default_value_not_null diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index d5ff2c607f..21901bec3c 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -14,6 +14,11 @@ module ActiveRecord remove_column 'test_models', :updated_at end + def teardown + rename_table :octopi, :test_models if connection.table_exists? :octopi + super + end + def test_rename_table_for_sqlite_should_work_with_reserved_words renamed = false @@ -26,8 +31,7 @@ module ActiveRecord renamed = true # Using explicit id in insert for compatibility across all databases - con = connection - con.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)" + connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)" assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1") ensure return unless renamed @@ -39,16 +43,13 @@ module ActiveRecord rename_table :test_models, :octopi # Using explicit id in insert for compatibility across all databases - con = connection - con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) + connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) - con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" + connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" - con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) + connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") - - rename_table :octopi, :test_models end def test_rename_table_with_an_index @@ -57,15 +58,22 @@ module ActiveRecord rename_table :test_models, :octopi # Using explicit id in insert for compatibility across all databases - con = ActiveRecord::Base.connection - con.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) - con.execute "INSERT INTO octopi (#{con.quote_column_name('id')}, #{con.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" - con.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) + connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter) + connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" + connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter) assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1") assert connection.indexes(:octopi).first.columns.include?("url") + end + + def test_rename_table_for_postgresql_should_also_rename_default_sequence + skip 'not supported' unless current_adapter?(:PostgreSQLAdapter) + + rename_table :test_models, :octopi + + pk, seq = connection.pk_and_sequence_for('octopi') - rename_table :octopi, :test_models + assert_equal "octopi_#{pk}_seq", seq end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 92dc150104..3c0d2b18d9 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -36,7 +36,7 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Base.connection.initialize_schema_migrations_table ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}" - %w(things awesome_things prefix_things_suffix prefix_awesome_things_suffix).each do |table| + %w(things awesome_things prefix_things_suffix p_awesome_things_s ).each do |table| Thing.connection.drop_table(table) rescue nil end Thing.reset_column_information @@ -56,6 +56,21 @@ class MigrationTest < ActiveRecord::TestCase Person.reset_column_information end + def test_migrator_versions + migrations_path = MIGRATIONS_ROOT + "/valid" + ActiveRecord::Migrator.migrations_paths = migrations_path + + ActiveRecord::Migrator.up(migrations_path) + assert_equal 3, ActiveRecord::Migrator.current_version + assert_equal 3, ActiveRecord::Migrator.last_version + assert_equal false, ActiveRecord::Migrator.needs_migration? + + ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid") + assert_equal 0, ActiveRecord::Migrator.current_version + assert_equal 3, ActiveRecord::Migrator.last_version + assert_equal true, ActiveRecord::Migrator.needs_migration? + end + def test_create_table_with_force_true_does_not_drop_nonexisting_table if Person.connection.table_exists?(:testings2) Person.connection.drop_table :testings2 @@ -109,7 +124,7 @@ class MigrationTest < ActiveRecord::TestCase :value_of_e => BigDecimal("2.7182818284590452353602875") ) - b = BigNumber.find(:first) + b = BigNumber.first assert_not_nil b assert_not_nil b.bank_balance @@ -153,7 +168,7 @@ class MigrationTest < ActiveRecord::TestCase end GiveMeBigNumbers.down - assert_raise(ActiveRecord::StatementInvalid) { BigNumber.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { BigNumber.first } end def test_filtering_migrations @@ -165,13 +180,13 @@ class MigrationTest < ActiveRecord::TestCase Person.reset_column_information assert Person.column_methods_hash.include?(:last_name) - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter) Person.reset_column_information assert !Person.column_methods_hash.include?(:last_name) - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } end class MockMigration < ActiveRecord::Migration @@ -278,19 +293,19 @@ class MigrationTest < ActiveRecord::TestCase def test_rename_table_with_prefix_and_suffix assert !Thing.table_exists? - ActiveRecord::Base.table_name_prefix = 'prefix_' - ActiveRecord::Base.table_name_suffix = '_suffix' + ActiveRecord::Base.table_name_prefix = 'p_' + ActiveRecord::Base.table_name_suffix = '_s' Thing.reset_table_name Thing.reset_sequence_name WeNeedThings.up assert Thing.create("content" => "hello world") - assert_equal "hello world", Thing.find(:first).content + assert_equal "hello world", Thing.first.content RenameThings.up - Thing.table_name = "prefix_awesome_things_suffix" + Thing.table_name = "p_awesome_things_s" - assert_equal "hello world", Thing.find(:first).content + assert_equal "hello world", Thing.first.content ensure ActiveRecord::Base.table_name_prefix = '' ActiveRecord::Base.table_name_suffix = '' @@ -306,10 +321,10 @@ class MigrationTest < ActiveRecord::TestCase Reminder.reset_sequence_name WeNeedReminders.up assert Reminder.create("content" => "hello world", "remind_at" => Time.now) - assert_equal "hello world", Reminder.find(:first).content + assert_equal "hello world", Reminder.first.content WeNeedReminders.down - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } ensure ActiveRecord::Base.table_name_prefix = '' ActiveRecord::Base.table_name_suffix = '' @@ -375,6 +390,27 @@ class MigrationTest < ActiveRecord::TestCase end end + def test_out_of_range_limit_should_raise + skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + + Person.connection.drop_table :test_limits rescue nil + assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do + Person.connection.create_table :test_integer_limits, :force => true do |t| + t.column :bigone, :integer, :limit => 10 + end + end + + unless current_adapter?(:PostgreSQLAdapter) + assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do + Person.connection.create_table :test_text_limits, :force => true do |t| + t.column :bigtext, :text, :limit => 0xfffffffff + end + end + end + + Person.connection.drop_table :test_limits rescue nil + end + protected def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz @@ -400,227 +436,6 @@ class ReservedWordsMigrationTest < ActiveRecord::TestCase end end - -class ChangeTableMigrationsTest < ActiveRecord::TestCase - def setup - @connection = Person.connection - @connection.create_table :delete_me, :force => true do |t| - end - end - - def teardown - Person.connection.drop_table :delete_me rescue nil - end - - def test_references_column_type_adds_id - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {}) - t.references :customer - end - end - - def test_remove_references_column_type_removes_id - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'customer_id') - t.remove_references :customer - end - end - - def test_add_belongs_to_works_like_add_references - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {}) - t.belongs_to :customer - end - end - - def test_remove_belongs_to_works_like_remove_references - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'customer_id') - t.remove_belongs_to :customer - end - end - - def test_references_column_type_with_polymorphic_adds_type - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {}) - @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {}) - t.references :taggable, :polymorphic => true - end - end - - def test_remove_references_column_type_with_polymorphic_removes_type - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'taggable_type') - @connection.expects(:remove_column).with(:delete_me, 'taggable_id') - t.remove_references :taggable, :polymorphic => true - end - end - - def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {:null => false}) - @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {:null => false}) - t.references :taggable, :polymorphic => true, :null => false - end - end - - def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'taggable_type') - @connection.expects(:remove_column).with(:delete_me, 'taggable_id') - t.remove_references :taggable, :polymorphic => true, :null => false - end - end - - def test_timestamps_creates_updated_at_and_created_at - with_change_table do |t| - @connection.expects(:add_timestamps).with(:delete_me) - t.timestamps - end - end - - def test_remove_timestamps_creates_updated_at_and_created_at - with_change_table do |t| - @connection.expects(:remove_timestamps).with(:delete_me) - t.remove_timestamps - end - end - - def string_column - if current_adapter?(:PostgreSQLAdapter) - "character varying(255)" - elsif current_adapter?(:OracleAdapter) - 'VARCHAR2(255)' - else - 'varchar(255)' - end - end - - def integer_column - if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) - 'int(11)' - elsif current_adapter?(:OracleAdapter) - 'NUMBER(38)' - else - 'integer' - end - end - - def test_integer_creates_integer_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :foo, integer_column, {}) - @connection.expects(:add_column).with(:delete_me, :bar, integer_column, {}) - t.integer :foo, :bar - end - end - - def test_string_creates_string_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :foo, string_column, {}) - @connection.expects(:add_column).with(:delete_me, :bar, string_column, {}) - t.string :foo, :bar - end - end - - def test_column_creates_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :bar, :integer, {}) - t.column :bar, :integer - end - end - - def test_column_creates_column_with_options - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :bar, :integer, {:null => false}) - t.column :bar, :integer, :null => false - end - end - - def test_index_creates_index - with_change_table do |t| - @connection.expects(:add_index).with(:delete_me, :bar, {}) - t.index :bar - end - end - - def test_index_creates_index_with_options - with_change_table do |t| - @connection.expects(:add_index).with(:delete_me, :bar, {:unique => true}) - t.index :bar, :unique => true - end - end - - def test_index_exists - with_change_table do |t| - @connection.expects(:index_exists?).with(:delete_me, :bar, {}) - t.index_exists?(:bar) - end - end - - def test_index_exists_with_options - with_change_table do |t| - @connection.expects(:index_exists?).with(:delete_me, :bar, {:unique => true}) - t.index_exists?(:bar, :unique => true) - end - end - - def test_change_changes_column - with_change_table do |t| - @connection.expects(:change_column).with(:delete_me, :bar, :string, {}) - t.change :bar, :string - end - end - - def test_change_changes_column_with_options - with_change_table do |t| - @connection.expects(:change_column).with(:delete_me, :bar, :string, {:null => true}) - t.change :bar, :string, :null => true - end - end - - def test_change_default_changes_column - with_change_table do |t| - @connection.expects(:change_column_default).with(:delete_me, :bar, :string) - t.change_default :bar, :string - end - end - - def test_remove_drops_single_column - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, [:bar]) - t.remove :bar - end - end - - def test_remove_drops_multiple_columns - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, [:bar, :baz]) - t.remove :bar, :baz - end - end - - def test_remove_index_removes_index_with_options - with_change_table do |t| - @connection.expects(:remove_index).with(:delete_me, {:unique => true}) - t.remove_index :unique => true - end - end - - def test_rename_renames_column - with_change_table do |t| - @connection.expects(:rename_column).with(:delete_me, :bar, :baz) - t.rename :bar, :baz - end - end - - protected - def with_change_table - Person.connection.change_table :delete_me do |t| - yield t - end - end -end - if ActiveRecord::Base.connection.supports_bulk_alter? class BulkAlterTableMigrationsTest < ActiveRecord::TestCase def setup @@ -723,7 +538,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? # One query for columns (delete_me table) # One query for primary key (delete_me table) # One query to do the bulk change - assert_queries(3) do + assert_queries(3, :ignore_none => true) do with_bulk_change_table do |t| t.change :name, :string, :default => 'NONAME' t.change :birthdate, :datetime @@ -882,8 +697,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase def test_skipping_migrations @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" @existing_migrations = Dir[@migrations_path + "/*.rb"] - - sources = {} + + sources = {} sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_name_collision" diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index f7a5d05582..08b3408665 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -27,19 +27,19 @@ class ModulesTest < ActiveRecord::TestCase end def test_module_spanning_associations - firm = MyApplication::Business::Firm.find(:first) + firm = MyApplication::Business::Firm.first assert !firm.clients.empty?, "Firm should have clients" assert_nil firm.class.table_name.match('::'), "Firm shouldn't have the module appear in its table name" end def test_module_spanning_has_and_belongs_to_many_associations - project = MyApplication::Business::Project.find(:first) + project = MyApplication::Business::Project.first project.developers << MyApplication::Business::Developer.create("name" => "John") assert_equal "John", project.developers.last.name end def test_associations_spanning_cross_modules - account = MyApplication::Billing::Account.find(:first, :order => 'id') + 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.find(1, :include => :firm) + 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.find(3, :include => {:firm => :account}, :conditions => 'accounts.id IS NOT NULL', :references => :accounts) - clients << MyApplication::Business::Client.find(3, :include => {:firm => :account}) + 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| @@ -123,7 +123,7 @@ class ModulesTest < ActiveRecord::TestCase old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = true - collection = Shop::Collection.find(:first) + collection = Shop::Collection.first assert !collection.products.empty?, "Collection should have products" assert_nothing_raised { collection.destroy } ensure @@ -134,7 +134,7 @@ class ModulesTest < ActiveRecord::TestCase old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = true - product = Shop::Product.find(:first) + product = Shop::Product.first assert !product.variants.empty?, "Product should have variants" assert_nothing_raised { product.destroy } ensure diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index e704322b5d..42461e8ecb 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -1,15 +1,14 @@ 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 def setup @courses = create_fixtures("courses") { Course.retrieve_connection } + @colleges = create_fixtures("colleges") { College.retrieve_connection } @entrants = create_fixtures("entrants") end @@ -85,6 +84,25 @@ class MultipleDbTest < ActiveRecord::TestCase end def test_arel_table_engines - assert_equal Entrant.arel_engine, Bird.arel_engine + assert_not_equal Entrant.arel_engine, Bird.arel_engine + assert_not_equal Entrant.arel_engine, Course.arel_engine + end + + def test_connection + assert_equal Entrant.arel_engine.connection, Bird.arel_engine.connection + assert_not_equal Entrant.arel_engine.connection, Course.arel_engine.connection + end + + unless in_memory_db? + def test_associations_should_work_when_model_has_no_connection + begin + ActiveRecord::Model.remove_connection + assert_nothing_raised ActiveRecord::ConnectionNotEstablished do + College.first.courses.first + end + ensure + ActiveRecord::Model.establish_connection 'arunit' + end + end end end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index e17ba76437..bd121126e7 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -10,16 +10,15 @@ class NamedScopeTest < ActiveRecord::TestCase fixtures :posts, :authors, :topics, :comments, :author_addresses def test_implements_enumerable - assert !Topic.find(:all).empty? + assert !Topic.all.empty? - assert_equal Topic.find(:all), Topic.base - assert_equal Topic.find(:all), Topic.base.to_a - assert_equal Topic.find(:first), Topic.base.first - assert_equal Topic.find(:all), Topic.base.map { |i| i } + assert_equal Topic.all.to_a, Topic.base + assert_equal Topic.all.to_a, Topic.base.to_a + assert_equal Topic.first, Topic.base.first + assert_equal Topic.all.to_a, Topic.base.map { |i| i } end def test_found_items_are_cached - Topic.columns all_posts = Topic.base assert_queries(1) do @@ -30,7 +29,7 @@ class NamedScopeTest < ActiveRecord::TestCase def test_reload_expires_cache_of_found_items all_posts = Topic.base - all_posts.all + all_posts.to_a new_post = Topic.create! assert !all_posts.include?(new_post) @@ -38,14 +37,23 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_delegates_finds_and_calculations_to_the_base_class - assert !Topic.find(:all).empty? + assert !Topic.all.empty? - assert_equal Topic.find(:all), Topic.base.find(:all) - assert_equal Topic.find(:first), Topic.base.find(:first) - assert_equal Topic.count, Topic.base.count + assert_equal Topic.all.to_a, Topic.base.to_a + assert_equal Topic.first, Topic.base.first + assert_equal Topic.count, Topic.base.count assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count) end + def test_method_missing_priority_when_delegating + klazz = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + scope :since, Proc.new { where('written_on >= ?', Time.now - 1.day) } + scope :to, Proc.new { where('written_on <= ?', Time.now) } + end + assert_equal klazz.to.since.to_a, klazz.since.to.to_a + end + def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy assert Topic.approved.respond_to?(:limit) assert Topic.approved.respond_to?(:count) @@ -58,10 +66,10 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified - assert !Topic.find(:all, :conditions => {:approved => true}).empty? + assert !Topic.all.merge!(:where => {:approved => true}).to_a.empty? - assert_equal Topic.find(:all, :conditions => {:approved => true}), Topic.approved - assert_equal Topic.count(:conditions => {:approved => true}), Topic.approved.count + assert_equal Topic.all.merge!(:where => {:approved => true}).to_a, Topic.approved + assert_equal Topic.where(:approved => true).count, Topic.approved.count end def test_scopes_with_string_name_can_be_composed @@ -70,13 +78,9 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal Topic.replied.approved, Topic.replied.approved_as_string end - def test_scopes_can_be_specified_with_deep_hash_conditions - assert_equal Topic.replied.approved, Topic.replied.approved_as_hash_condition - end - def test_scopes_are_composable - assert_equal((approved = Topic.find(:all, :conditions => {:approved => true})), Topic.approved) - assert_equal((replied = Topic.find(:all, :conditions => 'replies_count > 0')), 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? @@ -84,8 +88,8 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_procedural_scopes - topics_written_before_the_third = Topic.find(:all, :conditions => ['written_on < ?', topics(:third).written_on]) - topics_written_before_the_second = Topic.find(:all, :conditions => ['written_on < ?', topics(:second).written_on]) + topics_written_before_the_third = Topic.where('written_on < ?', topics(:third).written_on) + topics_written_before_the_second = Topic.where('written_on < ?', topics(:second).written_on) assert_not_equal topics_written_before_the_second, topics_written_before_the_third assert_equal topics_written_before_the_third, Topic.written_before(topics(:third).written_on) @@ -93,46 +97,17 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_procedural_scopes_returning_nil - all_topics = Topic.find(:all) + all_topics = Topic.all assert_equal all_topics, Topic.written_before(nil) end - def test_scopes_with_joins - address = author_addresses(:david_address) - posts_with_authors_at_address = Post.find( - :all, :joins => 'JOIN authors ON authors.id = posts.author_id', - :conditions => [ 'authors.author_address_id = ?', address.id ] - ) - assert_equal posts_with_authors_at_address, Post.with_authors_at_address(address) - end - - def test_scopes_with_joins_respects_custom_select - address = author_addresses(:david_address) - posts_with_authors_at_address_titles = Post.find(:all, - :select => 'title', - :joins => 'JOIN authors ON authors.id = posts.author_id', - :conditions => [ 'authors.author_address_id = ?', address.id ] - ) - assert_equal posts_with_authors_at_address_titles.map(&:title), Post.with_authors_at_address(address).find(:all, :select => 'title').map(&:title) - end - def test_scope_with_object objects = Topic.with_object assert_operator objects.length, :>, 0 assert objects.all?(&:approved?), 'all objects should be approved' end - def test_extensions - assert_equal 1, Topic.anonymous_extension.one - assert_equal 2, Topic.named_extension.two - end - - def test_multiple_extensions - assert_equal 2, Topic.multiple_extensions.extension_two - assert_equal 1, Topic.multiple_extensions.extension_one - end - def test_has_many_associations_have_access_to_scopes assert_not_equal Post.containing_the_letter_a, authors(:david).posts assert !Post.containing_the_letter_a.empty? @@ -163,20 +138,16 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_active_records_have_scope_named__all__ - assert !Topic.find(:all).empty? + assert !Topic.all.empty? - assert_equal Topic.find(:all), Topic.base + assert_equal Topic.all.to_a, Topic.base end def test_active_records_have_scope_named__scoped__ - assert !Topic.find(:all, scope = {:conditions => "content LIKE '%Have%'"}).empty? - - assert_equal Topic.find(:all, scope), Topic.scoped(scope) - end + scope = Topic.where("content LIKE '%Have%'") + assert !scope.empty? - def test_first_and_last_should_support_find_options - assert_equal Topic.base.first(:order => 'title'), Topic.base.find(:first, :order => 'title') - assert_equal Topic.base.last(:order => 'title'), Topic.base.find(:last, :order => 'title') + assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'") end def test_first_and_last_should_allow_integers_for_limit @@ -193,15 +164,6 @@ class NamedScopeTest < ActiveRecord::TestCase end end - def test_first_and_last_find_options_should_use_query_when_results_are_loaded - topics = Topic.base - topics.reload # force load - assert_queries(2) do - topics.first(:order => 'title') - topics.last(:order => 'title') - end - end - def test_empty_should_not_load_results topics = Topic.base assert_queries(2) do @@ -264,9 +226,9 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_many_should_return_false_if_none_or_one - topics = Topic.base.scoped(:conditions => {:id => 0}) + topics = Topic.base.where(:id => 0) assert !topics.many? - topics = Topic.base.scoped(:conditions => {:id => 1}) + topics = Topic.base.where(:id => 1) assert !topics.many? end @@ -318,7 +280,7 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_should_use_where_in_query_for_scope - assert_equal Developer.find_all_by_name('Jamis').to_set, Developer.find_all_by_id(Developer.jamises).to_set + assert_equal Developer.where(name: 'Jamis').to_set, Developer.where(id: Developer.jamises).to_set end def test_size_should_use_count_when_results_are_not_loaded @@ -344,7 +306,7 @@ class NamedScopeTest < ActiveRecord::TestCase def test_chaining_with_duplicate_joins join = "INNER JOIN comments ON comments.post_id = posts.id" post = Post.find(1) - assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size + assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size end def test_chaining_should_use_latest_conditions_when_creating @@ -363,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 @@ -389,32 +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 - end - end - - def test_scopes_with_reserved_names - class << Topic - def public_method; end - public :public_method - - def protected_method; end - protected :protected_method - - def private_method; end - private :private_method - end - - [:public_method, :protected_method, :private_method].each do |reserved_method| - assert Topic.respond_to?(reserved_method, true) - ActiveRecord::Base.logger.expects(:warn) - Topic.scope(reserved_method) + 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 @@ -429,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 @@ -440,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 @@ -449,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 @@ -483,42 +426,24 @@ class NamedScopeTest < ActiveRecord::TestCase require "models/without_table" end end -end -class DynamicScopeMatchTest < ActiveRecord::TestCase - def test_scoped_by_no_match - assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all") - end - - def test_scoped_by - match = ActiveRecord::DynamicScopeMatch.match("scoped_by_age_and_sex_and_location") - assert_not_nil match - assert match.scope? - assert_equal %w(age sex location), match.attribute_names - end -end - -class DynamicScopeTest < ActiveRecord::TestCase - fixtures :posts + def test_eager_scopes_are_deprecated + klass = Class.new(ActiveRecord::Base) + klass.table_name = 'posts' - def setup - @test_klass = Class.new(Post) do - def self.name; "Post"; end + assert_deprecated do + klass.scope :welcome_2, klass.where(:id => posts(:welcome).id) end + assert_equal [posts(:welcome).title], klass.welcome_2.map(&:title) end - 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.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"}) - end - - def test_dynamic_scope_should_create_methods_after_hitting_method_missing - assert_blank @test_klass.methods.grep(/scoped_by_type/) - @test_klass.scoped_by_type(nil) - assert_present @test_klass.methods.grep(/scoped_by_type/) - end + def test_eager_default_scope_relations_are_deprecated + klass = Class.new(ActiveRecord::Base) + klass.table_name = 'posts' - def test_dynamic_scope_with_less_number_of_arguments - assert_raise(ArgumentError){ @test_klass.scoped_by_author_id_and_title(1) } + assert_deprecated do + klass.send(:default_scope, klass.where(:id => posts(:welcome).id)) + end + assert_equal [posts(:welcome).title], klass.all.map(&:title) end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 2ae9cb4888..07862ca4ca 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -172,6 +172,19 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase man.interests_attributes = [{:id => interest.id, :topic => 'gardening'}] assert_equal man.interests.first.topic, man.interests[0].topic end + + def test_allows_class_to_override_setter_and_call_super + mean_pirate_class = Class.new(Pirate) do + accepts_nested_attributes_for :parrot + def parrot_attributes=(attrs) + super(attrs.merge(:color => "blue")) + end + end + mean_pirate = mean_pirate_class.new + mean_pirate.parrot_attributes = { :name => "James" } + assert_equal "James", mean_pirate.parrot.name + assert_equal "blue", mean_pirate.parrot.color + end end class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase @@ -183,7 +196,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end def test_should_raise_argument_error_if_trying_to_build_polymorphic_belongs_to - assert_raise_with_message ArgumentError, "Cannot build association looter. Are you trying to build a polymorphic one-to-one association?" do + assert_raise_with_message ArgumentError, "Cannot build association `looter'. Are you trying to build a polymorphic one-to-one association?" do Treasure.new(:name => 'pearl', :looter_attributes => {:catchphrase => "Arrr"}) end end @@ -663,7 +676,7 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_sort_the_hash_by_the_keys_before_building_new_associated_models - attributes = ActiveSupport::OrderedHash.new + attributes = {} attributes['123726353'] = { :name => 'Grace OMalley' } attributes['2'] = { :name => 'Privateers Greed' } # 2 is lower then 123726353 @pirate.send(association_setter, attributes) @@ -673,7 +686,7 @@ module NestedAttributesOnACollectionAssociationTests def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) } - assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, ActiveSupport::OrderedHash.new) } + assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, Hash.new) } assert_raise_with_message ArgumentError, 'Hash or Array expected, got String ("foo")' do @pirate.send(association_setter, "foo") @@ -758,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 @@ -768,6 +781,18 @@ module NestedAttributesOnACollectionAssociationTests assert_nothing_raised(NoMethodError) { @pirate.save! } end + def test_numeric_colum_changes_from_zero_to_no_empty_string + Man.accepts_nested_attributes_for(:interests) + + 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 def association_setter @@ -823,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 @@ -853,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 adfd8e83a1..4ffa4836e0 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -13,12 +13,14 @@ require 'models/warehouse_thing' require 'models/parrot' require 'models/minivan' require 'models/person' +require 'models/pet' +require 'models/toy' require 'rexml/document' require 'active_support/core_ext/exception' class PersistencesTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys # Oracle UPDATE does not support ORDER BY unless current_adapter?(:OracleAdapter) @@ -53,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.find(:all, :limit => 2).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 @@ -76,10 +78,20 @@ class PersistencesTest < ActiveRecord::TestCase assert_equal Topic.count, Topic.delete_all end - def test_update_by_condition - Topic.update_all "content = 'bulk updated!'", ["approved = ?", true] - assert_equal "Have a nice day", Topic.find(1).content - assert_equal "bulk updated!", Topic.find(2).content + def test_delete_all_with_joins_and_where_part_is_hash + where_args = {:toys => {:name => 'Bone'}} + count = Pet.joins(:toys).where(where_args).count + + assert_equal count, 1 + assert_equal count, Pet.joins(:toys).where(where_args).delete_all + end + + def test_delete_all_with_joins_and_where_part_is_not_hash + where_args = ['toys.name = ?', 'Bone'] + count = Pet.joins(:toys).where(where_args).count + + assert_equal count, 1 + assert_equal count, Pet.joins(:toys).where(where_args).delete_all end def test_increment_attribute @@ -108,7 +120,7 @@ class PersistencesTest < ActiveRecord::TestCase def test_destroy_all conditions = "author_name = 'Mary'" - topics_by_mary = Topic.all(:conditions => conditions, :order => 'id') + 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 @@ -119,7 +131,7 @@ class PersistencesTest < ActiveRecord::TestCase end def test_destroy_many - clients = Client.find([2, 3], :order => 'id') + clients = Client.all.merge!(:order => 'id').find([2, 3]) assert_difference('Client.count', -2) do destroyed = Client.destroy([2, 3]).sort_by(&:id) @@ -293,6 +305,13 @@ class PersistencesTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } end + def test_destroy! + topic = Topic.find(1) + assert_equal topic, topic.destroy!, 'topic.destroy! did not return self' + assert topic.frozen?, 'topic not frozen after destroy!' + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } + end + def test_record_not_found_exception assert_raise(ActiveRecord::RecordNotFound) { Topic.find(99999) } end @@ -320,7 +339,7 @@ class PersistencesTest < ActiveRecord::TestCase end def test_update_all_with_non_standard_table_name - assert_equal 1, WarehouseThing.update_all(['value = ?', 0], ['id = ?', 1]) + assert_equal 1, WarehouseThing.where(id: 1).update_all(['value = ?', 0]) assert_equal 0, WarehouseThing.find(1).value end @@ -370,30 +389,8 @@ class PersistencesTest < ActiveRecord::TestCase assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') } end - # This test is correct, but it is hard to fix it since - # update_attribute trigger simply call save! that triggers - # all callbacks. - # def test_update_attribute_with_one_changed_and_one_updated - # t = Topic.order('id').limit(1).first - # title, author_name = t.title, t.author_name - # t.author_name = 'John' - # t.update_attribute(:title, 'super_title') - # assert_equal 'John', t.author_name - # assert_equal 'super_title', t.title - # assert t.changed?, "topic should have changed" - # assert t.author_name_changed?, "author_name should have changed" - # assert !t.title_changed?, "title should not have changed" - # assert_nil t.title_change, 'title change should be nil' - # assert_equal ['author_name'], t.changed - # - # t.reload - # assert_equal 'David', t.author_name - # assert_equal 'super_title', t.title - # end - def test_update_attribute_with_one_updated t = Topic.first - title = t.title t.update_attribute(:title, 'super_title') assert_equal 'super_title', t.title assert !t.changed?, "topic should not have changed" @@ -449,7 +446,7 @@ class PersistencesTest < ActiveRecord::TestCase def test_update_column_should_not_leave_the_object_dirty topic = Topic.find(1) - topic.update_attribute("content", "Have a nice day") + topic.update_column("content", "Have a nice day") topic.reload topic.update_column(:content, "You too") @@ -504,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/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index fba3006ebe..0a6354f5cc 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -17,7 +17,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase end def checkout_connections - ActiveRecord::Model.establish_connection(@connection.merge({:pool => 2, :wait_timeout => 0.3})) + ActiveRecord::Model.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.3})) @connections = [] @timed_out = 0 @@ -34,7 +34,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase # Will deadlock due to lack of Monitor timeouts in 1.9 def checkout_checkin_connections(pool_size, threads) - ActiveRecord::Model.establish_connection(@connection.merge({:pool => pool_size, :wait_timeout => 0.5})) + ActiveRecord::Model.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5})) @connection_count = 0 @timed_out = 0 threads.times do diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index f36f4237b1..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| @@ -96,7 +108,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_clear_after_close mw = ActiveRecord::QueryCache.new lambda { |env| - Post.find(:first) + Post.first [200, {}, nil] } body = mw.call({}).last @@ -107,7 +119,7 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_find_queries - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) { Task.find(1); Task.find(1) } + assert_queries(2) { Task.find(1); Task.find(1) } end def test_find_queries_with_cache @@ -151,16 +163,11 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_cache_does_not_wrap_string_results_in_arrays - if current_adapter?(:SQLite3Adapter) - require 'sqlite3/version' - sqlite3_version = RUBY_PLATFORM =~ /java/ ? Jdbc::SQLite3::VERSION : SQLite3::VERSION - end - Task.cache do # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter) && sqlite3_version > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) + elsif current_adapter?(:SQLite3Adapter) || current_adapter?(:Mysql2Adapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") @@ -169,6 +176,14 @@ class QueryCacheTest < ActiveRecord::TestCase end end end + + def test_cache_is_ignored_for_locked_relations + task = Task.find 1 + + Task.cache do + assert_queries(2) { task.lock!; task.lock! } + end + end end class QueryCacheExpiryTest < ActiveRecord::TestCase @@ -233,8 +248,8 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_cache_is_expired_by_habtm_update ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) ActiveRecord::Base.cache do - c = Category.find(:first) - p = Post.find(:first) + c = Category.first + p = Post.first p.categories << c end end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 80ee74e41e..3dd11ae89d 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -216,6 +216,14 @@ module ActiveRecord def test_string_with_crazy_column assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo)) end + + def test_quote_duration + assert_equal "1800", @quoter.quote(30.minutes) + end + + def test_quote_duration_int_column + assert_equal "7200", @quoter.quote(2.hours, FakeColumn.new(:integer)) + end end end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index e21109baae..df076c97b4 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -23,11 +23,12 @@ class ReadOnlyTest < ActiveRecord::TestCase end assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save } assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! } + assert_raise(ActiveRecord::ReadOnlyRecord) { dev.destroy } end def test_find_with_readonly_option - Developer.find(:all).each { |d| assert !d.readonly? } + Developer.all.each { |d| assert !d.readonly? } Developer.readonly(false).each { |d| assert !d.readonly? } Developer.readonly(true).each { |d| assert d.readonly? } Developer.readonly.each { |d| assert d.readonly? } @@ -48,7 +49,7 @@ class ReadOnlyTest < ActiveRecord::TestCase post = Post.find(1) assert !post.comments.empty? assert !post.comments.any?(&:readonly?) - assert !post.comments.find(:all).any?(&:readonly?) + assert !post.comments.to_a.any?(&:readonly?) assert post.comments.readonly(true).all?(&:readonly?) end @@ -70,13 +71,13 @@ class ReadOnlyTest < ActiveRecord::TestCase end def test_readonly_scoping - Post.send(:with_scope, :find => { :conditions => '1=1' }) 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.send(:with_scope, :find => { :joins => ' ' }) do + Post.joins(' ').scoping do assert !Post.find(1).readonly? assert Post.readonly.find(1).readonly? assert !Post.readonly(false).find(1).readonly? @@ -85,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.send(:with_scope, :find => { :joins => ', developers' }) 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.send(:with_scope, :find => { :readonly => true }) 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/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index 576ab60090..e53a27d5dd 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -64,7 +64,7 @@ module ActiveRecord spec.config[:reaping_frequency] = 0.0001 pool = ConnectionPool.new spec - pool.timeout = 0 + pool.dead_connection_timeout = 0 conn = pool.checkout count = pool.connections.length diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 16f05f2198..588da68ec1 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -63,7 +63,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_column_null_not_null - subscriber = Subscriber.find(:first) + subscriber = Subscriber.first assert subscriber.column_for_attribute("name").null assert !subscriber.column_for_attribute("nick").null end @@ -77,7 +77,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_klass_for_nested_class_name - reflection = MacroReflection.new(:company, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) + reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end @@ -85,15 +85,15 @@ class ReflectionTest < ActiveRecord::TestCase def test_aggregation_reflection reflection_for_address = AggregateReflection.new( - :composed_of, :address, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer + :composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer ) reflection_for_balance = AggregateReflection.new( - :composed_of, :balance, { :class_name => "Money", :mapping => %w(balance amount) }, Customer + :composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer ) reflection_for_gps_location = AggregateReflection.new( - :composed_of, :gps_location, { }, Customer + :composed_of, :gps_location, nil, { }, Customer ) assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location) @@ -117,7 +117,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_many_reflection - reflection_for_clients = AssociationReflection.new(:has_many, :clients, { :order => "id", :dependent => :destroy }, Firm) + reflection_for_clients = AssociationReflection.new(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) assert_equal reflection_for_clients, Firm.reflect_on_association(:clients) @@ -129,7 +129,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_one_reflection - reflection_for_account = AssociationReflection.new(:has_one, :account, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) + reflection_for_account = AssociationReflection.new(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) assert_equal reflection_for_account, Firm.reflect_on_association(:account) assert_equal Account, Firm.reflect_on_association(:account).klass @@ -214,21 +214,25 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal expected, actual end - def test_conditions + def test_scope_chain expected = [ - [{ :tags => { :name => 'Blue' } }], - [{ :taggings => { :comment => 'first' } }], - [{ :posts => { :title => ['misc post by bob', 'misc post by mary'] } }] + [Tagging.reflect_on_association(:tag).scope, Post.reflect_on_association(:first_blue_tags).scope], + [Post.reflect_on_association(:first_taggings).scope], + [Author.reflect_on_association(:misc_posts).scope] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags).conditions + actual = Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain assert_equal expected, actual expected = [ - [{ :tags => { :name => 'Blue' } }, { :taggings => { :comment => 'first' } }, { :posts => { :title => ['misc post by bob', 'misc post by mary'] } }], + [ + Tagging.reflect_on_association(:blue_tag).scope, + Post.reflect_on_association(:first_blue_tags_2).scope, + Author.reflect_on_association(:misc_post_first_blue_tags_2).scope + ], [], [] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).conditions + actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain assert_equal expected, actual end @@ -254,10 +258,10 @@ class ReflectionTest < ActiveRecord::TestCase end def test_association_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, {}, Author) + reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } - through = ActiveRecord::Reflection::ThroughReflection.new(:fuu, :edge, {}, Author) + through = ActiveRecord::Reflection::ThroughReflection.new(:fuu, :edge, nil, {}, Author) through.stubs(:source_reflection).returns(stub_everything(:options => {}, :class_name => 'Edge')) assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key } end @@ -268,7 +272,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_active_record_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, {}, Edge) + reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, nil, {}, Edge) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key } end @@ -286,32 +290,32 @@ class ReflectionTest < ActiveRecord::TestCase end def test_default_association_validation - assert AssociationReflection.new(:has_many, :clients, {}, Firm).validate? + assert AssociationReflection.new(:has_many, :clients, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_one, :client, {}, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, {}, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, {}, Firm).validate? + assert !AssociationReflection.new(:has_one, :client, nil, {}, Firm).validate? + assert !AssociationReflection.new(:belongs_to, :client, nil, {}, Firm).validate? + assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, {}, Firm).validate? end def test_always_validate_association_if_explicit - assert AssociationReflection.new(:has_one, :client, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, { :validate => true }, Firm).validate? + assert AssociationReflection.new(:has_one, :client, nil, { :validate => true }, Firm).validate? + assert AssociationReflection.new(:belongs_to, :client, nil, { :validate => true }, Firm).validate? + assert AssociationReflection.new(:has_many, :clients, nil, { :validate => true }, Firm).validate? + assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :validate => true }, Firm).validate? end def test_validate_association_if_autosave - assert AssociationReflection.new(:has_one, :client, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, { :autosave => true }, Firm).validate? + assert AssociationReflection.new(:has_one, :client, nil, { :autosave => true }, Firm).validate? + assert AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? + assert AssociationReflection.new(:has_many, :clients, nil, { :autosave => true }, Firm).validate? + assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true }, Firm).validate? end def test_never_validate_association_if_explicit - assert !AssociationReflection.new(:has_one, :client, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_many, :clients, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, { :autosave => true, :validate => false }, Firm).validate? + assert !AssociationReflection.new(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !AssociationReflection.new(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? end def test_foreign_key @@ -319,16 +323,68 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "category_id", Post.reflect_on_association(:categorizations).foreign_key.to_s end - def test_through_reflection_conditions_do_not_modify_other_reflections - orig_conds = Post.reflect_on_association(:first_blue_tags_2).conditions.inspect - Author.reflect_on_association(:misc_post_first_blue_tags_2).conditions - assert_equal orig_conds, Post.reflect_on_association(:first_blue_tags_2).conditions.inspect + def test_through_reflection_scope_chain_does_not_modify_other_reflections + orig_conds = Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect + Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain + assert_equal orig_conds, Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect end def test_symbol_for_class_name assert_equal Client, Firm.reflect_on_association(:unsorted_clients_with_symbol).klass end + def test_join_table + category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) + product = Struct.new(:table_name, :pluralize_table_names).new('products', true) + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection.stubs(:klass).returns(category) + assert_equal 'categories_products', reflection.join_table + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection.stubs(:klass).returns(product) + assert_equal 'categories_products', reflection.join_table + end + + def test_join_table_with_common_prefix + category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) + product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true) + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection.stubs(:klass).returns(category) + assert_equal 'catalog_categories_products', reflection.join_table + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection.stubs(:klass).returns(product) + assert_equal 'catalog_categories_products', reflection.join_table + end + + def test_join_table_with_different_prefix + category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) + page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true) + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, page) + reflection.stubs(:klass).returns(category) + assert_equal 'catalog_categories_content_pages', reflection.join_table + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :pages, nil, {}, category) + reflection.stubs(:klass).returns(page) + assert_equal 'catalog_categories_content_pages', reflection.join_table + end + + def test_join_table_can_be_overridden + category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) + product = Struct.new(:table_name, :pluralize_table_names).new('products', true) + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, { :join_table => 'product_categories' }, product) + reflection.stubs(:klass).returns(category) + assert_equal 'product_categories', reflection.join_table + + reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, { :join_table => 'product_categories' }, category) + reflection.stubs(:klass).returns(product) + assert_equal 'product_categories', reflection.join_table + end + private def assert_reflection(klass, association, options) assert reflection = klass.reflect_on_association(association) diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb new file mode 100644 index 0000000000..90c690e266 --- /dev/null +++ b/activerecord/test/cases/relation/where_test.rb @@ -0,0 +1,19 @@ +require "cases/helper" +require 'models/post' + +module ActiveRecord + class WhereTest < ActiveRecord::TestCase + fixtures :posts + + def test_where_error + assert_raises(ActiveRecord::StatementInvalid) do + Post.where(:id => { 'posts.author_id' => 10 }).first + end + end + + def test_where_with_table_name + post = Post.first + assert_equal post, Post.where(:posts => { 'id' => post.id }).first + end + end +end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index edf38cb7a3..d318dab1e1 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -53,8 +53,8 @@ class RelationScopingTest < ActiveRecord::TestCase end def test_scoped_find_last_preserves_scope - lowest_salary = Developer.first :order => "salary ASC" - highest_salary = Developer.first :order => "salary DESC" + lowest_salary = Developer.order("salary ASC").first + highest_salary = Developer.order("salary DESC").first Developer.order("salary").scoping do assert_equal highest_salary, Developer.last @@ -84,7 +84,7 @@ class RelationScopingTest < ActiveRecord::TestCase def test_scope_select_concatenates Developer.select("id, name").scoping do - developer = Developer.select('id, salary').where("name = 'David'").first + developer = Developer.select('salary').where("name = 'David'").first assert_equal 80000, developer.salary assert developer.has_attribute?(:id) assert developer.has_attribute?(:name) @@ -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 @@ -254,11 +254,6 @@ class HasManyScopingTest< ActiveRecord::TestCase assert_equal 2, @welcome.comments.search_by_type('Comment').size end - def test_forwarding_to_dynamic_finders - assert_equal 4, Comment.find_all_by_type('Comment').size - assert_equal 2, @welcome.comments.find_all_by_type('Comment').size - end - def test_nested_scope_finder Comment.where('1=0').scoping do assert_equal 0, @welcome.comments.count @@ -300,12 +295,6 @@ class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase assert_equal 'a category...', @welcome.categories.what_are_you end - def test_forwarding_to_dynamic_finders - assert_equal 4, Category.find_all_by_type('SpecialCategory').size - assert_equal 0, @welcome.categories.find_all_by_type('SpecialCategory').size - assert_equal 2, @welcome.categories.find_all_by_type('Category').size - end - def test_nested_scope_finder Category.where('1=0').scoping do assert_equal 0, @welcome.categories.count @@ -323,8 +312,8 @@ class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts def test_default_scope - expected = Developer.find(:all, :order => 'salary DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.find(: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,56 +351,42 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_default_scope_with_conditions_string - assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.find(:all).map(&:id).sort + assert_equal Developer.where(name: 'David').map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort assert_equal nil, DeveloperCalledDavid.create!.name end def test_default_scope_with_conditions_hash - assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.find(:all).map(&:id).sort + assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort assert_equal 'Jamis', DeveloperCalledJamis.create!.name end 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_method_scope - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } - assert_equal expected, received - end - - def test_nested_scope - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do - DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - end - assert_equal expected, received - end - def test_scope_overwrites_default - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.by_name.find(: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 @@ -422,22 +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_nested_exclusive_scope - expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do - DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - end - assert_equal expected, received - end - - def test_order_in_default_scope_should_prevail - expected = Developer.find(:all, :order => 'salary desc').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.find(:all, :order => 'salary').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 @@ -505,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 @@ -541,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 ac6dee3c6a..6399111be6 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -19,33 +19,17 @@ module ActiveRecord assert !relation.loaded, 'relation is not loaded' end - def test_single_values - assert_equal [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :uniq].map(&:to_s).sort, - Relation::SINGLE_VALUE_METHODS.map(&:to_s).sort + 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.each do |method| + (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end - end - - def test_association_methods - assert_equal [:includes, :eager_load, :preload].map(&:to_s).sort, - Relation::ASSOCIATION_METHODS.map(&:to_s).sort - end - - def test_initialize_association_methods - relation = Relation.new :a, :b - Relation::ASSOCIATION_METHODS.each do |method| - assert_equal [], relation.send("#{method}_values"), method.to_s - end - end - - def test_multi_value_methods - assert_equal [:select, :group, :order, :joins, :where, :having, :bind, :references].map(&:to_s).sort, - Relation::MULTI_VALUE_METHODS.map(&:to_s).sort + assert_equal({}, relation.create_with_value) end def test_multi_value_initialize @@ -64,19 +48,19 @@ module ActiveRecord relation = Relation.new :a, :b assert_equal({}, relation.where_values_hash) - relation.where_values << :hello + relation.where! :hello assert_equal({}, relation.where_values_hash) end def test_has_values relation = Relation.new Post, Post.arel_table - relation.where_values << relation.table[:id].eq(10) + relation.where! relation.table[:id].eq(10) assert_equal({:id => 10}, relation.where_values_hash) end def test_values_wrong_table relation = Relation.new Post, Post.arel_table - relation.where_values << Comment.arel_table[:id].eq(10) + relation.where! Comment.arel_table[:id].eq(10) assert_equal({}, relation.where_values_hash) end @@ -85,7 +69,7 @@ module ActiveRecord left = relation.table[:id].eq(10) right = relation.table[:id].eq(10) combine = left.and right - relation.where_values << combine + relation.where! combine assert_equal({}, relation.where_values_hash) end @@ -108,7 +92,7 @@ module ActiveRecord def test_create_with_value_with_wheres relation = Relation.new Post, Post.arel_table - relation.where_values << relation.table[:id].eq(10) + relation.where! relation.table[:id].eq(10) relation.create_with_value = {:hello => 'world'} assert_equal({:hello => 'world', :id => 10}, relation.scope_for_create) end @@ -118,7 +102,7 @@ module ActiveRecord relation = Relation.new Post, Post.arel_table assert_equal({}, relation.scope_for_create) - relation.where_values << relation.table[:id].eq(10) + relation.where! relation.table[:id].eq(10) assert_equal({}, relation.scope_for_create) relation.create_with_value = {:hello => 'world'} @@ -132,7 +116,7 @@ module ActiveRecord def test_eager_load_values relation = Relation.new :a, :b - relation.eager_load_values << :b + relation.eager_load! :b assert relation.eager_loading? end @@ -149,10 +133,128 @@ module ActiveRecord assert_equal ['foo'], relation.references_values end - def test_apply_finder_options_takes_references + test 'merging a hash into a relation' do relation = Relation.new :a, :b - relation = relation.apply_finder_options(:references => :foo) - assert_equal ['foo'], relation.references_values + relation = relation.merge where: :lol, readonly: true + + assert_equal [:lol], relation.where_values + assert_equal true, relation.readonly_value + end + + test 'merging an empty hash into a relation' do + assert_equal [], Relation.new(:a, :b).merge({}).where_values + end + + test 'merging a hash with unknown keys raises' do + assert_raises(ArgumentError) { Relation::HashMerger.new(nil, omg: 'lol') } + end + + test '#values returns a dup of the values' do + relation = Relation.new(:a, :b).where! :foo + values = relation.values + + values[:where] = nil + assert_not_nil relation.where_values + end + + test 'relations can be created with a values hash' do + relation = Relation.new(:a, :b, where: [:foo]) + assert_equal [:foo], relation.where_values + end + + test 'merging a single where value' do + relation = Relation.new(:a, :b) + relation.merge!(where: :foo) + assert_equal [:foo], relation.where_values + end + + test 'merging a hash interpolates conditions' do + klass = stub + klass.stubs(:sanitize_sql).with(['foo = ?', 'bar']).returns('foo = bar') + + relation = Relation.new(klass, :b) + relation.merge!(where: ['foo = ?', 'bar']) + assert_equal ['foo = bar'], relation.where_values + end + end + + class RelationMutationTest < ActiveSupport::TestCase + def relation + @relation ||= Relation.new :a, :b + end + + (Relation::MULTI_VALUE_METHODS - [:references, :extending]).each do |method| + test "##{method}!" do + assert relation.public_send("#{method}!", :foo).equal?(relation) + assert_equal [:foo], relation.public_send("#{method}_values") + end + end + + test '#references!' do + assert relation.references!(:foo).equal?(relation) + assert relation.references_values.include?('foo') + end + + test 'extending!' do + mod, mod2 = Module.new, Module.new + + assert relation.extending!(mod).equal?(relation) + 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 + relation.extending! + assert_equal [], relation.extending_values + end + + (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method| + test "##{method}!" do + assert relation.public_send("#{method}!", :foo).equal?(relation) + assert_equal :foo, relation.public_send("#{method}_value") + end + end + + test '#from!' do + assert relation.from!('foo').equal?(relation) + assert_equal ['foo', nil], relation.from_value + end + + test '#lock!' do + assert relation.lock!('foo').equal?(relation) + assert_equal 'foo', relation.lock_value + end + + test '#reorder!' do + relation = self.relation.order('foo') + + assert relation.reorder!('bar').equal?(relation) + assert_equal ['bar'], relation.order_values + assert relation.reordering_value + end + + test 'reverse_order!' do + assert relation.reverse_order!.equal?(relation) + assert relation.reverse_order_value + relation.reverse_order! + assert !relation.reverse_order_value + end + + test 'create_with!' do + assert relation.create_with!(foo: 'bar').equal?(relation) + assert_equal({foo: 'bar'}, relation.create_with_value) + end + + test 'merge!' do + 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 0471d03f3b..b91423351e 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -4,11 +4,11 @@ require 'models/tagging' require 'models/post' require 'models/topic' require 'models/comment' -require 'models/reply' require 'models/author' require 'models/comment' require 'models/entrant' require 'models/developer' +require 'models/reply' require 'models/company' require 'models/bird' require 'models/car' @@ -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 } @@ -133,6 +133,13 @@ class RelationTest < ActiveRecord::TestCase assert topics.loaded? end + def test_finding_with_subquery + relation = Topic.where(:approved => true) + assert_equal relation.to_a, Topic.select('*').from(relation).to_a + assert_equal relation.to_a, Topic.select('subquery.*').from(relation).to_a + assert_equal relation.to_a, Topic.select('a.*').from(relation, :a).to_a + end + def test_finding_with_conditions assert_equal ["David"], Author.where(:name => 'David').map(&:name) assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name) @@ -158,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 @@ -211,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 @@ -228,6 +235,44 @@ class RelationTest < ActiveRecord::TestCase end end + def test_none_chainable_to_existing_scope_extension_method + assert_no_queries do + assert_equal 1, Topic.anonymous_extension.none.one + end + end + + def test_none_chained_to_methods_firing_queries_straight_to_db + assert_no_queries do + assert_equal [], Developer.none.pluck(:id) # => uses select_all + assert_equal 0, Developer.none.delete_all + assert_equal 0, Developer.none.update_all(:name => 'David') + assert_equal 0, Developer.none.delete(1) + assert_equal false, Developer.none.exists?(1) + end + end + + def test_null_relation_content_size_methods + assert_no_queries do + assert_equal 0, Developer.none.size + assert_equal 0, Developer.none.count + assert_equal true, Developer.none.empty? + assert_equal false, Developer.none.any? + assert_equal false, Developer.none.many? + end + end + + def test_null_relation_calculations_methods + assert_no_queries do + assert_equal 0, Developer.none.count + assert_equal nil, Developer.none.calculate(:average, 'salary') + end + end + + def test_null_relation_metadata_methods + assert_equal "", Developer.none.to_sql + assert_equal({}, Developer.none.where_values_hash) + end + def test_joins_with_nil_argument assert_nothing_raised { DependentFirm.joins(nil).first } end @@ -249,7 +294,7 @@ class RelationTest < ActiveRecord::TestCase end def test_find_on_hash_conditions - assert_equal Topic.find(:all, :conditions => {:approved => false}), 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 @@ -262,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] @@ -289,21 +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 DeveloperOrderedBySalary.scoped.respond_to?(:all_ordered_by_name) - 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 @@ -323,7 +367,7 @@ class RelationTest < ActiveRecord::TestCase assert posts.first.comments.first end - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do + assert_queries(2) do posts = Post.preload(:comments).order('posts.id') assert posts.first.comments.first end @@ -333,12 +377,12 @@ class RelationTest < ActiveRecord::TestCase assert posts.first.author end - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do + assert_queries(2) do posts = Post.preload(:author).order('posts.id') assert posts.first.author end - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 3) do + assert_queries(3) do posts = Post.preload(:author, :comments).order('posts.id') assert posts.first.author assert posts.first.comments.first @@ -351,8 +395,8 @@ class RelationTest < ActiveRecord::TestCase assert posts.first.comments.first end - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - posts = Post.scoped.includes(:comments).order('posts.id') + assert_queries(2) do + posts = Post.all.includes(:comments).order('posts.id') assert posts.first.comments.first end @@ -361,7 +405,7 @@ class RelationTest < ActiveRecord::TestCase assert posts.first.author end - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 3) do + assert_queries(3) do posts = Post.includes(:author, :comments).order('posts.id') assert posts.first.author assert posts.first.comments.first @@ -369,18 +413,18 @@ class RelationTest < ActiveRecord::TestCase end def test_default_scope_with_conditions_string - assert_equal Developer.find_all_by_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.find_all_by_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 def test_default_scoping_finder_methods developers = DeveloperCalledDavid.order('id').map(&:id).sort - assert_equal Developer.find_all_by_name('David').map(&:id).sort, developers + assert_equal Developer.where(name: 'David').map(&:id).sort, developers end def test_loading_with_one_association @@ -404,15 +448,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.find(1).last_comment, post.last_comment end - def test_dynamic_find_by_attributes_should_yield_found_object - david = authors(:david) - yielded_value = nil - Author.find_by_name(david.name) do |author| - yielded_value = author - end - assert_equal david, yielded_value - end - def test_dynamic_find_by_attributes david = authors(:david) author = Author.preload(:taggings).find_by_id(david.id) @@ -422,48 +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') } - end - - def test_dynamic_find_all_by_attributes - authors = Author.scoped - - davids = authors.find_all_by_name('David') - assert_kind_of Array, davids - assert_equal [authors(:david)], davids - end - - def test_dynamic_find_or_initialize_by_attributes - authors = Author.scoped - - lifo = authors.find_or_initialize_by_name('Lifo') - assert_equal "Lifo", lifo.name - assert !lifo.persisted? - - assert_equal authors(:david), authors.find_or_initialize_by_name(:name => 'David') - end - - def test_dynamic_find_or_create_by_attributes - authors = Author.scoped - - lifo = authors.find_or_create_by_name('Lifo') - assert_equal "Lifo", lifo.name - assert lifo.persisted? - - assert_equal authors(:david), authors.find_or_create_by_name(:name => 'David') + 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 @@ -486,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 @@ -521,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 @@ -540,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 @@ -549,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 @@ -559,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 @@ -570,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 @@ -580,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 @@ -589,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 @@ -600,6 +607,7 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.exists?(authors(:mary).id) assert ! davids.exists?("42") assert ! davids.exists?(42) + assert ! davids.exists?(davids.new) fake = Author.where(:name => 'fake author') assert ! fake.exists? @@ -607,7 +615,7 @@ class RelationTest < ActiveRecord::TestCase end def test_last - authors = Author.scoped + authors = Author.all assert_equal authors(:bob), authors.last end @@ -644,6 +652,18 @@ class RelationTest < ActiveRecord::TestCase assert davids.loaded? end + def test_delete_all_limit_error + 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 @@ -656,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 } @@ -673,10 +712,8 @@ class RelationTest < ActiveRecord::TestCase end def test_relation_merging_with_preload - ActiveRecord::IdentityMap.without do - [Post.scoped.merge(Post.preload(:author)), Post.preload(:author).merge(Post.scoped)].each do |posts| - assert_queries(2) { assert posts.first.author } - end + [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts| + assert_queries(2) { assert posts.first.author } end end @@ -685,8 +722,16 @@ class RelationTest < ActiveRecord::TestCase assert_equal 1, comments.count end + def test_relation_merging_with_association + assert_queries(2) do # one for loading post, and another one merged query + post = Post.where(:body => 'Such a lovely day').first + comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments) + assert_equal 1, comments.count + end + end + def test_count - posts = Post.scoped + posts = Post.all assert_equal 11, posts.count assert_equal 11, posts.count(:all) @@ -697,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) @@ -708,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 @@ -720,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? @@ -772,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? @@ -798,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 @@ -820,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() @@ -832,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 @@ -854,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 @@ -866,7 +911,7 @@ class RelationTest < ActiveRecord::TestCase end def test_create_bang - birds = Bird.scoped + birds = Bird.all assert_raises(ActiveRecord::RecordInvalid) { birds.create! } @@ -1008,32 +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 - end - - def test_extensions_with_except - assert_equal 2, Topic.named_extension.order(:author_name).except(:order).two + 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 - end - - def test_extensions_with_only - assert_equal 2, Topic.named_extension.order(:author_name).only(:order).two + assert_equal Post.limit(1).to_a.first, all_posts.first end def test_anonymous_extension @@ -1054,39 +1091,29 @@ class RelationTest < ActiveRecord::TestCase end def test_order_by_relation_attribute - assert_equal Post.order(Post.arel_table[:title]).all, Post.order("title").all - end - - def test_order_with_find_with_order - assert_equal 'zyke', CoolCar.order('name desc').find(:first, :order => 'id').name - assert_equal 'zyke', FastCar.order('name desc').find(:first, :order => 'id').name + 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', CoolCar.order_using_old_style.limit(1).first.name - assert_equal 'zyke', FastCar.order_using_new_style.limit(1).first.name - assert_equal 'zyke', FastCar.order_using_old_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.find(:first, :order => 'id asc') + 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.find(:first, :order => 'id asc') + FastCar.all.merge!(:order => 'id asc').first end - assert_equal 'zyke', car2.name + assert_equal 'honda', car2.name end def test_unscoped_block_style assert_equal 'honda', CoolCar.unscoped { CoolCar.order_using_new_style.limit(1).first.name} - assert_equal 'honda', CoolCar.unscoped { CoolCar.order_using_old_style.limit(1).first.name} - assert_equal 'honda', FastCar.unscoped { FastCar.order_using_new_style.limit(1).first.name} - assert_equal 'honda', FastCar.unscoped { FastCar.order_using_old_style.limit(1).first.name} end def test_intersection_with_array @@ -1097,12 +1124,8 @@ class RelationTest < ActiveRecord::TestCase assert_equal [rails_author], relation & [rails_author] end - def test_removing_limit_with_options - assert_not_equal 1, Post.limit(1).all(:limit => nil).count - 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 @@ -1126,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 @@ -1224,4 +1251,134 @@ class RelationTest < ActiveRecord::TestCase scope = Post.order('foo(comments.body)') assert_equal [], scope.references_values end + + def test_presence + topics = Topic.all + + # the first query is triggered because there are no topics yet. + assert_queries(1) { assert topics.present? } + + # checking if there are topics is used before you actually display them, + # thus it shouldn't invoke an extra count query. + assert_no_queries { assert topics.present? } + assert_no_queries { assert !topics.blank? } + + # shows count of topics and loops after loading the query should not trigger extra queries either. + assert_no_queries { topics.size } + assert_no_queries { topics.length } + assert_no_queries { topics.each } + + # count always trigger the COUNT query. + assert_queries(1) { topics.count } + + assert topics.loaded? + end + + test "find_by with hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by(author_id: 2) + end + + test "find_by with non-hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by("author_id = 2") + end + + test "find_by with multi-arg conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by('author_id = ?', 2) + end + + test "find_by returns nil if the record is missing" do + assert_equal nil, Post.all.find_by("1 = 0") + end + + test "find_by doesn't have implicit ordering" do + assert_sql(/^((?!ORDER).)*$/) { Post.find_by(author_id: 2) } + end + + test "find_by! with hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by!(author_id: 2) + end + + test "find_by! with non-hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by!("author_id = 2") + end + + test "find_by! with multi-arg conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by!('author_id = ?', 2) + end + + test "find_by! doesn't have implicit ordering" do + assert_sql(/^((?!ORDER).)*$/) { Post.find_by!(author_id: 2) } + end + + test "find_by! raises RecordNotFound if the record is missing" do + assert_raises(ActiveRecord::RecordNotFound) do + Post.all.find_by!("1 = 0") + end + end + + test "loaded relations cannot be mutated by multi value methods" do + relation = Post.all + relation.to_a + + assert_raises(ActiveRecord::ImmutableRelation) do + relation.where! 'foo' + end + end + + test "loaded relations cannot be mutated by single value methods" do + relation = Post.all + relation.to_a + + assert_raises(ActiveRecord::ImmutableRelation) do + relation.limit! 5 + end + end + + test "loaded relations cannot be mutated by merge!" do + relation = Post.all + relation.to_a + + assert_raises(ActiveRecord::ImmutableRelation) do + relation.merge! where: 'foo' + end + end + + test "relations show the records in #inspect" do + relation = Post.limit(2) + assert_equal "#<ActiveRecord::Relation [#{Post.limit(2).map(&:inspect).join(', ')}]>", relation.inspect + end + + test "relations limit the records in #inspect at 10" do + relation = Post.limit(11) + assert_equal "#<ActiveRecord::Relation [#{Post.limit(10).map(&:inspect).join(', ')}, ...]>", relation.inspect + end + + test "already-loaded relations don't perform a new query in #inspect" do + relation = Post.limit(2) + relation.to_a + + expected = "#<ActiveRecord::Relation [#{Post.limit(2).map(&:inspect).join(', ')}]>" + + assert_no_queries do + assert_equal expected, relation.inspect + end + end + + test 'using a custom table affects the wheres' do + table_alias = Post.arel_table.alias('omg_posts') + + relation = ActiveRecord::Relation.new Post, table_alias + relation.where!(:foo => "bar") + + node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first + assert_equal table_alias, node.relation + end + + test '#load' do + relation = Post.all + assert_queries(1) do + assert_equal relation, relation.load + end + assert_no_queries { relation.to_a } + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index abeb56fd3f..68e88372f3 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -182,7 +182,16 @@ 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 + index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip + if current_adapter?(:PostgreSQLAdapter) + assert_equal 'add_index "companies", ["firm_id", "type"], :name => "company_partial_index", :where => "(rating > 10)"', index_definition + else + assert_equal 'add_index "companies", ["firm_id", "type"], :name => "company_partial_index"', index_definition + end end def test_schema_dump_should_honor_nonstandard_primary_keys @@ -227,6 +236,48 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dump_includes_json_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_json_data_type"} =~ output + assert_match %r|t.json "json_data", :default => {}|, output + end + end + + def test_schema_dump_includes_inet_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_network_address"} =~ output + assert_match %r{t.inet "inet_address"}, output + end + end + + def test_schema_dump_includes_cidr_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_network_address"} =~ output + assert_match %r{t.cidr "cidr_address"}, output + end + end + + def test_schema_dump_includes_macaddr_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_network_address"} =~ output + assert_match %r{t.macaddr "macaddr_address"}, output + end + end + + def test_schema_dump_includes_uuid_shorthand_definition + output = standard_dump + if %r{create_table "poistgresql_uuids"} =~ output + assert_match %r{t.uuid "guid"}, output + end + end + + def test_schema_dump_includes_hstores_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_hstores"} =~ output + assert_match %r[t.hstore "hash_store", :default => {}], output + end + end + def test_schema_dump_includes_tsvector_shorthand_definition output = standard_dump if %r{create_table "postgresql_tsvectors"} =~ output @@ -257,4 +308,36 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump assert_match %r{create_table "subscribers", :id => false}, output end + + class CreateDogMigration < ActiveRecord::Migration + def up + create_table("dogs") do |t| + t.column :name, :string + end + add_index "dogs", [:name] + end + def down + drop_table("dogs") + end + end + + def test_schema_dump_with_table_name_prefix_and_suffix + original, $stdout = $stdout, StringIO.new + ActiveRecord::Base.table_name_prefix = 'foo_' + ActiveRecord::Base.table_name_suffix = '_bar' + + migration = CreateDogMigration.new + migration.migrate(:up) + + output = standard_dump + assert_no_match %r{create_table "foo_.+_bar"}, output + assert_no_match %r{create_index "foo_.+_bar"}, output + assert_no_match %r{create_table "schema_migrations"}, output + ensure + migration.migrate(:down) + + ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' + $stdout = original + end + end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index a4c065e667..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}") 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.rb b/activerecord/test/cases/session_store/sql_bypass.rb deleted file mode 100644 index 7402b2afd6..0000000000 --- a/activerecord/test/cases/session_store/sql_bypass.rb +++ /dev/null @@ -1,56 +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_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 40520d6da2..2741f223da 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -3,15 +3,17 @@ require 'models/admin' require 'models/admin/user' class StoreTest < ActiveRecord::TestCase + fixtures :'admin/users' + setup do - @john = Admin::User.create(:name => 'John Doe', :color => 'black', :remember_login => true) + @john = Admin::User.create!(:name => 'John Doe', :color => 'black', :remember_login => true, :height => 'tall', :is_a_good_guy => true) end test "reading store attributes through accessors" do assert_equal 'black', @john.color assert_nil @john.homepage end - + test "writing store attributes through accessors" do @john.color = 'red' @john.homepage = '37signals.com' @@ -32,6 +34,17 @@ class StoreTest < ActiveRecord::TestCase assert @john.settings_changed? end + test "updating the store populates the changed array correctly" do + @john.color = 'red' + assert_equal 'black', @john.settings_change[0]['color'] + assert_equal 'red', @john.settings_change[1]['color'] + end + + test "updating the store won't mark it as changed if an attribute isn't changed" do + @john.color = @john.color + assert !@john.settings_changed? + end + test "object initialization with not nullable column" do assert_equal true, @john.remember_login end @@ -40,4 +53,83 @@ class StoreTest < ActiveRecord::TestCase @john.remember_login = false assert_equal false, @john.remember_login end + + test "preserve store attributes data in HashWithIndifferentAccess format without any conversion" do + @john.json_data = HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy') + @john.height = 'low' + assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess) + assert_equal 'low', @john.json_data[:height] + assert_equal 'low', @john.json_data['height'] + assert_equal 'heavy', @john.json_data[:weight] + assert_equal 'heavy', @john.json_data['weight'] + end + + test "convert store attributes from Hash to HashWithIndifferentAccess saving the data and access attributes indifferently" do + user = Admin::User.find_by_name('Jamis') + assert_equal 'symbol', user.settings[:symbol] + assert_equal 'symbol', user.settings['symbol'] + assert_equal 'string', user.settings[:string] + assert_equal 'string', user.settings['string'] + assert_equal true, user.settings.instance_of?(ActiveSupport::HashWithIndifferentAccess) + + user.height = 'low' + assert_equal 'symbol', user.settings[:symbol] + assert_equal 'symbol', user.settings['symbol'] + assert_equal 'string', user.settings[:string] + assert_equal 'string', user.settings['string'] + assert_equal true, user.settings.instance_of?(ActiveSupport::HashWithIndifferentAccess) + end + + test "convert store attributes from any format other than Hash or HashWithIndifferent access losing the data" do + @john.json_data = "somedata" + @john.height = 'low' + assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess) + assert_equal 'low', @john.json_data[:height] + assert_equal 'low', @john.json_data['height'] + assert_equal false, @john.json_data.delete_if { |k, v| k == 'height' }.any? + end + + test "reading store attributes through accessors encoded with JSON" do + assert_equal 'tall', @john.height + assert_nil @john.weight + end + + test "writing store attributes through accessors encoded with JSON" do + @john.height = 'short' + @john.weight = 'heavy' + + assert_equal 'short', @john.height + assert_equal 'heavy', @john.weight + end + + test "accessing attributes not exposed by accessors encoded with JSON" do + @john.json_data['somestuff'] = 'somecoolstuff' + @john.save + + assert_equal 'somecoolstuff', @john.reload.json_data['somestuff'] + end + + test "updating the store will mark it as changed encoded with JSON" do + @john.height = 'short' + assert @john.json_data_changed? + end + + test "object initialization with not nullable column encoded with JSON" do + assert_equal true, @john.is_a_good_guy + end + + test "writing with not nullable column encoded with JSON" do + @john.is_a_good_guy = false + assert_equal false, @john.is_a_good_guy + end + + test "all stored attributes are returned" do + assert_equal [:color, :homepage, :favorite_food], Admin::User.stored_attributes[:settings] + end + + test "stores_attributes are class level settings" do + assert_raise(NoMethodError) { @john.stored_attributes = Hash.new } + assert_raise(NoMethodError) { @john.stored_attributes } + end + end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb new file mode 100644 index 0000000000..4f3489b7a5 --- /dev/null +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -0,0 +1,302 @@ +require 'cases/helper' + +module ActiveRecord + module DatabaseTasksSetupper + def setup + @mysql_tasks, @postgresql_tasks, @sqlite_tasks = stub, stub, stub + ActiveRecord::Tasks::MySQLDatabaseTasks.stubs(:new).returns @mysql_tasks + ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stubs(:new).returns @postgresql_tasks + ActiveRecord::Tasks::SQLiteDatabaseTasks.stubs(:new).returns @sqlite_tasks + end + end + + ADAPTERS_TASKS = { + :mysql => :mysql_tasks, + :mysql2 => :mysql_tasks, + :postgresql => :postgresql_tasks, + :sqlite3 => :sqlite_tasks + } + + class DatabaseTasksRegisterTask < ActiveRecord::TestCase + def test_register_task + klazz = Class.new do + def initialize(*arguments); end + def structure_dump(filename); end + end + instance = klazz.new + + klazz.stubs(:new).returns instance + instance.expects(:structure_dump).with("awesome-file.sql") + + ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz) + ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => :foo}, "awesome-file.sql") + end + end + + class DatabaseTasksCreateTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_create") do + eval("@#{v}").expects(:create) + ActiveRecord::Tasks::DatabaseTasks.create 'adapter' => k + end + end + end + + class DatabaseTasksCreateAllTest < ActiveRecord::TestCase + def setup + @configurations = {'development' => {'database' => 'my-db'}} + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_ignores_configurations_without_databases + @configurations['development'].merge!('database' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:create).never + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_ignores_remote_databases + @configurations['development'].merge!('host' => 'my.server.tld') + $stderr.stubs(:puts).returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:create).never + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_warning_for_remote_databases + @configurations['development'].merge!('host' => 'my.server.tld') + + $stderr.expects(:puts).with('This task only modifies local databases. my-db is on a remote host.') + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_creates_configurations_with_local_ip + @configurations['development'].merge!('host' => '127.0.0.1') + + ActiveRecord::Tasks::DatabaseTasks.expects(:create) + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_creates_configurations_with_local_host + @configurations['development'].merge!('host' => 'localhost') + + ActiveRecord::Tasks::DatabaseTasks.expects(:create) + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + + def test_creates_configurations_with_blank_hosts + @configurations['development'].merge!('host' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:create) + + ActiveRecord::Tasks::DatabaseTasks.create_all + end + end + + class DatabaseTasksCreateCurrentTest < ActiveRecord::TestCase + def setup + @configurations = { + 'development' => {'database' => 'dev-db'}, + 'test' => {'database' => 'test-db'}, + 'production' => {'database' => 'prod-db'} + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_creates_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'prod-db') + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new('production') + ) + end + + def test_creates_test_database_when_environment_is_database + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'dev-db') + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'test-db') + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new('development') + ) + end + + def test_establishes_connection_for_the_given_environment + ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true + + ActiveRecord::Base.expects(:establish_connection).with('development') + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new('development') + ) + end + end + + class DatabaseTasksDropTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_drop") do + eval("@#{v}").expects(:drop) + ActiveRecord::Tasks::DatabaseTasks.drop 'adapter' => k + end + end + end + + class DatabaseTasksDropAllTest < ActiveRecord::TestCase + def setup + @configurations = {:development => {'database' => 'my-db'}} + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_ignores_configurations_without_databases + @configurations[:development].merge!('database' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_ignores_remote_databases + @configurations[:development].merge!('host' => 'my.server.tld') + $stderr.stubs(:puts).returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_warning_for_remote_databases + @configurations[:development].merge!('host' => 'my.server.tld') + + $stderr.expects(:puts).with('This task only modifies local databases. my-db is on a remote host.') + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_creates_configurations_with_local_ip + @configurations[:development].merge!('host' => '127.0.0.1') + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop) + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_creates_configurations_with_local_host + @configurations[:development].merge!('host' => 'localhost') + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop) + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + + def test_creates_configurations_with_blank_hosts + @configurations[:development].merge!('host' => nil) + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop) + + ActiveRecord::Tasks::DatabaseTasks.drop_all + end + end + + class DatabaseTasksDropCurrentTest < ActiveRecord::TestCase + def setup + @configurations = { + 'development' => {'database' => 'dev-db'}, + 'test' => {'database' => 'test-db'}, + 'production' => {'database' => 'prod-db'} + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_creates_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'prod-db') + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new('production') + ) + end + + def test_creates_test_database_when_environment_is_database + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'dev-db') + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'test-db') + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new('development') + ) + end + end + + + class DatabaseTasksPurgeTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_purge") do + eval("@#{v}").expects(:purge) + ActiveRecord::Tasks::DatabaseTasks.purge 'adapter' => k + end + end + end + + class DatabaseTasksCharsetTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_charset") do + eval("@#{v}").expects(:charset) + ActiveRecord::Tasks::DatabaseTasks.charset 'adapter' => k + end + end + end + + class DatabaseTasksCollationTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_collation") do + eval("@#{v}").expects(:collation) + ActiveRecord::Tasks::DatabaseTasks.collation 'adapter' => k + end + end + end + + class DatabaseTasksStructureDumpTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_structure_dump") do + eval("@#{v}").expects(:structure_dump).with("awesome-file.sql") + ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => k}, "awesome-file.sql") + end + end + end + + class DatabaseTasksStructureLoadTest < ActiveRecord::TestCase + include DatabaseTasksSetupper + + ADAPTERS_TASKS.each do |k, v| + define_method("test_#{k}_structure_load") do + eval("@#{v}").expects(:structure_load).with("awesome-file.sql") + ActiveRecord::Tasks::DatabaseTasks.structure_load({'adapter' => k}, "awesome-file.sql") + end + end + end +end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb new file mode 100644 index 0000000000..be591da8d6 --- /dev/null +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -0,0 +1,265 @@ +require 'cases/helper' + +module ActiveRecord + class MysqlDBCreateTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_without_database + ActiveRecord::Base.expects(:establish_connection). + with('adapter' => 'mysql', 'database' => nil) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_default_options + @connection.expects(:create_database). + with('my-app-db', {:charset => 'utf8', :collation => 'utf8_unicode_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_given_options + @connection.expects(:create_database). + with('my-app-db', {:charset => 'latin', :collation => 'latin_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge( + 'charset' => 'latin', 'collation' => 'latin_ci' + ) + end + + def test_establishes_connection_to_database + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end + + class MysqlDBCreateAsRootTest < ActiveRecord::TestCase + def setup + unless current_adapter?(:MysqlAdapter) + return skip("only tested on mysql") + end + + @connection = stub(:create_database => true, :execute => true) + @error = Mysql::Error.new "Invalid permissions" + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'wossname' + } + + $stdin.stubs(:gets).returns("secret\n") + $stdout.stubs(:print).returns(nil) + @error.stubs(:errno).returns(1045) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).raises(@error).then. + returns(true) + end + + def test_root_password_is_requested + skip "only if mysql is available" unless defined?(::Mysql) + $stdin.expects(:gets).returns("secret\n") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_connection_established_as_root + ActiveRecord::Base.expects(:establish_connection).with({ + 'adapter' => 'mysql', + 'database' => nil, + 'username' => 'root', + 'password' => 'secret' + }) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_database_created_by_root + @connection.expects(:create_database). + with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_grant_privileges_for_normal_user + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON my-app-db.* TO 'pat'@'localhost' IDENTIFIED BY 'wossname' WITH GRANT OPTION;") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_connection_established_as_normal_user + ActiveRecord::Base.expects(:establish_connection).returns do + ActiveRecord::Base.expects(:establish_connection).with({ + 'adapter' => 'mysql', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'secret' + }) + + raise @error + end + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_sends_output_to_stderr_when_other_errors + @error.stubs(:errno).returns(42) + + $stderr.expects(:puts).at_least_once.returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end + + class MySQLDBDropTest < ActiveRecord::TestCase + def setup + @connection = stub(:drop_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_mysql_database + ActiveRecord::Base.expects(:establish_connection).with @configuration + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + + def test_drops_database + @connection.expects(:drop_database).with('my-app-db') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + end + + class MySQLPurgeTest < ActiveRecord::TestCase + def setup + @connection = stub(:recreate_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'test-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_test_database + ActiveRecord::Base.expects(:establish_connection).with(:test) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_recreates_database_with_the_default_options + @connection.expects(:recreate_database). + with('test-db', {:charset => 'utf8', :collation => 'utf8_unicode_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_recreates_database_with_the_given_options + @connection.expects(:recreate_database). + with('test-db', {:charset => 'latin', :collation => 'latin_ci'}) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge( + 'charset' => 'latin', 'collation' => 'latin_ci' + ) + end + end + + class MysqlDBCharsetTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_charset + @connection.expects(:charset) + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + end + + class MysqlDBCollationTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_collation + @connection.expects(:collation) + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end + end + + class MySQLStructureDumpTest < ActiveRecord::TestCase + def setup + @connection = stub(:structure_dump => true) + @configuration = { + 'adapter' => 'mysql', + 'database' => 'test-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_structure_dump + filename = "awesome-file.sql" + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + @connection.expects(:structure_dump) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + assert File.exists?(filename) + ensure + FileUtils.rm(filename) + end + end + + class MySQLStructureLoadTest < ActiveRecord::TestCase + def setup + @connection = stub + @configuration = { + 'adapter' => 'mysql', + 'database' => 'test-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + Kernel.stubs(:system) + end + + def test_structure_load + filename = "awesome-file.sql" + Kernel.expects(:system).with('mysql', '--database', 'test-db', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}) + + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + end + +end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb new file mode 100644 index 0000000000..62acd53003 --- /dev/null +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -0,0 +1,226 @@ +require 'cases/helper' + +module ActiveRecord + class PostgreSQLDBCreateTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_postgresql_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'postgresql', + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_default_encoding + @connection.expects(:create_database). + with('my-app-db', @configuration.merge('encoding' => 'utf8')) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_given_encoding + @connection.expects(:create_database). + with('my-app-db', @configuration.merge('encoding' => 'latin')) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration. + merge('encoding' => 'latin') + end + + def test_creates_database_with_given_collation_and_ctype + @connection.expects(:create_database). + with('my-app-db', @configuration.merge('encoding' => 'utf8', 'collation' => 'ja_JP.UTF8', 'ctype' => 'ja_JP.UTF8')) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration. + merge('collation' => 'ja_JP.UTF8', 'ctype' => 'ja_JP.UTF8') + end + + def test_establishes_connection_to_new_database + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_db_create_with_error_prints_message + ActiveRecord::Base.stubs(:establish_connection).raises(Exception) + + $stderr.stubs(:puts).returns(true) + $stderr.expects(:puts). + with("Couldn't create database for #{@configuration.inspect}") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + end + + class PostgreSQLDBDropTest < ActiveRecord::TestCase + def setup + @connection = stub(:drop_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_postgresql_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'postgresql', + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + + def test_drops_database + @connection.expects(:drop_database).with('my-app-db') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + end + + class PostgreSQLPurgeTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true, :drop_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:clear_active_connections!).returns(true) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_clears_active_connections + ActiveRecord::Base.expects(:clear_active_connections!) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_establishes_connection_to_postgresql_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'postgresql', + 'database' => 'postgres', + 'schema_search_path' => 'public' + ) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_drops_database + @connection.expects(:drop_database).with('my-app-db') + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_creates_database + @connection.expects(:create_database). + with('my-app-db', @configuration.merge('encoding' => 'utf8')) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_establishes_connection + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + end + + class PostgreSQLDBCharsetTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_charset + @connection.expects(:encoding) + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + end + + class PostgreSQLDBCollationTest < ActiveRecord::TestCase + def setup + @connection = stub(:create_database => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_collation + @connection.expects(:collation) + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end + end + + class PostgreSQLStructureDumpTest < ActiveRecord::TestCase + def setup + @connection = stub(:structure_dump => true) + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + Kernel.stubs(:system) + end + + def test_structure_dump + filename = "awesome-file.sql" + Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{filename} my-app-db").returns(true) + @connection.expects(:schema_search_path).returns("foo") + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + assert File.exists?(filename) + ensure + FileUtils.rm(filename) + end + end + + class PostgreSQLStructureLoadTest < ActiveRecord::TestCase + def setup + @connection = stub + @configuration = { + 'adapter' => 'postgresql', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + Kernel.stubs(:system) + end + + def test_structure_dump + filename = "awesome-file.sql" + Kernel.expects(:system).with("psql -f #{filename} my-app-db") + + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + end + +end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb new file mode 100644 index 0000000000..7209c0f14d --- /dev/null +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -0,0 +1,191 @@ +require 'cases/helper' +require 'pathname' + +module ActiveRecord + class SqliteDBCreateTest < ActiveRecord::TestCase + def setup + @database = 'db_create.sqlite3' + @connection = stub :connection + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + + File.stubs(:exist?).returns(false) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_checks_database_exists + File.expects(:exist?).with(@database).returns(false) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_when_file_exists + File.stubs(:exist?).returns(true) + + $stderr.expects(:puts).with("#{@database} already exists") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_with_file_does_nothing + File.stubs(:exist?).returns(true) + $stderr.stubs(:puts).returns(nil) + + ActiveRecord::Base.expects(:establish_connection).never + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_establishes_a_connection + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + + def test_db_create_with_error_prints_message + ActiveRecord::Base.stubs(:establish_connection).raises(Exception) + + $stderr.stubs(:puts).returns(true) + $stderr.expects(:puts). + with("Couldn't create database for #{@configuration.inspect}") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + end + end + + class SqliteDBDropTest < ActiveRecord::TestCase + def setup + @database = "db_create.sqlite3" + @path = stub(:to_s => '/absolute/path', :absolute? => true) + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + + Pathname.stubs(:new).returns(@path) + File.stubs(:join).returns('/former/relative/path') + FileUtils.stubs(:rm).returns(true) + end + + def test_creates_path_from_database + Pathname.expects(:new).with(@database).returns(@path) + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + + def test_removes_file_with_absolute_path + File.stubs(:exist?).returns(true) + @path.stubs(:absolute?).returns(true) + + FileUtils.expects(:rm).with('/absolute/path') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + + def test_generates_absolute_path_with_given_root + @path.stubs(:absolute?).returns(false) + + File.expects(:join).with('/rails/root', @path). + returns('/former/relative/path') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + + def test_removes_file_with_relative_path + File.stubs(:exist?).returns(true) + @path.stubs(:absolute?).returns(false) + + FileUtils.expects(:rm).with('/former/relative/path') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + end + end + + class SqliteDBCharsetTest < ActiveRecord::TestCase + def setup + @database = 'db_create.sqlite3' + @connection = stub :connection + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + + File.stubs(:exist?).returns(false) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_charset + @connection.expects(:encoding) + ActiveRecord::Tasks::DatabaseTasks.charset @configuration, '/rails/root' + end + end + + class SqliteDBCollationTest < ActiveRecord::TestCase + def setup + @database = 'db_create.sqlite3' + @connection = stub :connection + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + + File.stubs(:exist?).returns(false) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_collation + assert_raise NoMethodError do + ActiveRecord::Tasks::DatabaseTasks.collation @configuration, '/rails/root' + end + end + end + + class SqliteStructureDumpTest < ActiveRecord::TestCase + def setup + @database = "db_create.sqlite3" + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + end + + def test_structure_dump + dbfile = @database + filename = "awesome-file.sql" + + ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, '/rails/root' + assert File.exists?(dbfile) + assert File.exists?(filename) + ensure + FileUtils.rm_f(filename) + FileUtils.rm_f(dbfile) + end + end + + class SqliteStructureLoadTest < ActiveRecord::TestCase + def setup + @database = "db_create.sqlite3" + @configuration = { + 'adapter' => 'sqlite3', + 'database' => @database + } + end + + def test_structure_load + dbfile = @database + filename = "awesome-file.sql" + + open(filename, 'w') { |f| f.puts("select datetime('now', 'localtime');") } + ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, '/rails/root' + assert File.exists?(dbfile) + ensure + FileUtils.rm_f(filename) + FileUtils.rm_f(dbfile) + end + end +end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 94a13d386c..f3f7054794 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation' ActiveSupport::Deprecation.silence do require 'active_record/test_case' end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 447aa29ffe..bb034848e1 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -11,7 +11,7 @@ class TimestampTest < ActiveRecord::TestCase def setup @developer = Developer.first - @developer.update_attribute(:updated_at, Time.now.prev_month) + @developer.update_columns(updated_at: Time.now.prev_month) @previously_updated_at = @developer.updated_at end @@ -114,9 +114,12 @@ class TimestampTest < ActiveRecord::TestCase end def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute - Pet.belongs_to :owner, :touch => :happy_at + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Pet'; end + belongs_to :owner, :touch => :happy_at + end - pet = Pet.first + pet = klass.first owner = pet.owner previously_owner_happy_at = owner.happy_at @@ -124,42 +127,41 @@ class TimestampTest < ActiveRecord::TestCase pet.save assert_not_equal previously_owner_happy_at, pet.owner.happy_at - ensure - Pet.belongs_to :owner, :touch => true end def test_touching_a_record_with_a_belongs_to_that_uses_a_counter_cache_should_update_the_parent - Pet.belongs_to :owner, :counter_cache => :use_count, :touch => true + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Pet'; end + belongs_to :owner, :counter_cache => :use_count, :touch => true + end - pet = Pet.first + pet = klass.first owner = pet.owner - owner.update_attribute(:happy_at, 3.days.ago) + owner.update_columns(happy_at: 3.days.ago) previously_owner_updated_at = owner.updated_at pet.name = "I'm a parrot" pet.save assert_not_equal previously_owner_updated_at, pet.owner.updated_at - ensure - Pet.belongs_to :owner, :touch => true end def test_touching_a_record_touches_parent_record_and_grandparent_record - Toy.belongs_to :pet, :touch => true - Pet.belongs_to :owner, :touch => true + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Toy'; end + belongs_to :pet, :touch => true + end - toy = Toy.first + toy = klass.first pet = toy.pet owner = pet.owner time = 3.days.ago - owner.update_column(:updated_at, time) + owner.update_columns(updated_at: time) toy.touch owner.reload assert_not_equal time, owner.updated_at - ensure - Toy.belongs_to :pet end def test_timestamp_attributes_for_create diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index f8b3e01a49..961ba8d9ba 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -287,6 +287,74 @@ class TransactionObserverCallbacksTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end + assert topic.id.nil? + assert !topic.persisted? assert_equal %w{ after_rollback }, topic.history end + + class TopicWithManualRollbackObserverAttached < ActiveRecord::Base + self.table_name = :topics + def history + @history ||= [] + end + end + + class TopicWithManualRollbackObserverAttachedObserver < ActiveRecord::Observer + def after_save(record) + record.history.push "after_save" + raise ActiveRecord::Rollback + end + end + + def test_after_save_called_with_manual_rollback + assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer' + + topic = TopicWithManualRollbackObserverAttached.new + + assert !topic.save + assert_equal nil, topic.id + assert !topic.persisted? + assert_equal %w{ after_save }, topic.history + end + def test_after_save_called_with_manual_rollback_bang + assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer' + + topic = TopicWithManualRollbackObserverAttached.new + + topic.save! + assert_equal nil, topic.id + assert !topic.persisted? + assert_equal %w{ after_save }, topic.history + end +end + +class SaveFromAfterCommitBlockTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class TopicWithSaveInCallback < ActiveRecord::Base + self.table_name = :topics + after_commit :cache_topic, :on => :create + after_commit :call_update, :on => :update + attr_accessor :cached, :record_updated + + def call_update + self.record_updated = true + end + + def cache_topic + unless cached + self.cached = true + self.save + else + self.cached = false + end + end + end + + def test_after_commit_in_save + topic = TopicWithSaveInCallback.new() + topic.save + assert_equal true, topic.cached + assert_equal true, topic.record_updated + end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 203dd054f1..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 @@ -362,6 +358,16 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_rollback_when_saving_a_frozen_record + topic = Topic.new(:title => 'test') + topic.freeze + e = assert_raise(RuntimeError) { topic.save } + assert_equal "can't modify frozen Hash", e.message + assert !topic.persisted?, 'not persisted' + assert_nil topic.id + assert topic.frozen?, 'not frozen' + end + def test_restore_active_record_state_for_all_records_in_a_transaction topic_1 = Topic.new(:title => 'test_1') topic_2 = Topic.new(:title => 'test_2') @@ -451,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/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index c72b7b35cd..7ac34bc71e 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -86,7 +86,7 @@ class AssociationValidationTest < ActiveRecord::TestCase assert !r.valid? assert r.errors[:topic].any? - r.topic = Topic.find :first + r.topic = Topic.first assert r.valid? end diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index 628029f8df..a8e513d81f 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -8,6 +8,16 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase I18n.backend = I18n::Backend::Simple.new end + def reset_i18n_load_path + @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + yield + ensure + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + end + # validates_associated: generate_message(attr_name, :invalid, :message => custom_message, :value => value) def test_generate_message_invalid_with_default_message assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :value => 'title') @@ -35,4 +45,13 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase assert_equal "Validation failed: Title is invalid, Title can't be blank", ActiveRecord::RecordInvalid.new(topic).message end + test "RecordInvalid exception translation falls back to the :errors namespace" do + reset_i18n_load_path do + I18n.backend.store_translations 'en', :errors => {:messages => {:record_invalid => 'fallback message'}} + topic = Topic.new + topic.errors.add(:title, :blank) + assert_equal "fallback message", ActiveRecord::RecordInvalid.new(topic).message + end + end + end 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 79442d68b0..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.find(t_utf8, :select => 'LOWER(title) AS title').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" @@ -261,10 +277,10 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert i1.errors[:value].any?, "Should not be empty" end - def test_validates_uniqueness_inside_with_scope + def test_validates_uniqueness_inside_scoping Topic.validates_uniqueness_of(:title) - Topic.send(:with_scope, :find => { :conditions => { :author_name => "David" } }) do + Topic.where(:author_name => "David").scoping do t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary") assert t1.save t2 = Topic.new("title" => "I'm unique!", "author_name" => "David") @@ -325,4 +341,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert w6.errors[:city].any?, "Should have errors for city" assert_equal ["has already been taken"], w6.errors[:city], "Should have uniqueness message for city" end + + def test_validate_uniqueness_with_conditions + Topic.validates_uniqueness_of(:title, :conditions => Topic.where('approved = ?', true)) + Topic.create("title" => "I'm a topic", "approved" => true) + Topic.create("title" => "I'm an unapproved topic", "approved" => false) + + t3 = Topic.new("title" => "I'm a topic", "approved" => true) + assert !t3.valid?, "t3 shouldn't be valid" + + t4 = Topic.new("title" => "I'm an unapproved topic", "approved" => false) + assert t4.valid?, "t4 should be valid" + end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index e575a98170..b11b330374 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -93,30 +93,6 @@ class ValidationsTest < ActiveRecord::TestCase end end - def test_scoped_create_without_attributes - WrongReply.send(:with_scope, :create => {}) do - assert_raise(ActiveRecord::RecordInvalid) { WrongReply.create! } - end - end - - def test_create_with_exceptions_using_scope_for_protected_attributes - assert_nothing_raised do - ProtectedPerson.send(:with_scope, :create => { :first_name => "Mary" } ) do - person = ProtectedPerson.create! :addon => "Addon" - assert_equal person.first_name, "Mary", "scope should ignore attr_protected" - end - end - end - - def test_create_with_exceptions_using_scope_and_empty_attributes - assert_nothing_raised do - ProtectedPerson.send(:with_scope, :create => { :first_name => "Mary" } ) do - person = ProtectedPerson.create! - assert_equal person.first_name, "Mary", "should be ok when no attributes are passed to create!" - end - end - end - def test_create_without_validation reply = WrongReply.new assert !reply.save diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 88751a72f9..7249ae9e4d 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -74,33 +74,88 @@ end class DefaultXmlSerializationTest < ActiveRecord::TestCase def setup - @xml = Contact.new(:name => 'aaron stack', :age => 25, :avatar => 'binarydata', :created_at => Time.utc(2006, 8, 1), :awesome => false, :preferences => { :gem => 'ruby' }).to_xml + @contact = Contact.new( + :name => 'aaron stack', + :age => 25, + :avatar => 'binarydata', + :created_at => Time.utc(2006, 8, 1), + :awesome => false, + :preferences => { :gem => 'ruby' } + ) end def test_should_serialize_string - assert_match %r{<name>aaron stack</name>}, @xml + assert_match %r{<name>aaron stack</name>}, @contact.to_xml end def test_should_serialize_integer - assert_match %r{<age type="integer">25</age>}, @xml + assert_match %r{<age type="integer">25</age>}, @contact.to_xml end def test_should_serialize_binary - assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, @xml - assert_match %r{<avatar(.*)(type="binary")}, @xml - assert_match %r{<avatar(.*)(encoding="base64")}, @xml + xml = @contact.to_xml + assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml + assert_match %r{<avatar(.*)(type="binary")}, xml + assert_match %r{<avatar(.*)(encoding="base64")}, xml end def test_should_serialize_datetime - assert_match %r{<created-at type=\"datetime\">2006-08-01T00:00:00Z</created-at>}, @xml + assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml end def test_should_serialize_boolean - assert_match %r{<awesome type=\"boolean\">false</awesome>}, @xml + assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml end def test_should_serialize_hash - assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @xml + assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml + end + + def test_uses_serializable_hash_with_only_option + def @contact.serializable_hash(options=nil) + super(only: %w(name)) + end + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_no_match %r{age}, xml + assert_no_match %r{awesome}, xml + end + + def test_uses_serializable_hash_with_except_option + def @contact.serializable_hash(options=nil) + super(except: %w(age)) + end + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml + assert_no_match %r{age}, xml + end + + def test_does_not_include_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_no_match %r{<type}, xml + assert_no_match %r{ContactSti}, xml + end + + def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti + @contact = ContactSti.new(@contact.attributes) + assert_equal 'ContactSti', @contact.type + + def @contact.serializable_hash(options={}) + super({ except: %w(age) }.merge!(options)) + end + + xml = @contact.to_xml + assert_match %r{<name>aaron stack</name>}, xml + assert_no_match %r{age}, xml + assert_no_match %r{<type}, xml + assert_no_match %r{ContactSti}, xml end end @@ -109,7 +164,7 @@ class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"datetime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml ensure Time.zone = timezone end @@ -118,7 +173,7 @@ class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"datetime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml ensure Time.zone = timezone end @@ -152,7 +207,7 @@ class NilXmlSerializationTest < ActiveRecord::TestCase assert %r{<created-at (.*)></created-at>}.match(@xml) attributes = $1 assert_match %r{nil="true"}, attributes - assert_match %r{type="datetime"}, attributes + assert_match %r{type="dateTime"}, attributes end def test_should_serialize_boolean @@ -188,7 +243,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text - assert_equal "datetime" , xml.elements["//written-on"].attributes['type'] + assert_equal "dateTime" , xml.elements["//written-on"].attributes['type'] assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text @@ -198,7 +253,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase if current_adapter?(:SybaseAdapter) assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text - assert_equal "datetime" , xml.elements["//last-read"].attributes['type'] + assert_equal "dateTime" , xml.elements["//last-read"].attributes['type'] else # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) assert_equal "2004-04-15", xml.elements["//last-read"].text @@ -211,7 +266,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert_equal "boolean" , xml.elements["//approved"].attributes['type'] assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text - assert_equal "datetime" , xml.elements["//bonus-time"].attributes['type'] + assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type'] end end diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index f450efd839..479b8c050d 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -1,5 +1,7 @@ default_connection: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %> +with_manual_interventions: false + connections: jdbcderby: arunit: activerecord_unittest diff --git a/activerecord/test/fixtures/admin/users.yml b/activerecord/test/fixtures/admin/users.yml index 6f11f2509e..e2884beda5 100644 --- a/activerecord/test/fixtures/admin/users.yml +++ b/activerecord/test/fixtures/admin/users.yml @@ -5,3 +5,6 @@ david: jamis: name: Jamis account: signals37 + settings: + :symbol: symbol + string: string diff --git a/activerecord/test/fixtures/colleges.yml b/activerecord/test/fixtures/colleges.yml new file mode 100644 index 0000000000..27591e0c2c --- /dev/null +++ b/activerecord/test/fixtures/colleges.yml @@ -0,0 +1,3 @@ +FIU: + id: 1 + name: Florida International University 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/courses.yml b/activerecord/test/fixtures/courses.yml index 5ee1916003..de3a4a97e5 100644 --- a/activerecord/test/fixtures/courses.yml +++ b/activerecord/test/fixtures/courses.yml @@ -1,6 +1,7 @@ ruby: id: 1 name: Ruby Development + college: FIU java: id: 2 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/peoples_treasures.yml b/activerecord/test/fixtures/peoples_treasures.yml new file mode 100644 index 0000000000..a72b190d0c --- /dev/null +++ b/activerecord/test/fixtures/peoples_treasures.yml @@ -0,0 +1,3 @@ +michael_diamond: + rich_person_id: <%= ActiveRecord::Fixtures.identify(:michael) %> + treasure_id: <%= ActiveRecord::Fixtures.identify(:diamond) %> diff --git a/activerecord/test/fixtures/reserved_words/distincts_selects.yml b/activerecord/test/fixtures/reserved_words/distinct_select.yml index 90e8c95fef..d96779ade4 100644 --- a/activerecord/test/fixtures/reserved_words/distincts_selects.yml +++ b/activerecord/test/fixtures/reserved_words/distinct_select.yml @@ -1,11 +1,11 @@ -distincts_selects1: +distinct_select1: distinct_id: 1 select_id: 1 -distincts_selects2: +distinct_select2: distinct_id: 1 select_id: 2 -distincts_selects3: +distinct_select3: distinct_id: 2 select_id: 3 diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml index 93f48aedc4..2b042bd135 100644 --- a/activerecord/test/fixtures/topics.yml +++ b/activerecord/test/fixtures/topics.yml @@ -25,7 +25,7 @@ third: id: 3 title: The Third Topic of the day author_name: Carl - written_on: 2005-07-15t15:28:00.0099+01:00 + written_on: 2012-08-12t20:24:22.129346+00:00 content: I'm a troll approved: true replies_count: 1 diff --git a/activerecord/test/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/admin/user.rb b/activerecord/test/models/admin/user.rb index d0e628bd50..6c4eb03b06 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -1,5 +1,8 @@ class Admin::User < ActiveRecord::Base belongs_to :account store :settings, :accessors => [ :color, :homepage ] + store_accessor :settings, :favorite_food store :preferences, :accessors => [ :remember_login ] + store :json_data, :accessors => [ :height, :weight ], :coder => JSON + store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => JSON end diff --git a/activerecord/test/models/arunit2_model.rb b/activerecord/test/models/arunit2_model.rb new file mode 100644 index 0000000000..04b8b15d3d --- /dev/null +++ b/activerecord/test/models/arunit2_model.rb @@ -0,0 +1,3 @@ +class ARUnit2Model < ActiveRecord::Base + self.abstract_class = true +end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index d50e11d6c9..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,18 +131,17 @@ 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 - scope :relation_include_posts, includes(:posts) - scope :relation_include_tags, includes(:tags) + scope :relation_include_posts, -> { includes(:posts) } + scope :relation_include_tags, -> { includes(:tags) } attr_accessor :post_log after_initialize :set_post_log 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 888afc7604..0dc2fdd8ae 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,5 +1,5 @@ class Bulb < ActiveRecord::Base - default_scope where(:name => 'defaulty') + default_scope { where(:name => 'defaulty') } belongs_to :car attr_protected :car_id, :frickinawesome @@ -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 6ff1329d8e..ac42f444e1 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -1,28 +1,27 @@ 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 has_many :wheels, :as => :wheelable, :dependent => :destroy - scope :incl_tyres, includes(:tyres) - scope :incl_engines, includes(:engines) + scope :incl_tyres, -> { includes(:tyres) } + scope :incl_engines, -> { includes(:engines) } - scope :order_using_new_style, order('name asc') - scope :order_using_old_style, :order => 'name asc' + scope :order_using_new_style, -> { order('name asc') } end class CoolCar < Car - default_scope :order => 'name desc' + default_scope { order('name desc') } end class FastCar < Car - default_scope :order => 'name desc' + default_scope { order('name desc') } end diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 4bd980e606..6588531de6 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -12,7 +12,7 @@ end class SpecialCategorization < ActiveRecord::Base self.table_name = 'categorizations' - default_scope where(:special => true) + default_scope { where(:special => true) } belongs_to :author belongs_to :category diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index 02b85fd38a..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,9 +25,9 @@ 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, :conditions => { :name => 'General' } + scope :general, -> { where(:name => 'General') } end class SpecialCategory < Category diff --git a/activerecord/test/models/college.rb b/activerecord/test/models/college.rb new file mode 100644 index 0000000000..c7495d7deb --- /dev/null +++ b/activerecord/test/models/college.rb @@ -0,0 +1,5 @@ +require_dependency 'models/arunit2_model' + +class College < ARUnit2Model + has_many :courses +end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 88b139d931..4b2015fe01 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -1,16 +1,16 @@ class Comment < ActiveRecord::Base scope :limit_by, lambda {|l| limit(l) } - scope :containing_the_letter_e, :conditions => "comments.body LIKE '%e%'" - scope :not_again, where("comments.body NOT LIKE '%again%'") - scope :for_first_post, :conditions => { :post_id => 1 } - scope :for_first_author, - :joins => :post, - :conditions => { "posts.author_id" => 1 } - scope :created + scope :containing_the_letter_e, -> { where("comments.body LIKE '%e%'") } + 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, -> { all } belongs_to :post, :counter_cache => true has_many :ratings + belongs_to :first_post, :foreign_key => :post_id + has_many :children, :class_name => 'Comment', :foreign_key => :parent_id belongs_to :parent, :class_name => 'Comment', :counter_cache => :children_count @@ -19,13 +19,13 @@ class Comment < ActiveRecord::Base end def self.search_by_type(q) - self.find(:all, :conditions => ["#{QUOTED_TYPE} = ?", q]) + where("#{QUOTED_TYPE} = ?", q) end def self.all_as_method all end - scope :all_as_scope, {} + scope :all_as_scope, -> { all } end class SpecialComment < Comment diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index fbdfaa2c29..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 @@ -198,6 +210,11 @@ class Account < ActiveRecord::Base @destroyed_account_ids ||= Hash.new { |h,k| h[k] = [] } end + # Test private kernel method through collection proxy using has_many. + def self.open + where('firm_name = ?', '37signals') + end + before_destroy do |account| if account.firm Account.destroyed_account_ids[account.firm.id] << account.id diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 2c8c30efb4..eb2aedc425 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -7,11 +7,13 @@ module MyApplication end class Firm < Company - has_many :clients, :order => "id", :dependent => :destroy - has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC" - has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id" - has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id" - has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' + has_many :clients, -> { order("id") }, :dependent => :destroy + has_many :clients_sorted_desc, -> { order("id DESC") }, :class_name => "Client" + has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client" + has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client" + ActiveSupport::Deprecation.silence do + has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' + end has_one :account, :class_name => 'MyApplication::Billing::Account', :dependent => :destroy end diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index 3d15c7fbed..a1cb8d62b6 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -1,25 +1,40 @@ -class Contact < ActiveRecord::Base - establish_connection(:adapter => 'fake') +module ContactFakeColumns + def self.extended(base) + base.class_eval do + establish_connection(:adapter => 'fake') + + connection.tables = [table_name] + connection.primary_keys = { + table_name => 'id' + } + + column :name, :string + column :age, :integer + column :avatar, :binary + column :created_at, :datetime + column :awesome, :boolean + column :preferences, :string + column :alternative_id, :integer + + serialize :preferences - connection.tables = ['contacts'] - connection.primary_keys = { - 'contacts' => 'id' - } + belongs_to :alternative, :class_name => 'Contact' + end + end # mock out self.columns so no pesky db is needed for these tests - def self.column(name, sql_type = nil, options = {}) - connection.merge_column('contacts', name, sql_type, options) + def column(name, sql_type = nil, options = {}) + connection.merge_column(table_name, name, sql_type, options) end +end - column :name, :string - column :age, :integer - column :avatar, :binary - column :created_at, :datetime - column :awesome, :boolean - column :preferences, :string - column :alternative_id, :integer +class Contact < ActiveRecord::Base + extend ContactFakeColumns +end - serialize :preferences +class ContactSti < ActiveRecord::Base + extend ContactFakeColumns + column :type, :string - belongs_to :alternative, :class_name => 'Contact' + def type; 'ContactSti' end end diff --git a/activerecord/test/models/course.rb b/activerecord/test/models/course.rb index 8a40fa740d..f3d0e05ff7 100644 --- a/activerecord/test/models/course.rb +++ b/activerecord/test/models/course.rb @@ -1,3 +1,6 @@ -class Course < ActiveRecord::Base +require_dependency 'models/arunit2_model' + +class Course < ARUnit2Model + belongs_to :college has_many :entrants end diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 777f6b5ba0..7e8e82542f 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -1,7 +1,11 @@ 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 diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 4dc9fff9fd..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 - find(:first, :order => "id DESC") + order("id DESC").first end end module DeveloperProjectsAssociationExtension2 def find_least_recent - find(:first, :order => "id ASC") + order("id ASC").first end end class Developer < ActiveRecord::Base has_and_belongs_to_many :projects do def find_most_recent - find(:first, :order => "id DESC") + 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 - find(:first, :order => "id ASC") + order("id ASC").first end end @@ -45,7 +45,7 @@ class Developer < ActiveRecord::Base has_many :audit_logs - scope :jamises, :conditions => {:name => 'Jamis'} + scope :jamises, -> { where(:name => 'Jamis') } validates_inclusion_of :salary, :in => 50000..200000 validates_length_of :name, :within => 3..20 @@ -57,12 +57,6 @@ class Developer < ActiveRecord::Base def log=(message) audit_logs.build :message => message end - - def self.all_johns - self.with_exclusive_scope :find => where(:name => 'John') do - self.all - end - end end class AuditLog < ActiveRecord::Base @@ -88,31 +82,25 @@ end class DeveloperWithSelect < ActiveRecord::Base self.table_name = 'developers' - default_scope select('name') + default_scope { select('name') } end class DeveloperWithIncludes < ActiveRecord::Base self.table_name = 'developers' has_many :audit_logs, :foreign_key => :developer_id - default_scope includes(:audit_logs) + default_scope { includes(:audit_logs) } end class DeveloperOrderedBySalary < ActiveRecord::Base self.table_name = 'developers' - default_scope :order => 'salary DESC' - - scope :by_name, order('name DESC') + default_scope { order('salary DESC') } - def self.all_ordered_by_name - with_scope(:find => { :order => 'name DESC' }) do - find(:all) - end - end + scope :by_name, -> { order('name DESC') } end class DeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' - default_scope where("name = 'David'") + default_scope { where("name = 'David'") } end class LazyLambdaDeveloperCalledDavid < ActiveRecord::Base @@ -140,7 +128,7 @@ end class ClassMethodReferencingScopeDeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' - scope :david, where(:name => 'David') + scope :david, -> { where(:name => 'David') } def self.default_scope david @@ -149,40 +137,40 @@ end class LazyBlockReferencingScopeDeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' - scope :david, where(:name => 'David') + scope :david, -> { where(:name => 'David') } default_scope { david } end class DeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' - default_scope where(:name => 'Jamis') - scope :poor, where('salary < 150000') + default_scope { where(:name => 'Jamis') } + scope :poor, -> { where('salary < 150000') } end class PoorDeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' - default_scope where(:name => 'Jamis', :salary => 50000) + default_scope -> { where(:name => 'Jamis', :salary => 50000) } end class InheritedPoorDeveloperCalledJamis < DeveloperCalledJamis self.table_name = 'developers' - default_scope where(:salary => 50000) + default_scope -> { where(:salary => 50000) } end class MultiplePoorDeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' - default_scope where(:name => 'Jamis') - default_scope where(:salary => 50000) + default_scope -> { where(:name => 'Jamis') } + default_scope -> { where(:salary => 50000) } end module SalaryDefaultScope extend ActiveSupport::Concern - included { default_scope where(:salary => 50000) } + included { default_scope { where(:salary => 50000) } } end class ModuleIncludedPoorDeveloperCalledJamis < DeveloperCalledJamis @@ -193,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) + 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) @@ -209,21 +197,21 @@ end class EagerDeveloperWithLambdaDefaultScope < ActiveRecord::Base self.table_name = 'developers' - has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id' + has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects' default_scope lambda { includes(:projects) } end class EagerDeveloperWithBlockDefaultScope < ActiveRecord::Base self.table_name = 'developers' - has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id' + has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects' default_scope { includes(:projects) } end class EagerDeveloperWithCallableDefaultScope < ActiveRecord::Base self.table_name = 'developers' - has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id' + has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects' default_scope OpenStruct.new(:call => includes(:projects)) end diff --git a/activerecord/test/models/friendship.rb b/activerecord/test/models/friendship.rb new file mode 100644 index 0000000000..6b4f7acc38 --- /dev/null +++ b/activerecord/test/models/friendship.rb @@ -0,0 +1,4 @@ +class Friendship < ActiveRecord::Base + belongs_to :friend, class_name: 'Person' + belongs_to :follower, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :followers_count +end diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb index 3fcd5e4b69..6cfd443e75 100644 --- a/activerecord/test/models/liquid.rb +++ b/activerecord/test/models/liquid.rb @@ -1,5 +1,5 @@ class Liquid < ActiveRecord::Base self.table_name = :liquid - has_many :molecules, :uniq => true + has_many :molecules, -> { uniq } end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 11a0f4ff63..1134b09d8b 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -5,8 +5,8 @@ class Member < ActiveRecord::Base has_many :fellow_members, :through => :club, :source => :members has_one :club, :through => :current_membership has_one :selected_club, :through => :selected_membership, :source => :club - has_one :favourite_club, :through => :membership, :conditions => ["memberships.favourite = ?", true], :source => :club - has_one :hairy_club, :through => :membership, :conditions => {:clubs => {:name => "Moustache and Eyebrow Fancier Club"}}, :source => :club + has_one :favourite_club, -> { where "memberships.favourite = ?", true }, :through => :membership, :source => :club + has_one :hairy_club, -> { where :clubs => {:name => "Moustache and Eyebrow Fancier Club"} }, :through => :membership, :source => :club has_one :sponsor, :as => :sponsorable has_one :sponsor_club, :through => :sponsor has_one :member_detail @@ -24,9 +24,13 @@ class Member < ActiveRecord::Base has_one :club_category, :through => :club, :source => :category - has_many :current_memberships + has_many :current_memberships, -> { where :favourite => true } + has_many :clubs, :through => :current_memberships + has_one :club_through_many, :through => :current_memberships, :source => :club +end - has_many :current_memberships, :conditions => { :favourite => true } - has_many :clubs, :through => :current_memberships +class SelfMember < ActiveRecord::Base + self.table_name = "members" + has_and_belongs_to_many :friends, :class_name => "SelfMember", :join_table => "member_friends" end diff --git a/activerecord/test/models/organization.rb b/activerecord/test/models/organization.rb index 4a4111833f..72e7bade68 100644 --- a/activerecord/test/models/organization.rb +++ b/activerecord/test/models/organization.rb @@ -8,5 +8,5 @@ class Organization < ActiveRecord::Base has_one :author, :primary_key => :name has_one :author_owned_essay_category, :through => :author, :source => :owned_essay_category - scope :clubs, { :from => 'clubs' } + scope :clubs, -> { from('clubs') } end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index d2a0c6b40c..6e6ff29f77 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -1,16 +1,20 @@ class Person < ActiveRecord::Base has_many :readers + has_many :secure_readers has_one :reader has_many :posts, :through => :readers - has_many :posts_with_no_comments, :through => :readers, :source => :post, :include => :comments, - :conditions => 'comments.id is null', :references => :comments + has_many :secure_posts, :through => :secure_readers + 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 @@ -25,8 +29,8 @@ class Person < ActiveRecord::Base has_many :agents_posts, :through => :agents, :source => :posts has_many :agents_posts_authors, :through => :agents_posts, :source => :author - scope :males, :conditions => { :gender => 'M' } - scope :females, :conditions => { :gender => 'F' } + scope :males, -> { where(:gender => 'M') } + scope :females, -> { where(:gender => 'F') } end class PersonWithDependentDestroyJobs < ActiveRecord::Base @@ -83,3 +87,28 @@ class TightPerson < ActiveRecord::Base end class TightDescendant < TightPerson; end + +class RichPerson < ActiveRecord::Base + self.table_name = 'people' + + has_and_belongs_to_many :treasures, :join_table => 'peoples_treasures' +end + +class NestedPerson < ActiveRecord::Base + self.table_name = 'people' + + attr_accessible :first_name, :best_friend_first_name, :best_friend_attributes + attr_accessible :first_name, :gender, :comments, :as => :admin + attr_accessible :best_friend_attributes, :best_friend_first_name, :as => :admin + + has_one :best_friend, :class_name => 'NestedPerson', :foreign_key => :best_friend_id + accepts_nested_attributes_for :best_friend, :update_only => true + + def comments=(new_comments) + raise RuntimeError + end + + def best_friend_first_name=(new_name) + assign_attributes({ :best_friend_attributes => { :first_name => new_name } }) + end +end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 5e0f5323e6..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/possession.rb b/activerecord/test/models/possession.rb new file mode 100644 index 0000000000..ddf759113b --- /dev/null +++ b/activerecord/test/models/possession.rb @@ -0,0 +1,3 @@ +class Possession < ActiveRecord::Base + self.table_name = 'having' +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 1cab78d8c7..c995f59a15 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -5,15 +5,10 @@ class Post < ActiveRecord::Base end end - scope :containing_the_letter_a, where("body LIKE '%a%'") - scope :ranked_by_comments, order("comments_count DESC") + scope :containing_the_letter_a, -> { where("body LIKE '%a%'") } + scope :ranked_by_comments, -> { order("comments_count DESC") } scope :limit_by, lambda {|l| limit(l) } - scope :with_authors_at_address, lambda { |address| { - :conditions => [ 'authors.author_address_id = ?', address.id ], - :joins => 'JOIN authors ON authors.id = posts.author_id' - } - } belongs_to :author do def greeting @@ -21,24 +16,22 @@ 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, :conditions => {:comments => {:type => 'SpecialComment'} } - scope :with_very_special_comments, joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) - scope :with_post, lambda {|post_id| - { :joins => :comments, :conditions => {:comments => {:post_id => post_id} } } - } + scope :with_special_comments, -> { joins(:comments).where(:comments => {:type => 'SpecialComment'}) } + scope :with_very_special_comments, -> { joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) } + scope :with_post, ->(post_id) { joins(:comments).where(:comments => { :post_id => post_id }) } has_many :comments do def find_most_recent - find(:first, :order => "id DESC") + order("id DESC").first end def newest @@ -54,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 @@ -71,33 +65,30 @@ class Post < ActiveRecord::Base has_many :taggings, :as => :taggable has_many :tags, :through => :taggings do def add_joins_and_select - find :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' + 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 @@ -115,17 +106,22 @@ class Post < ActiveRecord::Base has_many :named_categories, :through => :standard_categorizations has_many :readers - has_many :readers_with_person, :include => :person, :class_name => "Reader" + has_many :secure_readers + 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 has_many :people_with_callbacks, :source=>:person, :through => :readers, :before_add => lambda {|owner, reader| log(:added, :before, reader.first_name) }, :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 @@ -169,7 +165,7 @@ end class FirstPost < ActiveRecord::Base self.table_name = 'posts' - default_scope where(:id => 1) + default_scope { where(:id => 1) } has_many :comments, :foreign_key => :post_id has_one :comment, :foreign_key => :post_id @@ -177,16 +173,16 @@ end class PostWithDefaultInclude < ActiveRecord::Base self.table_name = 'posts' - default_scope includes(:comments) + default_scope { includes(:comments) } has_many :comments, :foreign_key => :post_id end class PostWithDefaultScope < ActiveRecord::Base self.table_name = 'posts' - default_scope :order => :title + default_scope { order(:title) } end class SpecialPostWithDefaultScope < ActiveRecord::Base self.table_name = 'posts' - default_scope where(:id => [1, 5,6]) + default_scope { where(:id => [1, 5,6]) } end diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index efe1ce67da..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, {} + scope :all_as_scope, -> { all } end class SpecialProject < Project diff --git a/activerecord/test/models/reader.rb b/activerecord/test/models/reader.rb index 0207a2bd92..f5b6079bd2 100644 --- a/activerecord/test/models/reader.rb +++ b/activerecord/test/models/reader.rb @@ -3,3 +3,20 @@ class Reader < ActiveRecord::Base belongs_to :person, :inverse_of => :readers belongs_to :single_person, :class_name => 'Person', :foreign_key => :person_id, :inverse_of => :reader end + +class SecureReader < ActiveRecord::Base + self.table_name = "readers" + + belongs_to :secure_post, :class_name => "Post", :foreign_key => "post_id" + belongs_to :secure_person, :inverse_of => :secure_readers, :class_name => "Person", :foreign_key => "person_id" + + 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/reference.rb b/activerecord/test/models/reference.rb index c5af0b5d5f..561b431766 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -19,5 +19,5 @@ end class BadReference < ActiveRecord::Base self.table_name = 'references' - default_scope where(:favourite => false) + default_scope { where(:favourite => false) } end diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index 6adfe0ae3c..53bc95e5f2 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -1,7 +1,7 @@ require 'models/topic' class Reply < Topic - scope :base + scope :base, -> { scoped } belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true belongs_to :topic_with_primary_key, :class_name => "Topic", :primary_key => "title", :foreign_key => "parent_title", :counter_cache => "replies_count" 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 8bcb9df8a8..4b27c16681 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -1,21 +1,20 @@ class Topic < ActiveRecord::Base - scope :base + scope :base, -> { all } scope :written_before, lambda { |time| if time - { :conditions => ['written_on < ?', time] } + where 'written_on < ?', time end } - scope :approved, :conditions => {:approved => true} - scope :rejected, :conditions => {:approved => false} + 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, :conditions => {:author_name => 'lifo'} + scope :by_lifo, -> { where(:author_name => 'lifo') } + scope :replied, -> { where 'replies_count > 0' } - scope :approved_as_hash_condition, :conditions => {:topics => {:approved => true}} - scope 'approved_as_string', :conditions => {:approved => true} - scope :replied, :conditions => ['replies_count > 0'] - scope :anonymous_extension do + scope 'approved_as_string', -> { where(:approved => true) } + scope :anonymous_extension, -> { all } do def one 1 end @@ -32,18 +31,6 @@ class Topic < ActiveRecord::Base 2 end end - module MultipleExtensionOne - def extension_one - 1 - end - end - module MultipleExtensionTwo - def extension_two - 2 - end - end - scope :named_extension, :extend => NamedExtension - scope :multiple_extensions, :extend => [MultipleExtensionTwo, MultipleExtensionOne] has_many :replies, :dependent => :destroy, :foreign_key => "parent_id" has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title" @@ -71,6 +58,8 @@ class Topic < ActiveRecord::Base id end + alias_attribute :heading, :title + before_validation :before_validation_for_transaction before_save :before_save_for_transaction before_destroy :before_destroy_for_transaction diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb index 0377e50011..ddc7048a56 100644 --- a/activerecord/test/models/toy.rb +++ b/activerecord/test/models/toy.rb @@ -2,5 +2,5 @@ class Toy < ActiveRecord::Base self.primary_key = :toy_id belongs_to :pet - scope :with_pet, joins(:pet) + scope :with_pet, -> { joins(:pet) } 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/models/without_table.rb b/activerecord/test/models/without_table.rb index 184ab1649e..50c824e4ac 100644 --- a/activerecord/test/models/without_table.rb +++ b/activerecord/test/models/without_table.rb @@ -1,3 +1,3 @@ class WithoutTable < ActiveRecord::Base - default_scope where(:published => true) + default_scope -> { where(:published => true) } end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index ab2c7ccc10..24a43d7ece 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -1,5 +1,5 @@ ActiveRecord::Schema.define do - create_table :binary_fields, :force => true, :options => 'CHARACTER SET latin1' do |t| + create_table :binary_fields, :force => true do |t| t.binary :tiny_blob, :limit => 255 t.binary :normal_blob, :limit => 65535 t.binary :medium_blob, :limit => 16777215 @@ -32,4 +32,13 @@ CREATE TABLE collation_tests ( ) CHARACTER SET utf8 COLLATE utf8_general_ci SQL -end
\ No newline at end of file + ActiveRecord::Base.connection.execute <<-SQL +DROP TABLE IF EXISTS enum_tests; +SQL + + ActiveRecord::Base.connection.execute <<-SQL +CREATE TABLE enum_tests ( + enum_column ENUM('true','false') +) +SQL +end diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index a0adfe3752..802c08b819 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -1,5 +1,5 @@ ActiveRecord::Schema.define do - create_table :binary_fields, :force => true, :options => 'CHARACTER SET latin1' do |t| + create_table :binary_fields, :force => true do |t| t.binary :tiny_blob, :limit => 255 t.binary :normal_blob, :limit => 65535 t.binary :medium_blob, :limit => 16777215 @@ -43,4 +43,14 @@ CREATE TABLE collation_tests ( ) CHARACTER SET utf8 COLLATE utf8_general_ci SQL + ActiveRecord::Base.connection.execute <<-SQL +DROP TABLE IF EXISTS enum_tests; +SQL + + ActiveRecord::Base.connection.execute <<-SQL +CREATE TABLE enum_tests ( + enum_column ENUM('true','false') +) +SQL + end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 5cf9a207f3..2d8c0aa69a 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,8 +1,8 @@ ActiveRecord::Schema.define do - %w(postgresql_tsvectors postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings - postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones).each do |table_name| - execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" + %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids + postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name| + execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end execute 'DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE' @@ -10,6 +10,8 @@ ActiveRecord::Schema.define do execute "ALTER TABLE companies ALTER COLUMN id SET DEFAULT nextval('companies_nonstd_seq')" execute 'DROP SEQUENCE IF EXISTS companies_id_seq' + execute 'DROP FUNCTION IF EXISTS partitioned_insert_trigger()' + %w(accounts_id_seq developers_id_seq projects_id_seq topics_id_seq customers_id_seq orders_id_seq).each do |seq_name| execute "SELECT setval('#{seq_name}', 100)" end @@ -57,12 +59,38 @@ _SQL _SQL execute <<_SQL + CREATE TABLE postgresql_uuids ( + id SERIAL PRIMARY KEY, + guid uuid, + compact_guid uuid + ); +_SQL + + execute <<_SQL CREATE TABLE postgresql_tsvectors ( id SERIAL PRIMARY KEY, text_vector tsvector ); _SQL + if 't' == select_value("select 'hstore'=ANY(select typname from pg_type)") + execute <<_SQL + CREATE TABLE postgresql_hstores ( + id SERIAL PRIMARY KEY, + hash_store hstore default ''::hstore + ); +_SQL + end + + if 't' == select_value("select 'json'=ANY(select typname from pg_type)") + execute <<_SQL + CREATE TABLE postgresql_json_data_type ( + id SERIAL PRIMARY KEY, + json_data json default '{}'::json + ); +_SQL + end + execute <<_SQL CREATE TABLE postgresql_moneys ( id SERIAL PRIMARY KEY, @@ -116,6 +144,37 @@ _SQL ); _SQL +begin + execute <<_SQL + CREATE TABLE postgresql_partitioned_table_parent ( + id SERIAL PRIMARY KEY, + number integer + ); + CREATE TABLE postgresql_partitioned_table ( ) + INHERITS (postgresql_partitioned_table_parent); + + CREATE OR REPLACE FUNCTION partitioned_insert_trigger() + RETURNS TRIGGER AS $$ + BEGIN + INSERT INTO postgresql_partitioned_table VALUES (NEW.*); + RETURN NULL; + END; + $$ + LANGUAGE plpgsql; + + CREATE TRIGGER insert_partitioning_trigger + BEFORE INSERT ON postgresql_partitioned_table_parent + FOR EACH ROW EXECUTE PROCEDURE partitioned_insert_trigger(); +_SQL +rescue ActiveRecord::StatementInvalid => e + if e.message =~ /language "plpgsql" does not exist/ + execute "CREATE LANGUAGE 'plpgsql';" + retry + else + raise e + end +end + begin execute <<_SQL CREATE TABLE postgresql_xml_data_type ( diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index da0c4cecdd..b4e611cb09 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -37,8 +37,12 @@ ActiveRecord::Schema.define do create_table :admin_users, :force => true do |t| t.string :name - t.text :settings, :null => true - t.text :preferences, :null => false, :default => "" + 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 => true, :default => '', :limit => 1024 + t.string :json_data, :null => true, :limit => 1024 + t.string :json_data_empty, :null => true, :default => "", :limit => 1024 t.references :account end @@ -76,6 +80,7 @@ ActiveRecord::Schema.define do create_table :binaries, :force => true do |t| t.string :name t.binary :data + t.binary :short_data, :limit => 2048 end create_table :birds, :force => true do |t| @@ -91,6 +96,7 @@ ActiveRecord::Schema.define do create_table :booleans, :force => true do |t| t.boolean :value + t.boolean :has_fun, :null => false, :default => false end create_table :bulbs, :force => true do |t| @@ -165,16 +171,23 @@ 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, :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 @@ -262,11 +275,20 @@ 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 end + create_table :having, :force => true do |t| + t.string :where + end + create_table :guids, :force => true do |t| t.column :key, :string end @@ -353,6 +375,11 @@ ActiveRecord::Schema.define do t.string :extra_data end + create_table :member_friends, :force => true, :id => false do |t| + t.integer :member_id + t.integer :friend_id + end + create_table :memberships, :force => true do |t| t.datetime :joined_on t.integer :club_id, :member_id @@ -437,6 +464,7 @@ ActiveRecord::Schema.define do create_table :parrots, :force => true do |t| t.column :name, :string + t.column :color, :string t.column :parrot_sti_class, :string t.column :killer_id, :integer t.column :created_at, :datetime @@ -462,11 +490,17 @@ 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 end + create_table :peoples_treasures, :id => false, :force => true do |t| + t.column :rich_person_id, :integer + t.column :treasure_id, :integer + end + create_table :pets, :primary_key => :pet_id ,:force => true do |t| t.string :name t.integer :owner_id, :integer @@ -759,4 +793,9 @@ end Course.connection.create_table :courses, :force => true do |t| t.column :name, :string, :null => false + t.column :college_id, :integer +end + +College.connection.create_table :colleges, :force => true do |t| + t.column :name, :string, :null => false end diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index ea05b35fe0..e9ddeb32cf 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -1,5 +1,5 @@ ActiveRecord::Schema.define do - # For sqlite 3.1.0+, make a table with a autoincrement column + # For sqlite 3.1.0+, make a table with an autoincrement column if supports_autoincrement? create_table :table_with_autoincrement, :force => true do |t| t.column :name, :string diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index 60fea46fd3..92736e0ca9 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -1,5 +1,6 @@ require 'active_support/logger' -require_dependency 'models/course' +require 'models/college' +require 'models/course' module ARTest def self.connection_name @@ -11,10 +12,10 @@ module ARTest end def self.connect - puts "Using #{connection_name} with Identity Map #{ActiveRecord::IdentityMap.enabled? ? 'on' : 'off'}" + puts "Using #{connection_name}" ActiveRecord::Model.logger = ActiveSupport::Logger.new("debug.log") ActiveRecord::Model.configurations = connection_config ActiveRecord::Model.establish_connection 'arunit' - Course.establish_connection 'arunit2' + ARUnit2Model.establish_connection 'arunit2' end end |