diff options
Diffstat (limited to 'activerecord')
326 files changed, 10186 insertions, 7508 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 33ba77bca2..7e6ef27964 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,1736 +1,933 @@ -## Rails 4.0.0 (unreleased) ## +* ActiveRecord::Base#attribute_for_inspect now truncates long arrays (more than 10 elements) -* Uniqueness validation allows you to pass `:conditions` to limit - the constraint lookup. - - Example: - - validates_uniqueness_of :title, conditions: -> { where('approved = ?', true) } - - *Mattias Pfeiffer + Yves Senn* - -* `connection` is deprecated as an instance method. - This allows end-users to have a `connection` method on their models - without clashing with Active Record internals. - - *Ben Moss* - -* When copying migrations, preserve their magic comments and content encoding. - - *OZAWA Sakuro* - -* Fix `subclass_from_attrs` when `eager_load` is false. It cannot find - subclass because all classes are loaded automatically when it needs. - - *Dmitry Vorotilin* - -* When `:name` option is provided to `remove_index`, use it if there is no - index by the conventional name. + *Jan Bernacki* - For example, previously if an index was removed like so - `remove_index :values, column: :value, name: 'a_different_name'` - the generated SQL would not contain the specified index name, - and hence the migration would fail. - Fixes #8858. +* Allow for the name of the schema_migrations table to be configured. - *Ezekiel Smithburg* + *Jerad Phelps* -* Created block to by-pass the prepared statement bindings. - This will allow to compose fragments of large SQL statements to - avoid multiple round-trips between Ruby and the DB. +* Do not add to scope includes values from through associations. + Fixed bug when providing `includes` in through association scope, and fetching targets. Example: - - sql = Post.connection.unprepared_statement do - Post.first.comments.to_sql + class Vendor < ActiveRecord::Base + has_many :relationships, -> { includes(:user) } + has_many :users, through: :relationships end - *Cédric Fabianski* - -* Change the semantics of combining scopes to be the same as combining - class methods which return scopes. For example: - - class User < ActiveRecord::Base - scope :active, -> { where state: 'active' } - scope :inactive, -> { where state: 'inactive' } - end - - class Post < ActiveRecord::Base - def self.active - where state: 'active' - end - - def self.inactive - where state: 'inactive' - end - end - - ### BEFORE ### - - User.where(state: 'active').where(state: 'inactive') - # => SELECT * FROM users WHERE state = 'active' AND state = 'inactive' - - User.active.inactive - # => SELECT * FROM users WHERE state = 'inactive' - - Post.active.inactive - # => SELECT * FROM posts WHERE state = 'active' AND state = 'inactive' + vendor = Vendor.first - ### AFTER ### + # Before - User.active.inactive - # => SELECT * FROM posts WHERE state = 'active' AND state = 'inactive' + vendor.users.to_a # => Raises exception: not found `:user` for `User` - Before this change, invoking a scope would merge it into the current - scope and return the result. `Relation#merge` applies "last where - wins" logic to de-duplicate the conditions, but this lead to - confusing and inconsistent behaviour. This fixes that. + # After - If you really do want the "last where wins" logic, you can opt-in to - it like so: + vendor.users.to_a # => No exception is raised - User.active.merge(User.inactive) - Fixes #7365. + Fixes #12242, #9517, #10240. - *Neeraj Singh* and *Jon Leighton* + *Paul Nikitochkin* -* Expand `#cache_key` to consult all relevant updated timestamps. - - Previously only `updated_at` column was checked, now it will - consult other columns that received updated timestamps on save, - such as `updated_on`. When multiple columns are present it will - use the most recent timestamp. - Fixes #9033. - - *Brendon Murphy* - -* Throw `NotImplementedError` when trying to instantiate `ActiveRecord::Base` or an abstract class. - - *Aaron Weiner* - -* Warn when `rake db:structure:dump` with a mysl database and - `mysqldump` is not in the PATH or fails. - Fixes #9518. - - *Yves Senn* - -* Remove `connection#structure_dump`, which is no longer used. *Yves Senn* - -* Make it possible to execute migrations without a transaction even - if the database adapter supports DDL transactions. - Fixes #9483. +* Type cast json values on write, so that the value is consistent + with reading from the database. Example: - class ChangeEnum < ActiveRecord::Migration - disable_ddl_transaction! - - def up - execute "ALTER TYPE model_size ADD VALUE 'new_value'" - end - end + x = JsonDataType.new tags: {"string" => "foo", :symbol => :bar} - *Yves Senn* + # Before: + x.tags # => {"string" => "foo", :symbol => :bar} -* Assigning "0.0" to a nullable numeric column does not make it dirty. - Fixes #9034. + # After: + x.tags # => {"string" => "foo", "symbol" => "bar"} - Example: + *Severin Schoepke* - product = Product.create price: 0.0 - product.price = '0.0' - product.changed? # => false (this used to return true) - product.changes # => {} (this used to return { price: [0.0, 0.0] }) +* `ActiveRecord::Store` works together with PG `hstore` columns. + Fixes #12452. *Yves Senn* -* Added functionality to unscope relations in a relations chain. For - instance, if you are passed in a chain of relations as follows: +* Fix bug where `ActiveRecord::Store` used a global `Hash` to keep track of + all registered `stored_attributes`. Now every subclass of + `ActiveRecord::Base` has it's own `Hash`. - User.where(name: "John").order('id DESC') + *Yves Senn* - but you want to get rid of order, then this feature allows you to do: +* Save `has_one` association when primary key is manually set. - User.where(name: "John").order('id DESC').unscope(:order) - == User.where(name: "John") + Fixes #12302. - The .unscope() function is more general than the .except() method because - .except() only works on the relation it is acting on. However, .unscope() - works for any relation in the entire relation chain. + *Lauro Caetano* - *John Wang* +* Allow any version of BCrypt when using `has_secure_password`. -* Postgresql timestamp with time zone (timestamptz) datatype now returns a - ActiveSupport::TimeWithZone instance instead of a string + *Mike Perham* - *Troy Kruthoff* +* Sub-query generated for `Relation` passed as array condition did not take in account + bind values and have invalid syntax. -* The `#append` method for collection associations behaves like`<<`. - `#prepend` is not defined and `<<` or `#append` should be used. - Fixes #7364. + Generate sub-query with inline bind values. - *Yves Senn* + Fixes #12586. -* Added support for creating a table via Rails migration generator. - For example, + *Paul Nikitochkin* - rails g migration create_books title:string content:text +* Fix a bug where rake db:structure:load crashed when the path contained + spaces. - will generate a migration that creates a table called books with - the listed attributes, without creating a model. + *Kevin Mook* - *Sammy Larbi* +* `ActiveRecord::QueryMethods#unscope` unscopes negative equality -* Fix bug that raises the wrong exception when the exception handled by PostgreSQL adapter - doesn't respond to `#result`. - Fixes #8617. + Allows you to call `#unscope` on a relation with negative equality + operators, i.e. `Arel::Nodes::NotIn` and `Arel::Nodes::NotEqual` that have + been generated through the use of `where.not`. - *kennyj* + *Eric Hankins* -* Support PostgreSQL specific column types when using `change_table`. - Fixes #9480. +* Raise an exception when model without primary key calls `.find_with_ids`. - Example: + *Shimpei Makimoto* - change_table :authors do |t| - t.hstore :books - t.json :metadata - end +* Make `Relation#empty?` use `exists?` instead of `count`. - *Yves Senn* + *Szymon Nowak* -* Revert 408227d9c5ed7d, 'quote numeric'. This introduced some regressions. +* `rake db:structure:dump` no longer crashes when the port was specified as `Fixnum`. - *Steve Klabnik* + *Kenta Okamoto* -* Fix calculation of `db_runtime` property in - `ActiveRecord::Railties::ControllerRuntime#cleanup_view_runtime`. - Previously, after raising `ActionView::MissingTemplate`, `db_runtime` was - not populated. - Fixes #9215. +* `NullRelation#pluck` takes a list of columns - *Igor Fedoronchuk* + The method signature in `NullRelation` was updated to mimic that in + `Calculations`. -* Do not try to touch invalid (and thus not persisted) parent record - for a `belongs_to :parent, touch: true` association + *Derek Prior* - *Olek Janiszewski* +* `scope_chain` should not be mutated for other reflections. -* Fix when performing an ordered join query. The bug only - affected queries where the order was given with a symbol. - Fixes #9275. + Currently `scope_chain` uses same array for building different + `scope_chain` for different associations. During processing + these arrays are sometimes mutated and because of in-place + mutation the changed `scope_chain` impacts other reflections. - Example: + Fix is to dup the value before adding to the `scope_chain`. - # This will expand the order :name to "authors".name. - Author.joins(:books).where('books.published = 1').order(:name) + Fixes #3882. + *Neeraj Singh* -## Rails 4.0.0.beta1 (February 25, 2013) ## +* Prevent the inversed association from being reloaded on save. -* Fix overriding of attributes by `default_scope` on `ActiveRecord::Base#dup`. + Fixes #9499. - *Hiroshige UMINO* + *Dmitry Polushkin* -* Update queries now use prepared statements. - - *Olli Rissanen* - -* Fixing issue #8345. Now throwing an error when one attempts to touch a - new object that has not yet been persisted. For instance: +* Generate subquery for `Relation` if it passed as array condition for `where` + method. Example: - ball = Ball.new - ball.touch :updated_at # => raises error + # Before + Blog.where('id in (?)', Blog.where(id: 1)) + # => SELECT "blogs".* FROM "blogs" WHERE "blogs"."id" = 1 + # => SELECT "blogs".* FROM "blogs" WHERE (id IN (1)) - It is not until the ball object has been persisted that it can be touched. - This follows the behavior of update_column. + # After + Blog.where('id in (?)', Blog.where(id: 1).select(:id)) + # => SELECT "blogs".* FROM "blogs" + # WHERE "blogs"."id" IN (SELECT "blogs"."id" FROM "blogs" WHERE "blogs"."id" = 1) - *John Wang* + Fixes #12415. -* Preloading ordered `has_many :through` associations no longer applies - invalid ordering to the `:through` association. - Fixes #8663. + *Paul Nikitochkin* - *Yves Senn* +* For missed association exception message + which is raised in `ActiveRecord::Associations::Preloader` class + added owner record class name in order to simplify to find problem code. -* The auto explain feature has been removed. This feature was - activated by configuring `config.active_record.auto_explain_threshold_in_seconds`. - The configuration option was deprecated and has no more effect. + *Paul Nikitochkin* - You can still use `ActiveRecord::Relation#explain` to see the EXPLAIN output for - any given relation. +* `has_and_belongs_to_many` is now transparently implemented in terms of + `has_many :through`. Behavior should remain the same, if not, it is a bug. - *Yves Senn* - -* The `:on` option for `after_commit` and `after_rollback` now - accepts an Array of actions. - Fixes #988. - - Example: - - after_commit :update_cache on: [:create, :update] +* `create_savepoint`, `rollback_to_savepoint` and `release_savepoint` accept + a savepoint name. *Yves Senn* -* Rename related indexes on `rename_table` and `rename_column`. This - does not affect indexes with custom names. +* Make `next_migration_number` accessible for third party generators. *Yves Senn* -* Prevent the creation of indices with too long names, which cause - internal operations to fail (sqlite3 adapter only). The method - `allowed_index_name_length` defines the length limit enforced by - rails. It's value defaults to `index_name_length` but can vary per adapter. - Fixes #8264. +* Objects instantiated using a null relationship will now retain the + attributes of the where clause. - *Yves Senn* + Fixes #11676, #11675, #11376. -* Fixing issue #776. + *Paul Nikitochkin*, *Peter Brown*, *Nthalk* - Memory bloat in transactions is handled by having the transaction hold only - the AR objects which it absolutely needs to know about. These are the AR - objects with callbacks (they need to be updated as soon as something in the - transaction occurs). +* Fixed `ActiveRecord::Associations::CollectionAssociation#find` + when using `has_many` association with `:inverse_of` and finding an array of one element, + it should return an array of one element too. - All other AR objects can be updated lazily by keeping a reference to a - TransactionState object. If an AR object gets inside a transaction, then - the transaction will add its TransactionState to the AR object. When the - user makes a call to some attribute on an AR object (which has no - callbacks) associated with a transaction, the AR object will call the - sync_with_transaction_state method and make sure it is up to date with the - transaction. After it has synced with the transaction state, the AR object - will return the attribute that was requested. + *arthurnn* - Most of the logic in the changes are used to handle multiple transactions, - in which case the AR object has to recursively follow parent pointers of - TransactionState objects. +* Callbacks on has_many should access the in memory parent if a inverse_of is set. - *John Wang* + *arthurnn* -* Descriptive error message when the necessary AR adapter gem was not found. - Fixes #7313. +* `ActiveRecord::ConnectionAdapters.string_to_time` respects + string with timezone (e.g. Wed, 04 Sep 2013 20:30:00 JST). - *Yves Senn* + Fixes #12278. -* Active Record now raises an error when blank arguments are passed to query - methods for which blank arguments do not make sense. + *kennyj* + +* Calling `update_attributes` will now throw an `ArgumentError` whenever it + gets a `nil` argument. More specifically, it will throw an error if the + argument that it gets passed does not respond to to `stringify_keys`. Example: - Post.includes() # => raises error + @my_comment.update_attributes(nil) # => raises ArgumentError *John Wang* -* Simplified type casting code for timezone aware attributes to use the - `in_time_zone` method if it is available. This introduces a subtle change - of behavior when using `Date` instances as they are directly converted to - `ActiveSupport::TimeWithZone` instances without first being converted to - `Time` instances. For example: - - # Rails 3.2 behavior - >> Date.today.to_time.in_time_zone - => Wed, 13 Feb 2013 07:00:00 UTC +00:00 - - # Rails 4.0 behavior - >> Date.today.in_time_zone - => Wed, 13 Feb 2013 00:00:00 UTC +00:00 +* Deprecate `quoted_locking_column` method, which isn't used anywhere. - On the plus side it now behaves the same whether you pass a `String` date - or an actual `Date` instance. For example: - - # Rails 3.2 behavior - >> Date.civil(2013, 2, 13).to_time.in_time_zone - => Wed, 13 Feb 2013 07:00:00 UTC +00:00 - >> Time.zone.parse("2013-02-13") - => Wed, 13 Feb 2013 00:00:00 UTC +00:00 - - # Rails 4.0 behavior - >> Date.civil(2013, 2, 13).in_time_zone - => Wed, 13 Feb 2013 00:00:00 UTC +00:00 - >> "2013-02-13".in_time_zone - => Wed, 13 Feb 2013 00:00:00 UTC +00:00 + *kennyj* - If you need the old behavior you can convert the dates to times manually. - For example: +* Migration dump UUID default functions to schema.rb. - >> Post.new(created_at: Date.today).created_at - => Wed, 13 Feb 2013 00:00:00 UTC +00:00 + Fixes #10751. - >> Post.new(created_at: Date.today.to_time).created_at - => Wed, 13 Feb 2013 07:00:00 UTC +00:00 + *kennyj* - *Andrew White* +* Fixed a bug in `ActiveRecord::Associations::CollectionAssociation#find_by_scan` + when using `has_many` association with `:inverse_of` option and UUID primary key. -* Preloading `has_many :through` associations with conditions won't - cache the `:through` association. This will prevent invalid - subsets to be cached. - Fixes #8423. + Fixes #10450. - Example: + *kennyj* - class User - has_many :posts - has_many :recent_comments, -> { where('created_at > ?', 1.week.ago) }, :through => :posts - end +* ActiveRecord::Base#<=> has been removed. Primary keys may not be in order, + or even be numbers, so sorting by id doesn't make sense. Please use `sort_by` + and specify the attribute you wish to sort with. For example, change: - a_user = User.includes(:recent_comments).first + Post.all.to_a.sort - # This is preloaded. - a_user.recent_comments + to: - # This is not preloaded, fetched now. - a_user.posts + Post.all.to_a.sort_by(&:id) - *Yves Senn* + *Aaron Patterson* -* Don't run `after_commit` callbacks when creating through an association - if saving the record fails. +* Fix: joins association, with defined in the scope block constraints by using several + where constraints and at least of them is not `Arel::Nodes::Equality`, + generates invalid SQL expression. - *James Miller* + Fixes #11963. -* Allow store accessors to be overrided like other attribute methods, e.g.: + *Paul Nikitochkin* - class User < ActiveRecord::Base - store :settings, accessors: [ :color, :homepage ], coder: JSON +* Deprecate the delegation of Array bang methods for associations. + To use them, instead first call `#to_a` on the association to access the + array to be acted on. - def color - super || 'red' - end - end + *Ben Woosley* - *Sergey Nartimov* +* `CollectionAssociation#first`/`#last` (e.g. `has_many`) use a `LIMIT`ed + query to fetch results rather than loading the entire collection. -* Quote numeric values being compared to non-numeric columns. Otherwise, - in some database, the string column values will be coerced to a numeric - allowing 0, 0.0 or false to match any string starting with a non-digit. + *Lann Martin* - Example: +* Make possible to run SQLite rake tasks without the `Rails` constant defined. - App.where(apikey: 0) # => SELECT * FROM users WHERE apikey = '0' + *Damien Mathieu* - *Dylan Smith* +* Allow Relation#from to accept other relations with bind values. -* Schema dumper supports dumping the enabled database extensions to `schema.rb` - (currently only supported by postgresql). + *Ryan Wallace* - *Justin George* +* Fix inserts with prepared statements disabled. -* The database adpters now converts the options passed thought `DATABASE_URL` - environment variable to the proper Ruby types before using. For example, SQLite requires - that the timeout value is an integer, and PostgreSQL requires that the - prepared_statements option is a boolean. These now work as expected: + Fixes #12023. - Example: + *Rafael Mendonça França* - DATABASE_URL=sqlite3://localhost/test_db?timeout=500 - DATABASE_URL=postgresql://localhost/test_db?prepared_statements=false +* Setting a has_one association on a new record no longer causes an empty + transaction. - *Aaron Stone + Rafael Mendonça França* + *Dylan Thacker-Smith* -* `Relation#merge` now only overwrites where values on the LHS of the - merge. Consider: +* Fix `AR::Relation#merge` sometimes failing to preserve `readonly(false)` flag. - left = Person.where(age: [13, 14, 15]) - right = Person.where(age: [13, 14]).where(age: [14, 15]) + *thedarkone* - `left` results in the following SQL: +* Re-use `order` argument pre-processing for `reorder`. - WHERE age IN (13, 14, 15) + *Paul Nikitochkin* - `right` results in the following SQL: +* Fix PredicateBuilder so polymorphic association keys in `where` clause can + accept objects other than direct descendants of `ActiveRecord::Base` (decorated + models, for example). - WHERE age IN (13, 14) AND age IN (14, 15) + *Mikhail Dieterle* - Previously, `left.merge(right)` would result in all but the last - condition being removed: +* PostgreSQL adapter recognizes negative money values formatted with + parentheses (eg. `($1.25) # => -1.25`)). + Fixes #11899. - WHERE age IN (14, 15) + *Yves Senn* - Now it results in the LHS condition(s) for `age` being removed, but - the RHS remains as it is: +* Stop interpreting SQL 'string' columns as :string type because there is no + common STRING datatype in SQL. - WHERE age IN (13, 14) AND age IN (14, 15) + *Ben Woosley* - *Jon Leighton* +* `ActiveRecord::FinderMethods#exists?` returns `true`/`false` in all cases. -* Fix handling of dirty time zone aware attributes + *Xavier Noria* - Previously, when `time_zone_aware_attributes` were enabled, after - changing a datetime or timestamp attribute and then changing it back - to the original value, `changed_attributes` still tracked the - attribute as changed. This caused `[attribute]_changed?` and - `changed?` methods to return true incorrectly. +* Assign inet/cidr attribute with `nil` value for invalid address. Example: - in_time_zone 'Paris' do - order = Order.new - original_time = Time.local(2012, 10, 10) - order.shipped_at = original_time - order.save - order.changed? # => false - - # changing value - order.shipped_at = Time.local(2013, 1, 1) - order.changed? # => true - - # reverting to original value - order.shipped_at = original_time - order.changed? # => false, used to return true - end + record = User.new + record.logged_in_from_ip # is type of an inet or a cidr - *Lilibeth De La Cruz* - -* When `#count` is used in conjunction with `#uniq` we perform `count(:distinct => true)`. - Fixes #6865. - - Example: + # Before: + record.logged_in_from_ip = 'bad ip address' # raise exception - relation.uniq.count # => SELECT COUNT(DISTINCT *) + # After: + record.logged_in_from_ip = 'bad ip address' # do not raise exception + record.logged_in_from_ip # => nil + record.logged_in_from_ip_before_type_cast # => 'bad ip address' - *Yves Senn + Kaspar Schiess* + *Paul Nikitochkin* -* PostgreSQL ranges type support. Includes: int4range, int8range, - numrange, tsrange, tstzrange, daterange +* `add_to_target` now accepts a second optional `skip_callbacks` argument - Ranges can be created with inclusive and exclusive bounds. + If truthy, it will skip the :before_add and :after_add callbacks. - Example: + *Ben Woosley* - create_table :Room do |t| - t.daterange :availability - end +* Fix interactions between `:before_add` callbacks and nested attributes + assignment of `has_many` associations, when the association was not + yet loaded: - Room.create(availability: (Date.today..Float::INFINITY)) - Room.first.availability # => Wed, 19 Sep 2012..Infinity + - A `:before_add` callback was being called when a nested attributes + assignment assigned to an existing record. - One thing to note: Range class does not support exclusive lower - bound. + - Nested Attributes assignment did not affect the record in the + association target when a `:before_add` callback triggered the + loading of the association - *Alexander Grebennik* + *Jörg Schray* -* Added a state instance variable to each transaction. Will allow other objects - to know whether a transaction has been committed or rolled back. +* Allow enable_extension migration method to be revertible. - *John Wang* + *Eric Tipton* -* Collection associations `#empty?` always respects builded records. - Fixes #8879. +* Type cast hstore values on write, so that the value is consistent + with reading from the database. Example: - widget = Widget.new - widget.things.build - widget.things.empty? # => false - - *Yves Senn* - -* Support for PostgreSQL's `ltree` data type. - - *Rob Worley* + x = Hstore.new tags: {"bool" => true, "number" => 5} -* Fix undefined method `to_i` when calling `new` on a scope that uses an - Array; Fix FloatDomainError when setting integer column to NaN. - Fixes #8718, #8734, #8757. - - *Jason Stirk + Tristan Harward* - -* Rename `update_attributes` to `update`, keep `update_attributes` as an alias for `update` method. - This is a soft-deprecation for `update_attributes`, although it will still work without any - deprecation message in 4.0 is recommended to start using `update` since `update_attributes` will be - deprecated and removed in future versions of Rails. - - *Amparo Luna + Guillermo Iguaran* - -* `after_commit` and `after_rollback` now validate the `:on` option and raise an `ArgumentError` - if it is not one of `:create`, `:destroy` or `:update` - - *Pascal Friederich* - -* Improve ways to write `change` migrations, making the old `up` & `down` methods no longer necessary. - - * The methods `drop_table` and `remove_column` are now reversible, as long as the necessary information is given. - The method `remove_column` used to accept multiple column names; instead use `remove_columns` (which is not reversible). - The method `change_table` is also reversible, as long as its block doesn't call `remove`, `change` or `change_default` - - * New method `reversible` makes it possible to specify code to be run when migrating up or down. - See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#using-the-reversible-method) - - * New method `revert` will revert a whole migration or the given block. - If migrating down, the given migration / block is run normally. - See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#reverting-previous-migrations) - - Attempting to revert the methods `execute`, `remove_columns` and `change_column` will now - raise an `IrreversibleMigration` instead of actually executing them without any output. + # Before: + x.tags # => {"bool" => true, "number" => 5} - *Marc-André Lafortune* + # After: + x.tags # => {"bool" => "true", "number" => "5"} -* Serialized attributes can be serialized in integer columns. - Fixes #8575. + *Yves Senn* , *Severin Schoepke* - *Rafael Mendonça França* - -* Keep index names when using `alter_table` with sqlite3. - Fixes #3489. +* Fix multidimensional PG arrays containing non-string items. *Yves Senn* -* Add ability for postgresql adapter to disable user triggers in `disable_referential_integrity`. - Fixes #5523. - - *Gary S. Weaver* - -* Added support for `validates_uniqueness_of` in PostgreSQL array columns. - Fixes #8075. - - *Pedro Padron* - -* Allow int4range and int8range columns to be created in PostgreSQL and properly convert to/from database. - - *Alexey Vasiliev aka leopard* +* Fixes bug when using includes combined with select, the select statement was overwritten. -* Do not log the binding values for binary columns. + Fixes #11773. - *Matthew M. Boedicker* + *Edo Balvers* -* Fix counter cache columns not updated when replacing `has_many :through` - associations. +* Load fixtures from linked folders. - *Matthew Robertson* + *Kassio Borges* -* Recognize migrations placed in directories containing numbers and 'rb'. - Fixes #8492. +* Create a directory for sqlite3 file if not present on the system. - *Yves Senn* - -* Add `ActiveRecord::Base.cache_timestamp_format` class attribute to control - the format of the timestamp value in the cache key. Defaults to `:nsec`. - Fixes #8195. + *Richard Schneeman* - *Rafael Mendonça França* +* Removed redundant override of `xml` column definition for PG, + in order to use `xml` column type instead of `text`. -* Session variables can be set for the `mysql`, `mysql2`, and `postgresql` adapters - in the `variables: <hash>` parameter in `config/database.yml`. The key-value pairs of this - hash will be sent in a `SET key = value` query on new database connections. See also: - http://dev.mysql.com/doc/refman/5.0/en/set-statement.html - http://www.postgresql.org/docs/8.3/static/sql-set.html + *Paul Nikitochkin*, *Michael Nikitochkin* - *Aaron Stone* +* Revert `ActiveRecord::Relation#order` change that make new order + prepend the old one. -* Allow `Relation#where` with no arguments to be chained with new `not` query method. + Before: - Example: + User.order("name asc").order("created_at desc") + # SELECT * FROM users ORDER BY created_at desc, name asc - Developer.where.not(name: 'Aaron') + After: - *Akira Matsuda* + User.order("name asc").order("created_at desc") + # SELECT * FROM users ORDER BY name asc, created_at desc -* Unscope `update_column(s)` query to ignore default scope. + This also affects order defined in `default_scope` or any kind of associations. - When applying `default_scope` to a class with a where clause, using - `update_column(s)` could generate a query that would not properly update - the record due to the where clause from the `default_scope` being applied - to the update query. +* Add ability to define how a class is converted to Arel predicates. + For example, adding a very vendor specific regex implementation: - class User < ActiveRecord::Base - default_scope -> { where(active: true) } + regex_handler = proc do |column, value| + Arel::Nodes::InfixOperation.new('~', column, value.source) end + ActiveRecord::PredicateBuilder.register_handler(Regexp, regex_handler) - user = User.first - user.active = false - user.save! - - user.update_column(:active, true) # => false + *Sean Griffin & @joannecheng* - In this situation we want to skip the default_scope clause and just - update the record based on the primary key. With this change: +* Don't allow `quote_value` to be called without a column. - user.update_column(:active, true) # => true + Some adapters require column information to do their job properly. + By enforcing the provision of the column for this internal method + we ensure that those using adapters that require column information + will always get the proper behavior. - Fixes #8436. + *Ben Woosley* - *Carlos Antonio da Silva* +* When using optimistic locking, `update` was not passing the column to `quote_value` + to allow the connection adapter to properly determine how to quote the value. This was + affecting certain databases that use specific column types. -* SQLite adapter no longer corrupts binary data if the data contains `%00`. + Fixes #6763. - *Chris Feist* + *Alfred Wong* -* Fix performance problem with `primary_key` method in PostgreSQL adapter when having many schemas. - Uses `pg_constraint` table instead of `pg_depend` table which has many records in general. - Fixes #8414. +* rescue from all exceptions in `ConnectionManagement#call` - *kennyj* + Fixes #11497. -* Do not instantiate intermediate Active Record objects when eager loading. - These records caused `after_find` to run more than expected. - Fixes #3313. + As `ActiveRecord::ConnectionAdapters::ConnectionManagement` middleware does + not rescue from Exception (but only from StandardError), the Connection + Pool quickly runs out of connections when multiple erroneous Requests come + in right after each other. - *Yves Senn* - -* Add STI support to init and building associations. - Allows you to do `BaseClass.new(type: "SubClass")` as well as - `parent.children.build(type: "SubClass")` or `parent.build_child` - to initialize an STI subclass. Ensures that the class name is a - valid class and that it is in the ancestors of the super class - that the association is expecting. - - *Jason Rush* - -* Observers was extracted from Active Record as `rails-observers` gem. - - *Rafael Mendonça França* + Rescuing from all exceptions and not just StandardError, fixes this + behaviour. -* Ensure that associations take a symbol argument. *Steve Klabnik* + *Vipul A M* -* Fix dirty attribute checks for `TimeZoneConversion` with nil and blank - datetime attributes. Setting a nil datetime to a blank string should not - result in a change being flagged. - Fixes #8310. - - *Alisdair McDiarmid* - -* Prevent mass assignment to the type column of polymorphic associations when using `build` - Fixes #8265. +* `change_column` for PostgreSQL adapter respects the `:array` option. *Yves Senn* -* Deprecate calling `Relation#sum` with a block. To perform a calculation over - the array result of the relation, use `to_a.sum(&block)`. - - *Carlos Antonio da Silva* +* Remove deprecation warning from `attribute_missing` for attributes that are columns. -* Fix postgresql adapter to handle BC timestamps correctly + *Arun Agrawal* - HistoryEvent.create!(name: "something", occured_at: Date.new(0) - 5.years) +* Remove extra decrement of transaction deep level. - *Bogdan Gusiev* + Fixes #4566. -* When running migrations on Postgresql, the `:limit` option for `binary` and `text` columns is silently dropped. - Previously, these migrations caused sql exceptions, because Postgresql doesn't support limits on these types. + *Paul Nikitochkin* - *Victor Costan* +* Reset @column_defaults when assigning `locking_column`. + We had a potential problem. For example: -* Don't change STI type when calling `ActiveRecord::Base#becomes`. - Add `ActiveRecord::Base#becomes!` with the previous behavior. - - See #3023 for more information. - - *Thomas Hollstegge* + class Post < ActiveRecord::Base + self.column_defaults # if we call this unintentionally before setting locking_column ... + self.locking_column = 'my_locking_column' + end -* `rename_index` can be used inside a `change_table` block. + Post.column_defaults["my_locking_column"] + => nil # expected value is 0 ! - change_table :accounts do |t| - t.rename_index :user_id, :account_id - end + *kennyj* - *Jarek Radosz* +* Remove extra select and update queries on save/touch/destroy ActiveRecord model + with belongs to reflection with option `touch: true`. -* `#pluck` can be used on a relation with `select` clause. Fix #7551 + Fixes #11288. - Example: + *Paul Nikitochkin* - Topic.select([:approved, :id]).order(:id).pluck(:id) +* Remove deprecated nil-passing to the following `SchemaCache` methods: + `primary_keys`, `tables`, `columns` and `columns_hash`. *Yves Senn* -* Do not create useless database transaction when building `has_one` association. - - Example: - - User.has_one :profile - User.new.build_profile - - *Bogdan Gusiev* - -* `:counter_cache` option for `has_many` associations to support custom named counter caches. - Fixes #7993. +* Remove deprecated block filter from `ActiveRecord::Migrator#migrate`. *Yves Senn* -* Deprecate the possibility to pass a string as third argument of `add_index`. - Pass `unique: true` instead. +* Remove deprecated String constructor from `ActiveRecord::Migrator`. - add_index(:users, :organization_id, unique: true) - - *Rafael Mendonça França* - -* Raise an `ArgumentError` when passing an invalid option to `add_index`. - - *Rafael Mendonça França* + *Yves Senn* -* Fix `find_in_batches` crashing when IDs are strings and start option is not specified. +* Remove deprecated `scope` use without passing a callable object. - *Alexis Bernard* + *Arun Agrawal* -* `AR::Base#attributes_before_type_cast` now returns unserialized values for serialized attributes. +* Remove deprecated `transaction_joinable=` in favor of `begin_transaction` + with `:joinable` option. - *Nikita Afanasenko* + *Arun Agrawal* -* Use query cache/uncache when using `DATABASE_URL`. - Fixes #6951. +* Remove deprecated `decrement_open_transactions`. - *kennyj* + *Arun Agrawal* -* Fix bug where `update_columns` and `update_column` would not let you update the primary key column. +* Remove deprecated `increment_open_transactions`. - *Henrik Nyh* + *Arun Agrawal* -* The `create_table` method raises an `ArgumentError` when the primary key column is redefined. - Fixes #6378. +* Remove deprecated `PostgreSQLAdapter#outside_transaction?` + method. You can use `#transaction_open?` instead. *Yves Senn* -* `ActiveRecord::AttributeMethods#[]` raises `ActiveModel::MissingAttributeError` - error if the given attribute is missing. Fixes #5433. +* Remove deprecated `ActiveRecord::Fixtures.find_table_name` in favor of + `ActiveRecord::Fixtures.default_fixture_model_name`. - class Person < ActiveRecord::Base - belongs_to :company - end + *Vipul A M* - # Before: - person = Person.select('id').first - person[:name] # => nil - person.name # => ActiveModel::MissingAttributeError: missing_attribute: name - person[:company_id] # => nil - person.company # => nil +* Removed deprecated `columns_for_remove` from `SchemaStatements`. - # After: - person = Person.select('id').first - person[:name] # => ActiveModel::MissingAttributeError: missing_attribute: name - person.name # => ActiveModel::MissingAttributeError: missing_attribute: name - person[:company_id] # => ActiveModel::MissingAttributeError: missing_attribute: company_id - person.company # => ActiveModel::MissingAttributeError: missing_attribute: company_id + *Neeraj Singh* - *Francesco Rodriguez* +* Remove deprecated `SchemaStatements#distinct`. -* Small binary fields use the `VARBINARY` MySQL type, instead of `TINYBLOB`. - - *Victor Costan* - -* Decode URI encoded attributes on database connection URLs. - - *Shawn Veader* - -* Add `find_or_create_by`, `find_or_create_by!` and - `find_or_initialize_by` methods to `Relation`. - - These are similar to the `first_or_create` family of methods, but - the behaviour when a record is created is slightly different: + *Francesco Rodriguez* - User.where(first_name: 'Penélope').first_or_create +* Move deprecated `ActiveRecord::TestCase` into the rails test + suite. The class is no longer public and is only used for internal + Rails tests. - will execute: + *Yves Senn* - User.where(first_name: 'Penélope').create +* Removed support for deprecated option `:restrict` for `:dependent` + in associations. - Causing all the `create` callbacks to execute within the context of - the scope. This could affect queries that occur within callbacks. + *Neeraj Singh* - User.find_or_create_by(first_name: 'Penélope') +* Removed support for deprecated `delete_sql` in associations. - will execute: + *Neeraj Singh* - User.create(first_name: 'Penélope') +* Removed support for deprecated `insert_sql` in associations. - Which obviously does not affect the scoping of queries within - callbacks. + *Neeraj Singh* - The `find_or_create_by` version also reads better, frankly. +* Removed support for deprecated `finder_sql` in associations. - If you need to add extra attributes during create, you can do one of: + *Neeraj Singh* - User.create_with(active: true).find_or_create_by(first_name: 'Jon') - User.find_or_create_by(first_name: 'Jon') { |u| u.active = true } +* Support array as root element in JSON fields. - The `first_or_create` family of methods have been nodoc'ed in favour - of this API. They may be deprecated in the future but their - implementation is very small and it's probably not worth putting users - through lots of annoying deprecation warnings. + *Alexey Noskov & Francesco Rodriguez* - *Jon Leighton* +* Removed support for deprecated `counter_sql` in associations. -* Fix bug with presence validation of associations. Would incorrectly add duplicated errors - when the association was blank. Bug introduced in 1fab518c6a75dac5773654646eb724a59741bc13. + *Neeraj Singh* - *Scott Willson* +* Do not invoke callbacks when `delete_all` is called on collection. -* Fix bug where sum(expression) returns string '0' for no matching records. - Fixes #7439 + Method `delete_all` should not be invoking callbacks and this + feature was deprecated in Rails 4.0. This is being removed. + `delete_all` will continue to honor the `:dependent` option. However + if `:dependent` value is `:destroy` then the `:delete_all` deletion + strategy for that collection will be applied. - *Tim Macfarlane* + User can also force a deletion strategy by passing parameter to + `delete_all`. For example you can do `@post.comments.delete_all(:nullify)`. -* PostgreSQL adapter correctly fetches default values when using multiple schemas and domains in a db. Fixes #7914 + *Neeraj Singh* - *Arturo Pie* +* Calling default_scope without a proc will now raise `ArgumentError`. -* Learn ActiveRecord::QueryMethods#order work with hash arguments + *Neeraj Singh* - When symbol or hash passed we convert it to Arel::Nodes::Ordering. - If we pass invalid direction(like name: :DeSc) ActiveRecord::QueryMethods#order will raise an exception +* Removed deprecated method `type_cast_code` from Column. - User.order(:name, email: :desc) - # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC + *Neeraj Singh* - *Tima Maslyuchenko* +* Removed deprecated options `delete_sql` and `insert_sql` from HABTM + association. -* Rename `ActiveRecord::Fixtures` class to `ActiveRecord::FixtureSet`. - Instances of this class normally hold a collection of fixtures (records) - loaded either from a single YAML file, or from a file and a folder - with the same name. This change make the class name singular and makes - the class easier to distinguish from the modules like - `ActiveRecord::TestFixtures`, which operates on multiple fixture sets, - or `DelegatingFixtures`, `::Fixtures`, etc., - and from the class `ActiveRecord::Fixture`, which corresponds to a single - fixture. + Removed deprecated options `finder_sql` and `counter_sql` from + collection association. - *Alexey Muranov* + *Neeraj Singh* -* The postgres adapter now supports tables with capital letters. - Fixes #5920. +* Remove deprecated `ActiveRecord::Base#connection` method. + Make sure to access it via the class. *Yves Senn* -* `CollectionAssociation#count` returns `0` without querying if the - parent record is not persisted. - - Before: +* Remove deprecation warning for `auto_explain_threshold_in_seconds`. - person.pets.count - # SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" IS NULL - # => 0 + *Yves Senn* - After: +* Remove deprecated `:distinct` option from `Relation#count`. - person.pets.count - # fires without sql query - # => 0 + *Yves Senn* - *Francesco Rodriguez* +* Removed deprecated methods `partial_updates`, `partial_updates?` and + `partial_updates=`. -* Fix `reset_counters` crashing on `has_many :through` associations. - Fixes #7822. + *Neeraj Singh* - *lulalala* +* Removed deprecated method `scoped` -* Support for partial inserts. + *Neeraj Singh* - When inserting new records, only the fields which have been changed - from the defaults will actually be included in the INSERT statement. - The other fields will be populated by the database. +* Removed deprecated method `default_scopes?` - This is more efficient, and also means that it will be safe to - remove database columns without getting subsequent errors in running - app processes (so long as the code in those processes doesn't - contain any references to the removed column). + *Neeraj Singh* - The `partial_updates` configuration option is now renamed to - `partial_writes` to reflect the fact that it now impacts both inserts - and updates. +* Remove implicit join references that were deprecated in 4.0. - *Jon Leighton* + Example: -* Allow before and after validations to take an array of lifecycle events + # before with implicit joins + Comment.where('posts.author_id' => 7) - *John Foley* + # after + Comment.references(:posts).where('posts.author_id' => 7) -* Support for specifying transaction isolation level + *Yves Senn* - If your database supports setting the isolation level for a transaction, you can set - it like so: +* Apply default scope when joining associations. For example: - Post.transaction(isolation: :serializable) do - # ... + class Post < ActiveRecord::Base + default_scope -> { where published: true } end - Valid isolation levels are: - - * `:read_uncommitted` - * `:read_committed` - * `:repeatable_read` - * `:serializable` - - You should consult the documentation for your database to understand the - semantics of these different levels: - - * http://www.postgresql.org/docs/9.1/static/transaction-iso.html - * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html - - An `ActiveRecord::TransactionIsolationError` will be raised if: + class Comment + belongs_to :post + end - * The adapter does not support setting the isolation level - * You are joining an existing open transaction - * You are creating a nested (savepoint) transaction + When calling `Comment.joins(:post)`, we expect to receive only + comments on published posts, since that is the default scope for + posts. - The mysql, mysql2 and postgresql adapters support setting the transaction - isolation level. However, support is disabled for mysql versions below 5, - because they are affected by a bug (http://bugs.mysql.com/bug.php?id=39170) - which means the isolation level gets persisted outside the transaction. + Before this change, the default scope from `Post` was not applied, + so we'd get comments on unpublished posts. *Jon Leighton* -* `ActiveModel::ForbiddenAttributesProtection` is included by default - in Active Record models. Check the docs of `ActiveModel::ForbiddenAttributesProtection` - for more details. - - *Guillermo Iguaran* - -* Remove integration between Active Record and - `ActiveModel::MassAssignmentSecurity`, `protected_attributes` gem - should be added to use `attr_accessible`/`attr_protected`. Mass - assignment options has been removed from all the AR methods that - used it (ex. `AR::Base.new`, `AR::Base.create`, `AR::Base#update_attributes`, etc). - - *Guillermo Iguaran* - -* Fix the return of querying with an empty hash. - Fixes #6971. - - User.where(token: {}) - - Before: - - #=> SELECT * FROM users; - - After: - - #=> SELECT * FROM users WHERE 1=0; - - *Damien Mathieu* - -* Fix creation of through association models when using `collection=[]` - on a `has_many :through` association from an unsaved model. - Fixes #7661. +* Remove `activerecord-deprecated_finders` as a dependency - *Ernie Miller* + *Łukasz Strzałkowski* -* Explain only normal CRUD sql (select / update / insert / delete). - Fix problem that explains unexplainable sql. - Closes #7544 #6458. +* Remove Oracle / Sqlserver / Firebird database tasks that were deprecated in 4.0. *kennyj* -* You can now override the generated accessor methods for stored attributes - and reuse the original behavior with `read_store_attribute` and `write_store_attribute`, - which are counterparts to `read_attribute` and `write_attribute`. +* `find_each` now returns an `Enumerator` when called without a block, so that it + can be chained with other `Enumerable` methods. - *Matt Jones* + *Ben Woosley* -* Accept `belongs_to` (including polymorphic) association keys in queries. +* `ActiveRecord::Result.each` now returns an `Enumerator` when called without + a block, so that it can be chained with other `Enumerable` methods. - The following queries are now equivalent: + *Ben Woosley* - Post.where(author: author) - Post.where(author_id: author) +* Flatten merged join_values before building the joins. - PriceEstimate.where(estimate_of: treasure) - PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure) + While joining_values special treatment is given to string values. + By flattening the array it ensures that string values are detected + as strings and not arrays. - *Peter Brown* + Fixes #10669. -* Use native `mysqldump` command instead of `structure_dump` method - when dumping the database structure to a sql file. Fixes #5547. + *Neeraj Singh and iwiznia* - *kennyj* - -* PostgreSQL inet and cidr types are converted to `IPAddr` objects. - - *Dan McClain* - -* PostgreSQL array type support. Any datatype can be used to create an - array column, with full migration and schema dumper support. - - To declare an array column, use the following syntax: - - create_table :table_with_arrays do |t| - t.integer :int_array, array: true - # integer[] - t.integer :int_array, array: true, length: 2 - # smallint[] - t.string :string_array, array: true, length: 30 - # char varying(30)[] - end +* Do not load all child records for inverse case. - This respects any other migration detail (limits, defaults, etc). - Active Record will serialize and deserialize the array columns on - their way to and from the database. + currently `post.comments.find(Comment.first.id)` would load all + comments for the given post to set the inverse association. - One thing to note: PostgreSQL does not enforce any limits on the - number of elements, and any array can be multi-dimensional. Any - array that is multi-dimensional must be rectangular (each sub array - must have the same number of elements as its siblings). + This has a huge performance penalty. Because if post has 100k + records and all these 100k records would be loaded in memory + even though the comment id was supplied. - If the `pg_array_parser` gem is available, it will be used when - parsing PostgreSQL's array representation. + Fix is to use in-memory records only if loaded? is true. Otherwise + load the records using full sql. - *Dan McClain* + Fixes #10509. -* Attribute predicate methods, such as `article.title?`, will now raise - `ActiveModel::MissingAttributeError` if the attribute being queried for - truthiness was not read from the database, instead of just returning `false`. + *Neeraj Singh* - *Ernie Miller* +* `inspect` on Active Record model classes does not initiate a + new connection. This means that calling `inspect`, when the + database is missing, will no longer raise an exception. + Fixes #10936. -* `ActiveRecord::SchemaDumper` uses Ruby 1.9 style hash, which means that the - schema.rb file will be generated using this new syntax from now on. - - *Konstantin Shabanov* + Example: -* Map interval with precision to string datatype in PostgreSQL. Fixes #7518. + Author.inspect # => "Author(no database connection)" *Yves Senn* -* Fix eagerly loading associations without primary keys. Fixes #4976. - - *Kelley Reynolds* - -* 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. - - *Jan Bernacki* - -* Fix bug when calling `store_accessor` multiple times. - Fixes #7532. - - *Matt Jones* - -* Fix store attributes that show the changes incorrectly. - Fixes #7532. - - *Matt Jones* +* Handle single quotes in PostgreSQL default column values. + Fixes #10881. -* Fix `ActiveRecord::Relation#pluck` when columns or tables are reserved words. + *Dylan Markow* - *Ian Lesperance* +* Log the sql that is actually sent to the database. -* Allow JSON columns to be created in PostgreSQL and properly encoded/decoded. - to/from database. + If I have a query that produces sql + `WHERE "users"."name" = 'a b'` then in the log all the + whitespace is being squeezed. So the sql that is printed in the + log is `WHERE "users"."name" = 'a b'`. - *Dickson S. Guedes* + Do not squeeze whitespace out of sql queries. Fixes #10982. -* Fix time column type casting for invalid time string values to correctly return `nil`. + *Neeraj Singh* - *Adam Meehan* - -* Allow to pass Symbol or Proc into `:limit` option of #accepts_nested_attributes_for. - - *Mikhail Dieterle* +* Fixture setup no longer depends on `ActiveRecord::Base.configurations`. + This is relevant when `ENV["DATABASE_URL"]` is used in place of a `database.yml`. -* ActiveRecord::SessionStore has been extracted from Active Record as `activerecord-session_store` - gem. Please read the `README.md` file on the gem for the usage. - - *Prem Sichanugrist* - -* Fix `reset_counters` when there are multiple `belongs_to` association with the - same foreign key and one of them have a counter cache. - Fixes #5200. - - *Dave Desrochers* - -* `serialized_attributes` and `_attr_readonly` become class method only. Instance reader methods are deprecated. - - *kennyj* - -* Round usec when comparing timestamp attributes in the dirty tracking. - Fixes #6975. - - *kennyj* - -* Use inversed parent for first and last child of `has_many` association. - - *Ravil Bayramgalin* - -* Fix `Column.microseconds` and `Column.fast_string_to_time` to avoid converting - timestamp seconds to a float, since it occasionally results in inaccuracies - with microsecond-precision times. Fixes #7352. - - *Ari Pollak* - -* Fix AR#dup to nullify the validation errors in the dup'ed object. Previously the original - and the dup'ed object shared the same errors. - - *Christian Seiler* - -* Raise `ArgumentError` if list of attributes to change is empty in `update_all`. - - *Roman Shatsov* - -* Fix AR#create to return an unsaved record when AR::RecordInvalid is - raised. Fixes #3217. - - *Dave Yeu* - -* Fixed table name prefix that is generated in engines for namespaced models. - - *Wojciech Wnętrzak* - -* Make sure `:environment` task is executed before `db:schema:load` or `db:structure:load`. - Fixes #4772. - - *Seamus Abshere* + *Yves Senn* -* Allow Relation#merge to take a proc. +* Fix mysql2 adapter raises the correct exception when executing a query on a + closed connection. - This was requested by DHH to allow creating of one's own custom - association macros. + *Yves Senn* - For example: +* Ambiguous reflections are on :through relationships are no longer supported. + For example, you need to change this: - module Commentable - def has_many_comments(extra) - has_many :comments, -> { where(:foo).merge(extra) } - end + class Author < ActiveRecord::Base + has_many :posts + has_many :taggings, :through => :posts end class Post < ActiveRecord::Base - extend Commentable - has_many_comments -> { where(:bar) } + has_one :tagging + has_many :taggings end - *Jon Leighton* - -* Add CollectionProxy#scope. - - This can be used to get a Relation from an association. - - Previously we had a #scoped method, but we're deprecating that for - AR::Base, so it doesn't make sense to have it here. + class Tagging < ActiveRecord::Base + end - This was requested by DHH, to facilitate code like this: + To this: - Project.scope.order('created_at DESC').page(current_page).tagged_with(@tag).limit(5).scoping do - @topics = @project.topics.scope - @todolists = @project.todolists.scope - @attachments = @project.attachments.scope - @documents = @project.documents.scope + class Author < ActiveRecord::Base + has_many :posts + has_many :taggings, :through => :posts, :source => :tagging end - *Jon Leighton* + class Post < ActiveRecord::Base + has_one :tagging + has_many :taggings + end -* Add `Relation#load`. + class Tagging < ActiveRecord::Base + end - This method explicitly loads the records and then returns `self`. + *Aaron Patterson* - 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. +* Remove column restrictions for `count`, let the database raise if the SQL is + invalid. The previous behavior was untested and surprising for the user. + Fixes #5554. Example: - @posts = Post.where(published: true).load + User.select("name, username").count + # Before => SELECT count(*) FROM users + # After => ActiveRecord::StatementInvalid - *Jon Leighton* - -* `Relation#order`: make new order prepend old one. - - User.order("name asc").order("created_at desc") - # SELECT * FROM users ORDER BY created_at desc, name asc + # you can still use `count(:all)` to perform a query unrelated to the + # selected columns + User.select("name, username").count(:all) # => SELECT count(*) FROM users - This also affects order defined in `default_scope` or any kind of associations. - - *Bogdan Gusiev* - -* `Model.all` now returns an `ActiveRecord::Relation`, rather than an - array of records. Use `Relation#to_a` if you really want an array. - - In some specific cases, this may cause breakage when upgrading. - However in most cases the `ActiveRecord::Relation` will just act as a - lazy-loaded array and there will be no problems. + *Yves Senn* - 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. +* Rails now automatically detects inverse associations. If you do not set the + `:inverse_of` option on the association, then Active Record will guess the + inverse association based on heuristics. - `Model.scoped` is deprecated in favour of `Model.all`. + Note that automatic inverse detection only works on `has_many`, `has_one`, + and `belongs_to` associations. Extra options on the associations will + also prevent the association's inverse from being found automatically. - `Relation#all` still returns an array, but is deprecated (since it - would serve no purpose if we made it return a `Relation`). + The automatic guessing of the inverse association uses a heuristic based + on the name of the class, so it may not work for all associations, + especially the ones with non-standard names. - *Jon Leighton* + You can turn off the automatic detection of inverse associations by setting + the `:inverse_of` option to `false` like so: -* `:finder_sql` and `:counter_sql` options on collection associations - are deprecated. Please transition to using scopes. + class Taggable < ActiveRecord::Base + belongs_to :tag, inverse_of: false + end - *Jon Leighton* + *John Wang* -* `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many` - associations are deprecated. Please transition to using `has_many - :through`. +* Fix `add_column` with `array` option when using PostgreSQL. Fixes #10432 - *Jon Leighton* + *Adam Anderson* -* Added `#update_columns` method which updates the attributes from - the passed-in hash without calling save, hence skipping validations and - callbacks. `ActiveRecordError` will be raised when called on new objects - or when at least one of the attributes is marked as read only. +* Usage of `implicit_readonly` is being removed`. Please use `readonly` method + explicitly to mark records as `readonly. + Fixes #10615. - post.attributes # => {"id"=>2, "title"=>"My title", "body"=>"My content", "author"=>"Peter"} - post.update_columns(title: 'New title', author: 'Sebastian') # => true - post.attributes # => {"id"=>2, "title"=>"New title", "body"=>"My content", "author"=>"Sebastian"} + Example: - *Sebastian Martinez + Rafael Mendonça França* + user = User.joins(:todos).select("users.*, todos.title as todos_title").readonly(true).first + user.todos_title = 'clean pet' + user.save! # will raise error -* The migration generator now creates a join table with (commented) indexes every time - the migration name contains the word `join_table`: + *Yves Senn* - rails g migration create_join_table_for_artists_and_musics artist_id:index music_id +* Fix the `:primary_key` option for `has_many` associations. + Fixes #10693. - *Aleksey Magusev* + *Yves Senn* -* Add `add_reference` and `remove_reference` schema statements. Aliases, `add_belongs_to` - and `remove_belongs_to` are acceptable. References are reversible. +* Fix bug where tiny types are incorrectly coerced as boolean when the length is more than 1. - Examples: + Fixes #10620. - # 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) + *Aaron Patterson* - *Aleksey Magusev* +* Also support extensions in PostgreSQL 9.1. This feature has been supported since 9.1. -* Add `:default` and `:null` options to `column_exists?`. + *kennyj* - column_exists?(:testings, :taggable_id, :integer, null: false) - column_exists?(:testings, :taggable_type, :string, default: 'Photo') +* Deprecate `ConnectionAdapters::SchemaStatements#distinct`, + as it is no longer used by internals. - *Aleksey Magusev* + *Ben Woosley* -* `ActiveRecord::Relation#inspect` now makes it clear that you are - dealing with a `Relation` object rather than an array:. +* Fix pending migrations error when loading schema and `ActiveRecord::Base.table_name_prefix` + is not blank. - User.where(age: 30).inspect - # => <ActiveRecord::Relation [#<User ...>, #<User ...>, ...]> + Call `assume_migrated_upto_version` on connection to prevent it from first + being picked up in `method_missing`. - User.where(age: 30).to_a.inspect - # => [#<User ...>, #<User ...>] + In the base class, `Migration`, `method_missing` expects the argument to be a + table name, and calls `proper_table_name` on the arguments before sending to + `connection`. If `table_name_prefix` or `table_name_suffix` is used, the schema + version changes to `prefix_version_suffix`, breaking `rake test:prepare`. - The number of records displayed will be limited to 10. + Fixes #10411. - *Brian Cardarella, Jon Leighton & Damien Mathieu* + *Kyle Stevens* -* Add `collation` and `ctype` support to PostgreSQL. These are available for PostgreSQL 8.4 or later. - Example: +* Method `read_attribute_before_type_cast` should accept input as symbol. - development: - adapter: postgresql - host: localhost - database: rails_development - username: foo - password: bar - encoding: UTF8 - collation: ja_JP.UTF8 - ctype: ja_JP.UTF8 + *Neeraj Singh* - *kennyj* +* Confirm a record has not already been destroyed before decrementing counter cache. -* 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. + *Ben Tucker* - *Nick Monje & Brent Wheeldon* +* Fixed a bug in `ActiveRecord#sanitize_sql_hash_for_conditions` in which + `self.class` is an argument to `PredicateBuilder#build_from_hash` + causing `PredicateBuilder` to call non-existent method + `Class#reflect_on_association`. -* `FinderMethods#exists?` now returns `false` with the `false` argument. + *Zach Ohlgren* - *Egor Lynko* +* While removing index if column option is missing then raise IrreversibleMigration exception. -* 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: + Following code should raise `IrreversibleMigration`. But the code was + failing since options is an array and not a hash. def change - create_table :foobars do |t| - t.timestamps precision: 0 + change_table :users do |t| + t.remove_index [:name, :email] end end - *Tony Schneider* - -* Allow `ActiveRecord::Relation#pluck` to accept multiple columns. Returns an - array of arrays containing the typecasted values: - - Person.pluck(:id, :name) - # SELECT people.id, people.name FROM people - # [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] - - *Jeroen van Ingen & Carlos Antonio da Silva* - -* Improve the derivation of HABTM join table name to take account of nesting. - It now takes the table names of the two models, sorts them lexically and - then joins them, stripping any common prefix from the second table name. - - Some examples: - - Top level models (Category <=> Product) - Old: categories_products - New: categories_products - - Top level models with a global table_name_prefix (Category <=> Product) - Old: site_categories_products - New: site_categories_products - - Nested models in a module without a table_name_prefix method (Admin::Category <=> Admin::Product) - Old: categories_products - New: categories_products - - Nested models in a module with a table_name_prefix method (Admin::Category <=> Admin::Product) - Old: categories_products - New: admin_categories_products + Fix was to check if the options is a Hash before operating on it. - Nested models in a parent model (Catalog::Category <=> Catalog::Product) - Old: categories_products - New: catalog_categories_products + Fixes #10419. - Nested models in different parent models (Catalog::Category <=> Content::Page) - Old: categories_pages - New: catalog_categories_content_pages + *Neeraj Singh* - *Andrew White* +* Do not overwrite manually built records during one-to-one nested attribute assignment -* 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. + For one-to-one nested associations, if you build the new (in-memory) + child object yourself before assignment, then the NestedAttributes + module will not overwrite it, e.g.: - *Andrew White* - -* Added `stored_attributes` hash which contains the attributes stored using - `ActiveRecord::Store`. This allows you to retrieve the list of attributes - you've defined. - - class User < ActiveRecord::Base - store :settings, accessors: [:color, :homepage] - end - - User.stored_attributes[:settings] # [:color, :homepage] - - *Joost Baaij & Carlos Antonio da Silva* - -* PostgreSQL default log level is now 'warning', to bypass the noisy notice - messages. You can change the log level using the `min_messages` option - available in your config/database.yml. - - *kennyj* - -* Add uuid datatype support to PostgreSQL adapter. - - *Konstantin Shabanov* - -* Added `ActiveRecord::Migration.check_pending!` that raises an error if - migrations are pending. - - *Richard Schneeman* + class Member < ActiveRecord::Base + has_one :avatar + accepts_nested_attributes_for :avatar -* Added `#destroy!` which acts like `#destroy` but will raise an - `ActiveRecord::RecordNotDestroyed` exception instead of returning `false`. - - *Marc-André Lafortune* - -* 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 - has_many :pets + def avatar + super || build_avatar(width: 200) + end end - person.pets.delete("1") # => [#<Pet id: 1>] - person.pets.delete(2, 3) # => [#<Pet id: 2>, #<Pet id: 3>] - - *Francesco Rodriguez* - -* 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: - - * `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 - `find_or_create_by(...)` or where(...).first_or_create` - * `find_or_create_by_...!` can be rewritten using - `find_or_create_by!(...) or `where(...).first_or_create!` - - The implementation of the deprecated dynamic finders has been moved - to the `activerecord-deprecated_finders` gem. See below for details. - - *Jon Leighton* - -* Deprecated the old-style hash based finder API. This means that - methods which previously accepted "finder options" no longer do. For - example this: - - Post.find(:all, conditions: { comments_count: 10 }, limit: 5) - - Should be rewritten in the new style which has existed since Rails 3: - - Post.where(comments_count: 10).limit(5) - - Note that as an interim step, it is possible to rewrite the above as: - - Post.all.merge(where: { comments_count: 10 }, limit: 5) - - This could save you a lot of work if there is a lot of old-style - finder usage in your application. - - `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: - - * `:conditions` becomes `:where`. - * `:include` becomes `:includes`. - - 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, so the interface works out of the box. It will no - longer be a dependency from Rails 4.1 (you'll need to add it to the - `Gemfile` in 4.1), and will be maintained until Rails 5.0. - - *Jon Leighton* - -* It's not possible anymore to destroy a model marked as read only. - - *Johannes Barre* - -* Added ability to ActiveRecord::Relation#from to accept other ActiveRecord::Relation objects. - - Record.from(subquery) - Record.from(subquery, :a) - - *Radoslav Stankov* - -* Added custom coders support for ActiveRecord::Store. Now you can set - your custom coder like this: - - store :settings, accessors: [ :color, :homepage ], coder: JSON - - *Andrey Voronkov* - -* `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`. - - *Michael Pearson* - -* Added default order to `first` to assure consistent results among - different database engines. Introduced `take` as a replacement to - the old behavior of `first`. - - *Marcelo Silveira* - -* Added an `:index` option to automatically create indexes for references - and belongs_to statements in migrations. - - 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: - - create_table :messages do |t| - t.references :person, index: true - end - - Is the same as: - - create_table :messages do |t| - t.references :person - end - add_index :messages, :person_id - - Generators have also been updated to use the new syntax. - - *Joshua Wood* - -* Added `#find_by` and `#find_by!` to mirror the functionality - provided by dynamic finders in a way that allows dynamic input more - easily: - - Post.find_by name: 'Spartacus', rating: 4 - Post.find_by "published_at < ?", 2.weeks.ago - Post.find_by! name: 'Spartacus' - - *Jon Leighton* - -* Added ActiveRecord::Base#slice to return a hash of the given methods with - their names as keys and returned values as values. - - *Guillermo Iguaran* + member = Member.new + member.avatar_attributes = {icon: 'sad'} + member.avatar.width # => 200 -* Deprecate eager-evaluated scopes. - - Don't use this: - - scope :red, where(color: 'red') - default_scope where(color: 'red') - - Use this: - - scope :red, -> { where(color: 'red') } - default_scope { where(color: 'red') } + *Olek Janiszewski* - The former has numerous issues. It is a common newbie gotcha to do - the following: +* fixes bug introduced by #3329. Now, when autosaving associations, + deletions happen before inserts and saves. This prevents a 'duplicate + unique value' database error that would occur if a record being created had + the same value on a unique indexed field as that of a record being destroyed. - scope :recent, where(published_at: Time.now - 2.weeks) + *Johnny Holton* - Or a more subtle variant: +* Handle aliased attributes in ActiveRecord::Relation. - scope :recent, -> { where(published_at: Time.now - 2.weeks) } - scope :recent_red, recent.where(color: 'red') + When using symbol keys, ActiveRecord will now translate aliased attribute names to the actual column name used in the database: - 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: + With the model - scope :remove_conditions, except(:where) - where(...).remove_conditions # => still has conditions + class Topic + alias_attribute :heading, :title + end - *Jon Leighton* + The call -* Remove IdentityMap + Topic.where(heading: 'The First Topic') - IdentityMap has never graduated to be an "enabled-by-default" feature, due - to some inconsistencies with associations, as described in this commit: + should yield the same result as - https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6 + Topic.where(title: 'The First Topic') - Hence the removal from the codebase, until such issues are fixed. + This also applies to ActiveRecord::Relation::Calculations calls such as `Model.sum(:aliased)` and `Model.pluck(:aliased)`. - *Carlos Antonio da Silva* + This will not work with SQL fragment strings like `Model.sum('DISTINCT aliased')`. -* Added the schema cache dump feature. + *Godfrey Chan* - `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. +* Mute `psql` output when running rake db:schema:load. - Usage notes: + *Godfrey Chan* - 1) execute rake task. - RAILS_ENV=production bundle exec rake db:schema:cache:dump - => generate db/schema_cache.dump +* Trigger a save on `has_one association=(associate)` when the associate contents have changed. - 2) add config.active_record.use_schema_cache_dump = true in config/production.rb. BTW, true is default. + Fix #8856. - 3) boot rails. - RAILS_ENV=production bundle exec rails server - => use db/schema_cache.dump + *Chris Thompson* - 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 +* Abort a rake task when missing db/structure.sql like `db:schema:load` task. *kennyj* -* Added support for partial indices to PostgreSQL adapter. - - The `add_index` method now supports a `where` option that receives a - string with the partial index criteria. - - add_index(:accounts, :code, where: 'active') - - Generates - - CREATE INDEX index_accounts_on_code ON accounts(code) WHERE active - - *Marcelo Silveira* - -* 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* - -* Added the `ActiveRecord::NullRelation` class implementing the null - object pattern for the Relation class. - - *Juanjo Bazán* - -* Added new `dependent: :restrict_with_error` option. This will add - an error to the model, rather than raising an exception. - - The `:restrict` option is renamed to `:restrict_with_exception` to - make this distinction explicit. - - *Manoj Kumar & Jon Leighton* - -* 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 - - *Rafael Mendonça França* - -* The primary key is always initialized in the @attributes hash to `nil` (unless - another value has been specified). - - *Aaron Paterson* - -* In previous releases, the following would generate a single query with - an `OUTER JOIN comments`, rather than two separate queries: - - Post.includes(:comments) - .where("comments.name = 'foo'") - - 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. - - Therefore, it is now deprecated. - - To avoid deprecation warnings and for future compatibility, you must - explicitly state which tables you reference, when using SQL snippets: - - Post.includes(:comments) - .where("comments.name = 'foo'") - .references(:comments) - - Note that you do not need to explicitly specify references in the - following cases, as they can be automatically inferred: - - Post.includes(:comments).where(comments: { name: 'foo' }) - Post.includes(:comments).where('comments.name' => 'foo') - Post.includes(:comments).order('comments.name') - - You 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. - - *Jon Leighton* - -* Support for the `schema_info` table has been dropped. Please - switch to `schema_migrations`. - - *Aaron Patterson* - -* Connections *must* be closed at the end of a thread. If not, your - connection pool can fill and an exception will be raised. - - *Aaron Patterson* - -* PostgreSQL hstore records can be created. - - *Aaron Patterson* - -* PostgreSQL hstore types are automatically deserialized from the database. - - *Aaron Patterson* +* rake:db:test:prepare falls back to original environment after execution. + *Slava Markevich* -Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activerecord/CHANGELOG.md) for previous changes. +Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index ed1e171d58..e04abe9b37 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -175,7 +175,7 @@ by relying on a number of conventions that make it easy for Active Record to inf complex relations and structures from a minimal amount of explicit direction. Convention over Configuration: -* No XML-files! +* No XML files! * Lots of reflection and run-time extension * Magic is not inherently a bad word @@ -190,7 +190,7 @@ The latest version of Active Record can be installed with RubyGems: % [sudo] gem install activerecord -Source code can be downloaded as part of the Rails project on GitHub +Source code can be downloaded as part of the Rails project on GitHub: * https://github.com/rails/rails/tree/master/activerecord @@ -204,7 +204,7 @@ Active Record is released under the MIT license: == Support -API documentation is at +API documentation is at: * http://api.rubyonrails.org diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index 2f3d516c43..ca1f2fd665 100644 --- a/activerecord/RUNNING_UNIT_TESTS.rdoc +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc @@ -1,31 +1,44 @@ == Setup -If you don't have the environment set make sure to read - - http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#testing-active-record +If you don't have an environment for running tests, read +http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#setting-up-a-development-environment == Running the Tests -You can run a particular test file from the command line, e.g. +To run a specific test: + + $ ruby -Itest test/cases/base_test.rb -n method_name + +To run a set of tests: $ ruby -Itest test/cases/base_test.rb -To run a specific test: +You can also run tests that depend upon a specific database backend. For +example: - $ ruby -Itest test/cases/base_test.rb -n test_something_works + $ bundle exec rake test_sqlite3 -You can run with a database other than the default you set in test/config.yml, using the ARCONN -environment variable: +Simply executing <tt>bundle exec rake test</tt> is equivalent to the following: - $ ARCONN=postgresql ruby -Itest test/cases/base_test.rb + $ bundle exec rake test_mysql + $ bundle exec rake test_mysql2 + $ bundle exec rake test_postgresql + $ bundle exec rake test_sqlite3 + $ bundle exec rake test_sqlite3_mem -You can run all the tests for a given database via rake: +There should be tests available for each database backend listed in the {Config +File}[rdoc-label:label-Config+File]. (the exact set of available tests is +defined in +Rakefile+) - $ rake test_mysql +== Config File -The 'rake test' task will run all the tests for mysql, mysql2, sqlite3 and postgresql. +If +test/config.yml+ is present, it's parameters are obeyed. Otherwise, the +parameters in +test/config.example.yml+ are obeyed. -== Custom Config file +You can override the +connections:+ parameter in either file using the +ARCONN+ +(Active Record CONNection) environment variable: + + $ ARCONN=postgresql ruby -Itest test/cases/base_test.rb -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. +You can specify a custom location for the config file using the +ARCONFIG+ +environment variable. diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 0523314128..cee1dd5aeb 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,5 +1,4 @@ require 'rake/testtask' -require 'rake/packagetask' require 'rubygems/package_task' require File.expand_path(File.dirname(__FILE__)) + "/test/config" @@ -59,11 +58,10 @@ end task "isolated_test_#{adapter}" do adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] puts [adapter, adapter_short].inspect - ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) (Dir["test/cases/**/*_test.rb"].reject { |x| x =~ /\/adapters\// } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| - sh(ruby, "-Itest", file) + sh(Gem.ruby, '-w' ,"-Itest", file) end or raise "Failures" end @@ -120,14 +118,9 @@ namespace :postgresql do %x( createdb -E UTF8 -T template0 #{config['arunit']['database']} ) %x( createdb -E UTF8 -T template0 #{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 + # notify about preparing hstore + if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0" + puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html" end end @@ -225,7 +218,7 @@ end # Publishing ------------------------------------------------------ -desc "Release to gemcutter" +desc "Release to rubygems" task :release => :package do require 'rake/gemcutter' Rake::Gemcutter::Tasks.new(spec).define diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 89a62f0873..9986ded904 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,6 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '~> 4.0.0.beta1' - s.add_dependency 'activerecord-deprecated_finders', '~> 0.0.3' + s.add_dependency 'arel', '~> 4.0.0' end diff --git a/activerecord/examples/associations.png b/activerecord/examples/associations.png Binary files differdeleted file mode 100644 index 661c7a8bbc..0000000000 --- a/activerecord/examples/associations.png +++ /dev/null diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index ad12f8597f..d3546ce948 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -5,12 +5,12 @@ require 'benchmark/ips' TIME = (ENV['BENCHMARK_TIME'] || 20).to_i RECORDS = (ENV['BENCHMARK_RECORDS'] || TIME*1000).to_i -conn = { :adapter => 'sqlite3', :database => ':memory:' } +conn = { adapter: 'sqlite3', database: ':memory:' } ActiveRecord::Base.establish_connection(conn) class User < ActiveRecord::Base - connection.create_table :users, :force => true do |t| + connection.create_table :users, force: true do |t| t.string :name, :email t.timestamps end @@ -19,7 +19,7 @@ class User < ActiveRecord::Base end class Exhibit < ActiveRecord::Base - connection.create_table :exhibits, :force => true do |t| + connection.create_table :exhibits, force: true do |t| t.belongs_to :user t.string :name t.text :notes @@ -43,6 +43,8 @@ class Exhibit < ActiveRecord::Base def self.feel(exhibits) exhibits.each { |e| e.feel } end end +def progress_bar(int); print "." if (int%100).zero? ; end + puts 'Generating data...' module ActiveRecord @@ -75,30 +77,32 @@ notes = ActiveRecord::Faker::LOREM.join ' ' today = Date.today puts "Inserting #{RECORDS} users and exhibits..." -RECORDS.times do +RECORDS.times do |record| user = User.create( - :created_at => today, - :name => ActiveRecord::Faker.name, - :email => ActiveRecord::Faker.email + created_at: today, + name: ActiveRecord::Faker.name, + email: ActiveRecord::Faker.email ) Exhibit.create( - :created_at => today, - :name => ActiveRecord::Faker.name, - :user => user, - :notes => notes + created_at: today, + name: ActiveRecord::Faker.name, + user: user, + notes: notes ) + progress_bar(record) end +puts "Done!\n" Benchmark.ips(TIME) do |x| ar_obj = Exhibit.find(1) - attrs = { :name => 'sam' } - attrs_first = { :name => 'sam' } - attrs_second = { :name => 'tom' } + attrs = { name: 'sam' } + attrs_first = { name: 'sam' } + attrs_second = { name: 'tom' } exhibit = { - :name => ActiveRecord::Faker.name, - :notes => notes, - :created_at => Date.today + name: ActiveRecord::Faker.name, + notes: notes, + created_at: Date.today } x.report("Model#id") do @@ -117,10 +121,18 @@ Benchmark.ips(TIME) do |x| Exhibit.first.look end + x.report 'Model.take' do + Exhibit.take + end + x.report("Model.all limit(100)") do Exhibit.look Exhibit.limit(100) end + x.report("Model.all take(100)") do + Exhibit.look Exhibit.take(100) + end + x.report "Model.all limit(100) with relationship" do Exhibit.feel Exhibit.limit(100).includes(:user) end @@ -167,6 +179,6 @@ Benchmark.ips(TIME) do |x| end x.report "AR.execute(query)" do - ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}") + ActiveRecord::Base.connection.execute("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}") end end diff --git a/activerecord/examples/simple.rb b/activerecord/examples/simple.rb index c12f746992..4ed5d80eb2 100644 --- a/activerecord/examples/simple.rb +++ b/activerecord/examples/simple.rb @@ -1,14 +1,14 @@ -$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib" +require File.expand_path('../../../load_paths', __FILE__) require 'active_record' class Person < ActiveRecord::Base - establish_connection :adapter => 'sqlite3', :database => 'foobar.db' - connection.create_table table_name, :force => true do |t| + establish_connection adapter: 'sqlite3', database: 'foobar.db' + connection.create_table table_name, force: true do |t| t.string :name end end -bob = Person.create!(:name => 'bob') +bob = Person.create!(name: 'bob') puts Person.all.inspect bob.destroy puts Person.all.inspect diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index c33f03f13f..f19f5ecdf9 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -25,7 +25,6 @@ require 'active_support' require 'active_support/rails' require 'active_model' require 'arel' -require 'active_record/deprecated_finders' require 'active_record/version' @@ -35,8 +34,8 @@ module ActiveRecord autoload :Base autoload :Callbacks autoload :Core - autoload :CounterCache autoload :ConnectionHandling + autoload :CounterCache autoload :DynamicMatchers autoload :Explain autoload :Inheritance @@ -50,12 +49,14 @@ module ActiveRecord autoload :Querying autoload :ReadonlyAttributes autoload :Reflection + autoload :RuntimeRegistry autoload :Sanitization autoload :Schema autoload :SchemaDumper autoload :SchemaMigration autoload :Scoping autoload :Serialization + autoload :StatementCache autoload :Store autoload :Timestamp autoload :Transactions @@ -69,11 +70,12 @@ module ActiveRecord autoload :Aggregations autoload :Associations - autoload :AttributeMethods autoload :AttributeAssignment + autoload :AttributeMethods autoload :AutosaveAssociation autoload :Relation + autoload :AssociationRelation autoload :NullRelation autoload_under 'relation' do @@ -145,7 +147,6 @@ module ActiveRecord 'active_record/tasks/postgresql_database_tasks' end - autoload :TestCase autoload :TestFixtures, 'active_record/fixtures' def self.eager_load! diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 9d1c12ec62..0d5313956b 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -223,7 +223,8 @@ 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, nil, options, self) + reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self) + Reflection.add_aggregate_reflection self, part_id, reflection end private diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb new file mode 100644 index 0000000000..20516bba0c --- /dev/null +++ b/activerecord/lib/active_record/association_relation.rb @@ -0,0 +1,18 @@ +module ActiveRecord + class AssociationRelation < Relation + def initialize(klass, table, association) + super(klass, table) + @association = association + end + + def proxy_association + @association + end + + private + + def exec_queries + super.each { |r| @association.set_inverse_instance r } + end + end +end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 519e9112b8..74e2774626 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -73,12 +73,6 @@ module ActiveRecord end end - class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.") - end - end - class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: def initialize(reflection) super("Can not eagerly load the polymorphic association #{reflection.name.inspect}") @@ -114,7 +108,6 @@ module ActiveRecord autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' - autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association' autoload :HasManyAssociation, 'active_record/associations/has_many_association' autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association' autoload :HasOneAssociation, 'active_record/associations/has_one_association' @@ -164,7 +157,7 @@ module ActiveRecord private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) - @association_cache[name.to_sym] + @association_cache[name] end # Set the specified association instance. @@ -172,7 +165,7 @@ module ActiveRecord @association_cache[name] = association end - # Associations are a set of macro-like class methods for tying objects together through + # \Associations are a set of macro-like class methods for tying objects together through # foreign keys. They express relationships like "Project has one Project Manager" # or "Project belongs to a Portfolio". Each macro adds a number of methods to the # class which are specialized according to the collection or association symbol and the @@ -241,6 +234,7 @@ module ActiveRecord # others.destroy_all | X | X | X # others.find(*args) | X | X | X # others.exists? | X | X | X + # others.distinct | X | X | X # others.uniq | X | X | X # others.reset | X | X | X # @@ -364,11 +358,11 @@ module ActiveRecord # there is some special behavior you should be aware of, mostly involving the saving of # associated objects. # - # You can set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>, + # You can set the <tt>:autosave</tt> option on a <tt>has_one</tt>, <tt>belongs_to</tt>, # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it # to +true+ will _always_ save the members, whereas setting it to +false+ will - # _never_ save the members. More details about :autosave option is available at - # autosave_association.rb . + # _never_ save the members. More details about <tt>:autosave</tt> option is available at + # AutosaveAssociation. # # === One-to-one associations # @@ -401,7 +395,7 @@ module ActiveRecord # # == Customizing the query # - # Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax + # \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 @@ -567,6 +561,8 @@ module ActiveRecord # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around # @group.avatars.delete(@group.avatars.last) # so would this # + # == Setting Inverses + # # If you are using a +belongs_to+ on the join model, it is a good idea to set the # <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example # works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association): @@ -583,7 +579,27 @@ module ActiveRecord # belongs_to :tag, inverse_of: :taggings # end # - # == Nested Associations + # If you do not set the <tt>:inverse_of</tt> record, the association will + # do its best to match itself up with the correct inverse. Automatic + # inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and + # <tt>belongs_to</tt> associations. + # + # Extra options on the associations, as defined in the + # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will + # also prevent the association's inverse from being found automatically. + # + # The automatic guessing of the inverse association uses a heuristic based + # on the name of the class, so it may not work for all associations, + # especially the ones with non-standard names. + # + # You can turn off the automatic detection of inverse associations by setting + # the <tt>:inverse_of</tt> option to <tt>false</tt> like so: + # + # class Taggable < ActiveRecord::Base + # belongs_to :tag, inverse_of: false + # end + # + # == 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: @@ -626,7 +642,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 @@ -788,7 +804,7 @@ module ActiveRecord # For example if all the addressables are either of class Person or Company then a total # of 3 queries will be executed. The list of addressable types to load is determined on # the back of the addresses loaded. This is not supported if Active Record has to fallback - # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. + # to the previous implementation of eager loading and will raise <tt>ActiveRecord::EagerLoadPolymorphicError</tt>. # The reason is that the parent model's type is a column value so its corresponding table # name cannot be put in the +FROM+/+JOIN+ clauses of that query. # @@ -965,7 +981,7 @@ module ActiveRecord # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they # cause the records in the join table to be removed. # - # For +has_many+, <tt>destroy</tt> and <tt>destory_all</tt> will always call the <tt>destroy</tt> method of the + # For +has_many+, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. @@ -987,7 +1003,7 @@ module ActiveRecord # associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+ # <tt>:through</tt>, the join records will be deleted, but the associated records won't. # - # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by_name('food'))</tt> + # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt> # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself # to be removed from the database. # @@ -1023,7 +1039,7 @@ module ActiveRecord # An empty array is returned if none are found. # [collection<<(object, ...)] # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. - # Note that this operation instantly fires update sql without waiting for the save or update call on the + # Note that this operation instantly fires update SQL without waiting for the save or update call on the # parent object, unless the parent object is a new record. # [collection.delete(object, ...)] # Removes one or more objects from the collection by setting their foreign keys to +NULL+. @@ -1059,10 +1075,10 @@ module ActiveRecord # [collection.size] # Returns the number of associated objects. # [collection.find(...)] - # Finds an associated object according to the same rules as ActiveRecord::Base.find. + # Finds an associated object according to the same rules as <tt>ActiveRecord::Base.find</tt>. # [collection.exists?(...)] # Checks whether an associated object with the given conditions exists. - # Uses the same rules as ActiveRecord::Base.exists?. + # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>. # [collection.build(attributes = {}, ...)] # Returns one or more new objects of the collection type that have been instantiated # with +attributes+ and linked to this object through a foreign key, but have not yet @@ -1072,13 +1088,16 @@ module ActiveRecord # with +attributes+, linked to this object through a foreign key, and that has already # been saved (if it passed the validation). *Note*: This only works if the base model # already exists in the DB, not if it is a new (unsaved) record! + # [collection.create!(attributes = {})] + # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # if the record is invalid. # # (*Note*: +collection+ is replaced with the symbol passed as the first argument, so # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.) # # === Example # - # Example: A Firm class declares <tt>has_many :clients</tt>, which will add: + # A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add: # * <tt>Firm#clients</tt> (similar to <tt>Client.where(firm_id: id)</tt>) # * <tt>Firm#clients<<</tt> # * <tt>Firm#clients.delete</tt> @@ -1093,6 +1112,7 @@ module ActiveRecord # * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>) # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) + # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>) # The declaration can also include an options hash to specialize the behavior of the association. # # === Options @@ -1111,14 +1131,14 @@ module ActiveRecord # Controls what happens to the associated objects when # their owner is destroyed. Note that these are implemented as # callbacks, and Rails executes callbacks in order. Therefore, other - # similar callbacks may affect the :dependent behavior, and the - # :dependent behavior may affect other callbacks. + # similar callbacks may affect the <tt>:dependent</tt> behavior, and the + # <tt>:dependent</tt> behavior may affect other callbacks. # - # * <tt>:destroy</tt> causes all the associated objects to also be destroyed - # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not execute) + # * <tt>:destroy</tt> causes all the associated objects to also be destroyed. + # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed). # * <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 + # * <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 @@ -1158,8 +1178,8 @@ module ActiveRecord # 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. 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. + # +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] @@ -1178,13 +1198,14 @@ module ActiveRecord # 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) + reflection = Builder::HasMany.build(self, name, scope, options, &extension) + Reflection.add_reflection self, name, reflection end # Specifies a one-to-one association with another class. This method should only be used # if the other class contains the foreign key. If the current class contains the foreign key, # then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview - # on when to use has_one and when to use belongs_to. + # on when to use +has_one+ and when to use +belongs_to+. # # The following methods for retrieval and query of a single associated object will be added: # @@ -1281,7 +1302,8 @@ module ActiveRecord # 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) + reflection = Builder::HasOne.build(self, name, scope, options) + Reflection.add_reflection self, name, reflection end # Specifies a one-to-one association with another class. This method should only be used @@ -1352,7 +1374,7 @@ module ActiveRecord # 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) - that is the migration for - # <tt>#{table_name}_count</tt> is created on the associate class (such that Post.comments_count will + # <tt>#{table_name}_count</tt> is created on the associate class (such that <tt>Post.comments_count</tt> 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>.) @@ -1393,7 +1415,8 @@ module ActiveRecord # 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) + reflection = Builder::BelongsTo.build(self, name, scope, options) + Reflection.add_reflection self, name, reflection end # Specifies a many-to-many relationship with another class. This associates two classes via an @@ -1407,6 +1430,8 @@ module ActiveRecord # to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the # custom <tt>:join_table</tt> option if you need to. + # If your tables share a common prefix, it will only appear once at the beginning. For example, + # the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products". # # The join table should not have a primary key or a model associated with it. You must manually generate the # join table with a migration such as this: @@ -1432,7 +1457,7 @@ module ActiveRecord # [collection<<(object, ...)] # Adds one or more objects to the collection by creating associations in the join table # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method). - # Note that this operation instantly fires update sql without waiting for the save or update call on the + # Note that this operation instantly fires update SQL without waiting for the save or update call on the # parent object, unless the parent object is a new record. # [collection.delete(object, ...)] # Removes one or more objects from the collection by removing their associations from the join table. @@ -1455,10 +1480,10 @@ module ActiveRecord # [collection.find(id)] # Finds an associated object responding to the +id+ and that # meets the condition that it has to be associated with this object. - # Uses the same rules as ActiveRecord::Base.find. + # Uses the same rules as <tt>ActiveRecord::Base.find</tt>. # [collection.exists?(...)] # Checks whether an associated object with the given conditions exists. - # Uses the same rules as ActiveRecord::Base.exists?. + # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>. # [collection.build(attributes = {})] # Returns a new object of the collection type that has been instantiated # with +attributes+ and linked to this object through the join table, but has not yet been saved. @@ -1528,7 +1553,39 @@ module ActiveRecord # 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) + if scope.is_a?(Hash) + options = scope + scope = nil + end + + builder = Builder::HasAndBelongsToMany.new name, self, options + + join_model = builder.through_model + + middle_reflection = builder.middle_reflection join_model + + Builder::HasMany.define_callbacks self, middle_reflection + Reflection.add_reflection self, middle_reflection.name, middle_reflection + + include Module.new { + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def destroy_associations + association(:#{middle_reflection.name}).delete_all(:delete_all) + association(:#{name}).reset + super + end + RUBY + } + + hm_options = {} + hm_options[:through] = middle_reflection.name + hm_options[:source] = join_model.right_reflection.name + + [:before_add, :after_add, :before_remove, :after_remove].each do |k| + hm_options[k] = options[k] if options.key? k + end + + has_many name, scope, hm_options, &extension end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 868095f068..e6a45487d0 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -13,11 +13,11 @@ module ActiveRecord # BelongsToAssociation # BelongsToPolymorphicAssociation # CollectionAssociation - # HasAndBelongsToManyAssociation # HasManyAssociation # HasManyThroughAssociation + ThroughAssociation class Association #:nodoc: attr_reader :owner, :target, :reflection + attr_accessor :inversed delegate :options, :to => :reflection @@ -30,7 +30,7 @@ module ActiveRecord reset_scope end - # Returns the name of the table of the related class: + # Returns the name of the table of the associated class: # # post.comments.aliased_table_name # => "comments" # @@ -43,6 +43,7 @@ module ActiveRecord @loaded = false @target = nil @stale_state = nil + @inversed = false end # Reloads the \target and returns +self+ on success. @@ -60,8 +61,9 @@ module ActiveRecord # Asserts the \target has been loaded setting the \loaded flag to +true+. def loaded! - @loaded = true + @loaded = true @stale_state = stale_state + @inversed = false end # The target is stale if the target no longer points to the record(s) that the @@ -71,7 +73,7 @@ module ActiveRecord # # Note that if the target has not been loaded, it is not considered stale. def stale_target? - loaded? && @stale_state != stale_state + !inversed && loaded? && @stale_state != stale_state end # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+. @@ -84,15 +86,10 @@ module ActiveRecord 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 - # scoped method is called. This is because at that point the call may be surrounded + # scope method is called. This is because at that point the call may be surrounded # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which # actually gets built. def association_scope @@ -110,10 +107,11 @@ module ActiveRecord if record && invertible_for?(record) inverse = record.association(inverse_reflection_for(record).name) inverse.target = owner + inverse.inversed = true end end - # This class of the target. belongs_to polymorphic overrides this to look at the + # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. def klass reflection.klass @@ -122,7 +120,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.all + AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all) end # Loads the \target if needed and returns it. @@ -164,6 +162,13 @@ module ActiveRecord @reflection = @owner.class.reflect_on_association(reflection_name) end + def initialize_attributes(record) #:nodoc: + skip_assign = [reflection.foreign_key, reflection.type].compact + attributes = create_scope.except(*(record.changed - skip_assign)) + record.assign_attributes(attributes) + set_inverse_instance(record) + end + private def find_target? @@ -189,13 +194,14 @@ module ActiveRecord creation_attributes.each { |key, value| record[key] = value } end - # Should be true if there is a foreign key present on the owner which + # Returns true if there is a foreign key present on the owner which # references the target. This is used to determine whether we can load # the target if the owner is currently a new record (and therefore - # without a key). + # without a key). If the owner is a new record then foreign_key must + # be present in order to load target. # # Currently implemented by belongs_to (vanilla and polymorphic) and - # has_one/has_many :through associations which go through a belongs_to + # has_one/has_many :through associations which go through a belongs_to. def foreign_key_present? false end @@ -203,7 +209,7 @@ module ActiveRecord # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of # the kind of the class of the associated objects. Meant to be used as # a sanity check when you are about to assign an associated record. - def raise_on_type_mismatch(record) + def raise_on_type_mismatch!(record) unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize) message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" raise ActiveRecord::AssociationTypeMismatch, message @@ -217,7 +223,8 @@ module ActiveRecord reflection.inverse_of end - # Is this association invertible? Can be redefined by subclasses. + # Returns true if inverse association on the given record needs to be set. + # This method is redefined by subclasses. def invertible_for?(record) inverse_reflection_for(record) end @@ -232,9 +239,7 @@ module ActiveRecord def build_record(attributes) reflection.build_association(attributes) do |record| - skip_assign = [reflection.foreign_key, reflection.type].compact - attributes = create_scope.except(*(record.changed - skip_assign)) - record.assign_attributes(attributes) + initialize_attributes(record) end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index c5fb1fe2c7..17f056e764 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -22,7 +22,7 @@ module ActiveRecord private def column_for(table_name, column_name) - columns = alias_tracker.connection.schema_cache.columns_hash[table_name] + columns = alias_tracker.connection.schema_cache.columns_hash(table_name) columns[column_name] end @@ -44,18 +44,6 @@ module ActiveRecord chain.each_with_index do |reflection, i| table, foreign_table = tables.shift, tables.first - if reflection.source_macro == :has_and_belongs_to_many - join_table = tables.shift - - scope = scope.joins(join( - join_table, - table[reflection.association_primary_key]. - eq(join_table[reflection.association_foreign_key]) - )) - - table, foreign_table = join_table, tables.first - end - if reflection.source_macro == :belongs_to if reflection.options[:polymorphic] key = reflection.association_primary_key(self.klass) @@ -82,25 +70,32 @@ module ActiveRecord constraint = table[key].eq(foreign_table[foreign_key]) if reflection.type - type = chain[i + 1].klass.base_class.name - constraint = constraint.and(table[reflection.type].eq(type)) + value = chain[i + 1].klass.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 scope = scope.joins(join(foreign_table, constraint)) end + is_first_chain = i == 0 + klass = is_first_chain ? self.klass : reflection.klass + # Exclude the scope of the association itself, because that # was already merged in the #scope method. scope_chain[i].each do |scope_chain_item| - klass = i == 0 ? self.klass : reflection.klass item = eval_scope(klass, scope_chain_item) if scope_chain_item == self.reflection.scope - scope.merge! item.except(:where, :includes) + scope.merge! item.except(:where, :includes, :bind) + end + + if is_first_chain + scope.includes! item.includes_values end - scope.includes! item.includes_values scope.where_values += item.where_values + scope.order_values |= item.order_values end end @@ -118,7 +113,7 @@ module ActiveRecord # the owner klass.table_name else - reflection.table_name + super 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 54b1a69774..e1fa5225b5 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -8,7 +8,7 @@ module ActiveRecord end def replace(record) - raise_on_type_mismatch(record) if record + raise_on_type_mismatch!(record) if record update_counters(record) replace_keys(record) @@ -50,8 +50,8 @@ module ActiveRecord # Checks whether record is different to the current target, without loading it def different_target?(record) - if record.nil? - owner[reflection.foreign_key] + if record.nil? + owner[reflection.foreign_key] else record.id != owner[reflection.foreign_key] 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 88ce03a3cd..eae5eed3a1 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -22,7 +22,7 @@ module ActiveRecord reflection.polymorphic_inverse_of(record.class) end - def raise_on_type_mismatch(record) + def raise_on_type_mismatch!(record) # A polymorphic association cannot have a type mismatch, by definition end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 5c37f42794..d8d68eb908 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,70 +1,100 @@ +# This is the parent Association class which defines the variables +# used by all associations. +# +# The hierarchy is defined as follows: +# Association +# - SingularAssociation +# - BelongsToAssociation +# - HasOneAssociation +# - CollectionAssociation +# - HasManyAssociation + module ActiveRecord::Associations::Builder class Association #:nodoc: class << self - attr_accessor :valid_options + attr_accessor :extensions end + self.extensions = [] - self.valid_options = [:class_name, :foreign_key, :validate] - - attr_reader :model, :name, :scope, :options, :reflection + VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] - def self.build(*args, &block) - new(*args, &block).build + def self.build(model, name, scope, options, &block) + extension = define_extensions model, name, &block + reflection = create_reflection model, name, scope, options, extension + define_accessors model, reflection + define_callbacks model, reflection + reflection end - def initialize(model, name, scope, options) + def self.create_reflection(model, name, scope, options, extension = nil) raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) - @model = model - @name = name - if scope.is_a?(Hash) - @scope = nil - @options = scope - else - @scope = scope - @options = options + options = scope + scope = nil end - if @scope && @scope.arity == 0 - prev_scope = @scope - @scope = proc { instance_exec(&prev_scope) } + validate_options(options) + + scope = build_scope(scope, extension) + + ActiveRecord::Reflection.create(macro, name, scope, options, model) + end + + def self.build_scope(scope, extension) + new_scope = scope + + if scope && scope.arity == 0 + new_scope = proc { instance_exec(&scope) } + end + + if extension + new_scope = wrap_scope new_scope, extension end + + new_scope end - def mixin - @model.generated_feature_methods + def self.wrap_scope(scope, extension) + scope end - include Module.new { def build; end } + def self.macro + raise NotImplementedError + end - def build - validate_options - define_accessors - configure_dependency if options[:dependent] - @reflection = model.create_reflection(macro, name, scope, options, model) - super # provides an extension point - @reflection + def self.valid_options(options) + VALID_OPTIONS + Association.extensions.flat_map(&:valid_options) end - def macro - raise NotImplementedError + def self.validate_options(options) + options.assert_valid_keys(valid_options(options)) end - def valid_options - Association.valid_options + def self.define_extensions(model, name) end - def validate_options - options.assert_valid_keys(valid_options) + def self.define_callbacks(model, reflection) + add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent] + Association.extensions.each do |extension| + extension.build model, reflection + end end - def define_accessors - define_readers - define_writers + # Defines the setter and getter methods for the association + # class Post < ActiveRecord::Base + # has_many :comments + # end + # + # Post.first.comments and Post.first.comments= methods are defined by this method... + def self.define_accessors(model, reflection) + mixin = model.generated_feature_methods + name = reflection.name + define_readers(mixin, name) + define_writers(mixin, name) end - def define_readers + def self.define_readers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}(*args) association(:#{name}).reader(*args) @@ -72,7 +102,7 @@ module ActiveRecord::Associations::Builder CODE end - def define_writers + def self.define_writers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}=(value) association(:#{name}).writer(value) @@ -80,29 +110,17 @@ module ActiveRecord::Associations::Builder CODE end - 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 self.valid_dependent_options + raise NotImplementedError + end - if options[:dependent] == :restrict - ActiveSupport::Deprecation.warn( - "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \ - "provides the same functionality." - ) + def self.add_before_destroy_callbacks(model, reflection) + unless valid_dependent_options.include? reflection.options[:dependent] + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}" end - mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{macro}_dependent_for_#{name} - association(:#{name}).handle_dependency - end - CODE - - model.before_destroy "#{macro}_dependent_for_#{name}" - end - - def valid_dependent_options - raise NotImplementedError + name = reflection.name + model.before_destroy lambda { |o| o.association(name).handle_dependency } 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 97b1ff18e2..aa43c34d86 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,66 +1,130 @@ module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: - def macro + def self.macro :belongs_to end - def valid_options + def self.valid_options(options) super + [:foreign_type, :polymorphic, :touch] end - def constructable? - !options[:polymorphic] + def self.valid_dependent_options + [:destroy, :delete] end - def build - reflection = super - add_counter_cache_callbacks(reflection) if options[:counter_cache] - add_touch_callbacks(reflection) if options[:touch] - reflection + def self.define_callbacks(model, reflection) + super + add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache] + add_touch_callbacks(model, reflection) if reflection.options[:touch] end - def add_counter_cache_callbacks(reflection) - cache_column = reflection.counter_cache_column + def self.define_accessors(mixin, reflection) + super + add_counter_cache_methods mixin + end + + def self.add_counter_cache_methods(mixin) + return if mixin.method_defined? :belongs_to_counter_cache_after_create + + mixin.class_eval do + def belongs_to_counter_cache_after_create(reflection) + if record = send(reflection.name) + cache_column = reflection.counter_cache_column + record.class.increment_counter(cache_column, record.id) + @_after_create_counter_called = true + end + end - 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? + def belongs_to_counter_cache_before_destroy(reflection) + foreign_key = reflection.foreign_key.to_sym + unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key + record = send reflection.name + if record && !self.destroyed? + cache_column = reflection.counter_cache_column + record.class.decrement_counter(cache_column, record.id) + end + end end - def belongs_to_counter_cache_before_destroy_for_#{name} - unless marked_for_destruction? - record = #{name} - record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil? + def belongs_to_counter_cache_after_update(reflection) + foreign_key = reflection.foreign_key + cache_column = reflection.counter_cache_column + + if (@_after_create_counter_called ||= false) + @_after_create_counter_called = false + elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable? + model = reflection.klass + foreign_key_was = attribute_was foreign_key + foreign_key = attribute foreign_key + + if foreign_key && model.respond_to?(:increment_counter) + model.increment_counter(cache_column, foreign_key) + end + if foreign_key_was && model.respond_to?(:decrement_counter) + model.decrement_counter(cache_column, foreign_key_was) + end end end - CODE + end + end + + def self.add_counter_cache_callbacks(model, reflection) + cache_column = reflection.counter_cache_column + + model.after_create lambda { |record| + record.belongs_to_counter_cache_after_create(reflection) + } + + model.before_destroy lambda { |record| + record.belongs_to_counter_cache_before_destroy(reflection) + } - model.after_create "belongs_to_counter_cache_after_create_for_#{name}" - model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}" + model.after_update lambda { |record| + record.belongs_to_counter_cache_after_update(reflection) + } klass = reflection.class_name.safe_constantize klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) end - def add_touch_callbacks(reflection) - mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def belongs_to_touch_after_save_or_destroy_for_#{name} - record = #{name} + def self.touch_record(o, foreign_key, name, touch) # :nodoc: + old_foreign_id = o.changed_attributes[foreign_key] - unless record.nil? || record.new_record? - record.touch #{options[:touch].inspect if options[:touch] != true} + if old_foreign_id + klass = o.association(name).klass + old_record = klass.find_by(klass.primary_key => old_foreign_id) + + if old_record + if touch != true + old_record.touch touch + else + old_record.touch end end - CODE + 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}" + record = o.send name + unless record.nil? || record.new_record? + if touch != true + record.touch touch + else + record.touch + end + end end - def valid_dependent_options - [:destroy, :delete] + def self.add_touch_callbacks(model, reflection) + foreign_key = reflection.foreign_key + n = reflection.name + touch = reflection.options[:touch] + + callback = lambda { |record| + BelongsTo.touch_record(record, foreign_key, n, touch) + } + + model.after_save callback + model.after_touch callback + model.after_destroy callback 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 fdead16761..2ff67f904d 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,3 +1,5 @@ +# This class is inherited by the has_many and has_many_and_belongs_to_many association classes + require 'active_record/associations' module ActiveRecord::Associations::Builder @@ -5,68 +7,48 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] - def valid_options - super + [:table_name, :finder_sql, :counter_sql, :before_add, + def self.valid_options(options) + super + [:table_name, :before_add, :after_add, :before_remove, :after_remove, :extend] end - 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) } - reflection - end - - def writable? - true + def self.define_callbacks(model, reflection) + super + name = reflection.name + options = reflection.options + CALLBACKS.each { |callback_name| + define_callback(model, callback_name, name, options) + } end - 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 + def self.define_extensions(model, name) + if block_given? + extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" + extension = Module.new(&Proc.new) + model.parent.const_set(extension_module_name, extension) 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 - - prev_scope = @scope + def self.define_callback(model, callback_name, name, options) + full_callback_name = "#{callback_name}_for_#{name}" - if prev_scope - @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) } + # TODO : why do i need method_defined? I think its because of the inheritance chain + model.class_attribute full_callback_name unless model.method_defined?(full_callback_name) + callbacks = Array(options[callback_name.to_sym]).map do |callback| + case callback + when Symbol + ->(method, owner, record) { owner.send(callback, record) } + when Proc + ->(method, owner, record) { callback.call(owner, record) } else - @scope = proc { extending(mod) } + ->(method, owner, record) { callback.send(method, owner, record) } end end + model.send "#{full_callback_name}=", callbacks 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}" - - # 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 + # Defines the setter and getter methods for the collection_singular_ids. + def self.define_readers(mixin, name) super mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 @@ -76,7 +58,7 @@ module ActiveRecord::Associations::Builder CODE end - def define_writers + def self.define_writers(mixin, name) super mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 @@ -85,5 +67,13 @@ module ActiveRecord::Associations::Builder end CODE end + + def self.wrap_scope(scope, mod) + if scope + proc { |owner| instance_exec(owner, &scope).extending(mod) } + else + proc { extending(mod) } + end + 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 bdac02b5bf..1c9c04b044 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,39 +1,121 @@ module ActiveRecord::Associations::Builder - class HasAndBelongsToMany < CollectionAssociation #:nodoc: - def macro - :has_and_belongs_to_many - end + class HasAndBelongsToMany # :nodoc: + class JoinTableResolver + KnownTable = Struct.new :join_table + + class KnownClass + def initialize(lhs_class, rhs_class_name) + @lhs_class = lhs_class + @rhs_class_name = rhs_class_name + @join_table = nil + end + + def join_table + @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + end + + private + def klass; @rhs_class_name.constantize; end + end - def valid_options - super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql] + def self.build(lhs_class, name, options) + if options[:join_table] + KnownTable.new options[:join_table] + else + class_name = options.fetch(:class_name) { + name.to_s.camelize.singularize + } + KnownClass.new lhs_class, class_name + end + end end - def build - reflection = super - define_destroy_hook - reflection + attr_reader :lhs_model, :association_name, :options + + def initialize(association_name, lhs_model, options) + @association_name = association_name + @lhs_model = lhs_model + @options = options end - def show_deprecation_warnings - super + def through_model + habtm = JoinTableResolver.build lhs_model, association_name, options + + join_model = Class.new(ActiveRecord::Base) { + class << self; + attr_accessor :class_resolver + attr_accessor :name + attr_accessor :table_name_resolver + attr_accessor :left_reflection + attr_accessor :right_reflection + end + + def self.table_name + table_name_resolver.join_table + end + + def self.compute_type(class_name) + class_resolver.compute_type class_name + end + + def self.add_left_association(name, options) + belongs_to name, options + self.left_reflection = reflect_on_association(name) + end - [: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).") + def self.add_right_association(name, options) + rhs_name = name.to_s.singularize.to_sym + belongs_to rhs_name, options + self.right_reflection = reflect_on_association(rhs_name) end + + } + + join_model.name = "HABTM_#{association_name.to_s.camelize}" + join_model.table_name_resolver = habtm + join_model.class_resolver = lhs_model + + join_model.add_left_association :left_side, class: lhs_model + join_model.add_right_association association_name, belongs_to_options(options) + join_model + end + + def middle_reflection(join_model) + middle_name = [lhs_model.name.downcase.pluralize, + association_name].join('_').gsub(/::/, '_').to_sym + middle_options = middle_options join_model + + HasMany.create_reflection(lhs_model, + middle_name, + nil, + middle_options) + end + + private + + def middle_options(join_model) + middle_options = {} + middle_options[:class] = join_model + middle_options[:source] = join_model.left_reflection.name + if options.key? :foreign_key + middle_options[:foreign_key] = options[:foreign_key] end + middle_options 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 - }) + def belongs_to_options(options) + rhs_options = {} + + if options.key? :class_name + rhs_options[:foreign_key] = options[:class_name].foreign_key + rhs_options[:class_name] = options[:class_name] + end + + if options.key? :association_foreign_key + rhs_options[:foreign_key] = options[:association_foreign_key] + end + + rhs_options 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 0d1bdd21ee..227184cd19 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,15 +1,15 @@ module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: - def macro + def self.macro :has_many end - def valid_options + def self.valid_options(options) super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache] end - def valid_dependent_options - [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] + def self.valid_dependent_options + [:destroy, :delete_all, :nullify, :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 0da564f402..064a3c8b51 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,25 +1,21 @@ module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: - def macro + def self.macro :has_one end - def valid_options + def self.valid_options(options) valid = super + [:order, :as] valid += [:through, :source, :source_type] if options[:through] valid end - def constructable? - !options[:through] + def self.valid_dependent_options + [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception] end - def configure_dependency - super unless options[:through] - end - - def valid_dependent_options - [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] + def self.add_before_destroy_callbacks(model, reflection) + super unless reflection.options[:through] 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 6a5830e57f..2a4b1c441f 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -1,19 +1,18 @@ +# This class is inherited by the has_one and belongs_to association classes + module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: - def valid_options + def self.valid_options(options) super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of] end - def constructable? - true - end - - def define_accessors + def self.define_accessors(model, reflection) super - define_constructors if constructable? + define_constructors(model.generated_feature_methods, reflection.name) if reflection.constructable? end - def define_constructors + # Defines the (build|create)_association methods for belongs_to or has_one association + def self.define_constructors(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def build_#{name}(*args, &block) association(:#{name}).build(*args, &block) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 5feb149946..62f23f54f9 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -7,7 +7,6 @@ module ActiveRecord # collections. See the class hierarchy in AssociationProxy. # # CollectionAssociation: - # HasAndBelongsToManyAssociation => has_and_belongs_to_many # HasManyAssociation => has_many # HasManyThroughAssociation + ThroughAssociation => has_many :through # @@ -34,7 +33,7 @@ module ActiveRecord reload end - CollectionProxy.new(klass, self) + @proxy ||= CollectionProxy.create(klass, self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -44,7 +43,7 @@ module ActiveRecord # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items def ids_reader - if loaded? || options[:finder_sql] + if loaded? load_target.map do |record| record.send(reflection.association_primary_key) end @@ -79,8 +78,17 @@ module ActiveRecord if block_given? load_target.find(*args) { |*block_args| yield(*block_args) } else - if options[:finder_sql] - find_by_scan(*args) + if options[:inverse_of] && loaded? + args_flatten = args.flatten + raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank? + result = find_by_scan(*args) + + result_size = Array(result).size + if !result || result_size != args_flatten.size + scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size) + else + result + end else scope.find(*args) end @@ -141,11 +149,33 @@ module ActiveRecord end end - # Remove all records from this association. + # Removes all records from the association without calling callbacks + # on the associated records. It honors the `:dependent` option. However + # if the `:dependent` value is `:destroy` then in that case the `:delete_all` + # deletion strategy for the association is applied. + # + # You can force a particular deletion strategy by passing a parameter. + # + # Example: + # + # @author.books.delete_all(:nullify) + # @author.books.delete_all(:delete_all) # # See delete for more info. - def delete_all - delete(:all).tap do + def delete_all(dependent = nil) + if dependent.present? && ![:nullify, :delete_all].include?(dependent) + raise ArgumentError, "Valid values are :nullify or :delete_all" + end + + dependent = if dependent.present? + dependent + elsif options[:dependent] == :destroy + :delete_all + else + options[:dependent] + end + + delete(:all, dependent: dependent).tap do reset loaded! end @@ -161,35 +191,25 @@ module ActiveRecord end end - # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the - # association, it will be used for the query. Otherwise, construct options and pass them with + # Count all records using SQL. Construct options and pass them with # scope to the target class's +count+. - def count(column_name = nil, count_options = {}) - column_name, count_options = nil, column_name if column_name.is_a?(Hash) - - if options[:counter_sql] || options[:finder_sql] - unless count_options.blank? - raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" - end - - reflection.klass.count_by_sql(custom_counter_sql) - else - if association_scope.uniq_value - # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name ||= reflection.klass.primary_key - count_options[:distinct] = true - end + def count(column_name = nil) + relation = scope + if association_scope.distinct_value + # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. + column_name ||= reflection.klass.primary_key + relation = relation.distinct + end - value = scope.count(column_name, count_options) + value = relation.count(column_name) - limit = options[:limit] - offset = options[:offset] + limit = options[:limit] + offset = options[:offset] - if limit || offset - [ [value - offset.to_i, 0].max, limit.to_i ].min - else - value - end + if limit || offset + [ [value - offset.to_i, 0].max, limit.to_i ].min + else + value end end @@ -201,7 +221,8 @@ 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) - dependent = options[:dependent] + _options = records.extract_options! + dependent = _options[:dependent] || options[:dependent] if records.first == :all if loaded? || dependent == :destroy @@ -215,11 +236,11 @@ module ActiveRecord end end - # Destroy +records+ and remove them from this association calling - # +before_remove+ and +after_remove+ callbacks. + # Deletes the +records+ and removes them from this association calling + # +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks. # - # Note that this method will _always_ remove records from the database - # ignoring the +:dependent+ option. + # Note that this method removes records from the database ignoring the + # +:dependent+ option. def destroy(*records) records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } delete_or_destroy(records, :destroy) @@ -237,14 +258,14 @@ module ActiveRecord # +count_records+, which is a method descendants have to provide. def size if !find_target? || loaded? - if association_scope.uniq_value + if association_scope.distinct_value target.uniq.size else target.size end elsif !loaded? && !association_scope.group_values.empty? load_target.size - elsif !loaded? && !association_scope.uniq_value && target.is_a?(Array) + elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array) unsaved_records = target.select { |r| r.new_record? } unsaved_records.size + count_records else @@ -263,14 +284,14 @@ module ActiveRecord # 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 + # If the collection has been loaded + # 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? - if loaded? || options[:counter_sql] + if loaded? size.zero? else @target.blank? && !scope.exists? @@ -297,17 +318,18 @@ module ActiveRecord end end - def uniq + def distinct seen = {} load_target.find_all do |record| seen[record.id] = true unless seen.key?(record.id) end end + alias uniq distinct # 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) } + other_array.each { |val| raise_on_type_mismatch!(val) } original_target = load_target.dup if owner.new_record? @@ -322,7 +344,6 @@ module ActiveRecord if record.new_record? include_in_memory?(record) else - load_target if options[:finder_sql] loaded? ? target.include?(record) : scope.exists?(record) end else @@ -339,17 +360,17 @@ module ActiveRecord target end - def add_to_target(record) - callback(:before_add, record) + def add_to_target(record, skip_callbacks = false) + callback(:before_add, record) unless skip_callbacks yield(record) if block_given? - if association_scope.uniq_value && index = @target.index(record) + if association_scope.distinct_value && index = @target.index(record) @target[index] = record else @target << record end - callback(:after_add, record) + callback(:after_add, record) unless skip_callbacks set_inverse_instance(record) record @@ -367,31 +388,8 @@ module ActiveRecord private - def custom_counter_sql - if options[:counter_sql] - interpolate(options[:counter_sql]) - else - # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */ - interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do - count_with = $2.to_s - count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/ - "SELECT #{$1}COUNT(#{count_with}) FROM" - end - end - end - - def custom_finder_sql - interpolate(options[:finder_sql]) - end - def find_target - records = - if options[:finder_sql] - reflection.klass.find_by_sql(custom_finder_sql) - else - scope.to_a - end - + records = scope.to_a records.each { |record| set_inverse_instance(record) } records end @@ -454,7 +452,7 @@ module ActiveRecord def delete_or_destroy(records, method) records = records.flatten - records.each { |record| raise_on_type_mismatch(record) } + records.each { |record| raise_on_type_mismatch!(record) } existing_records = records.reject { |r| r.new_record? } if existing_records.empty? @@ -495,9 +493,9 @@ module ActiveRecord result = true records.flatten.each do |record| - raise_on_type_mismatch(record) - add_to_target(record) do |r| - result &&= insert_record(record) unless owner.new_record? + raise_on_type_mismatch!(record) + add_to_target(record) do |rec| + result &&= insert_record(rec) unless owner.new_record? end end @@ -506,20 +504,13 @@ module ActiveRecord def callback(method, record) callbacks_for(method).each do |callback| - case callback - when Symbol - owner.send(callback, record) - when Proc - callback.call(owner, record) - else - callback.send(method, owner, record) - end + callback.call(method, owner, record) end end def callbacks_for(callback_name) full_callback_name = "#{callback_name}_for_#{reflection.name}" - owner.class.send(full_callback_name.to_sym) || [] + owner.class.send(full_callback_name) end # Should we deal with assoc.first or assoc.last by issuing an independent query to @@ -530,24 +521,21 @@ module ActiveRecord # Otherwise, go to the database only if none of the following are true: # * target already loaded # * owner is new record - # * custom :finder_sql exists # * target contains new or changed record(s) - # * the first arg is an integer (which indicates the number of records to be returned) def fetch_first_or_last_using_find?(args) if args.first.is_a?(Hash) true else !(loaded? || owner.new_record? || - options[:finder_sql] || - target.any? { |record| record.new_record? || record.changed? } || - args.first.kind_of?(Integer)) + target.any? { |record| record.new_record? || record.changed? }) end end def include_in_memory?(record) if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) - owner.send(reflection.through_reflection.name).any? { |source| + assoc = owner.association(reflection.through_reflection.name) + assoc.reader.any? { |source| target = source.send(reflection.source_reflection.name) target.respond_to?(:include?) ? target.include?(record) : target == record } || target.include?(record) @@ -556,17 +544,18 @@ module ActiveRecord end end - # If using a custom finder_sql, #find scans the entire collection. + # If the :inverse_of option has been + # specified, then #find scans the entire collection. def find_by_scan(*args) expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq + ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq if ids.size == 1 id = ids.first - record = load_target.detect { |r| id == r.id } + record = load_target.detect { |r| id == r.id.to_s } expects_array ? [ record ] : record else - load_target.select { |r| ids.include?(r.id) } + load_target.select { |r| ids.include?(r.id.to_s) } end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 543204abac..2e70a07962 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -33,7 +33,6 @@ module ActiveRecord def initialize(klass, association) #:nodoc: @association = association super klass, klass.arel_table - self.default_scoped = true merge! association.scope(nullify: false) end @@ -92,7 +91,7 @@ module ActiveRecord # # => 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, + # This builds an array of objects from the database for the scope, # converting them into an array and iterating through them using # Array#select. # @@ -228,6 +227,7 @@ module ActiveRecord def build(attributes = {}, &block) @association.build(attributes, &block) end + alias_method :new, :build # 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 @@ -281,7 +281,7 @@ module ActiveRecord # so method calls may be chained. # # class Person < ActiveRecord::Base - # pets :has_many + # has_many :pets # end # # person.pets.size # => 0 @@ -303,7 +303,7 @@ module ActiveRecord @association.concat(*records) end - # Replace this collection with +other_array+. This will perform a diff + # Replaces this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. # # class Person < ActiveRecord::Base @@ -417,13 +417,13 @@ module ActiveRecord # # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound - def delete_all - @association.delete_all + def delete_all(dependent = nil) + @association.delete_all(dependent) end - # Deletes the records of the collection directly from the database. - # This will _always_ remove the records ignoring the +:dependent+ - # option. + # Deletes the records of the collection directly from the database + # ignoring the +:dependent+ option. It invokes +before_remove+, + # +after_remove+ , +before_destroy+ and +after_destroy+ callbacks. # # class Person < ActiveRecord::Base # has_many :pets @@ -649,11 +649,12 @@ module ActiveRecord # # #<Pet name: "Fancy-Fancy"> # # ] # - # person.pets.select(:name).uniq + # person.pets.select(:name).distinct # # => [#<Pet name: "Fancy-Fancy">] - def uniq - @association.uniq + def distinct + @association.distinct end + alias uniq distinct # Count all records using SQL. # @@ -668,8 +669,8 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def count(column_name = nil, options = {}) - @association.count(column_name, options) + def count(column_name = nil) + @association.count(column_name) end # Returns the size of the collection. If the collection hasn't been loaded, @@ -725,7 +726,7 @@ module ActiveRecord end # 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 + # loaded 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 @@ -828,11 +829,9 @@ module ActiveRecord # person.pets.include?(Pet.find(20)) # => true # person.pets.include?(Pet.find(21)) # => false def include?(record) - @association.include?(record) + !!@association.include?(record) end - alias_method :new, :build - def proxy_association @association end @@ -847,14 +846,8 @@ module ActiveRecord # Returns a <tt>Relation</tt> object for the records in this association def scope - association = @association - - @association.scope.extending! do - define_method(:proxy_association) { association } - end + @association.scope end - - # :nodoc: alias spawn scope # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays 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 deleted file mode 100644 index bb3e3db379..0000000000 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ /dev/null @@ -1,65 +0,0 @@ -module ActiveRecord - # = Active Record Has And Belongs To Many Association - module Associations - class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc: - attr_reader :join_table - - def initialize(owner, reflection) - @join_table = Arel::Table.new(reflection.join_table) - super - end - - def insert_record(record, validate = true, raise = false) - if record.new_record? - if raise - record.save!(:validate => validate) - else - return unless record.save(:validate => validate) - end - end - - if options[:insert_sql] - owner.connection.insert(interpolate(options[:insert_sql], record)) - else - stmt = join_table.compile_insert( - join_table[reflection.foreign_key] => owner.id, - join_table[reflection.association_foreign_key] => record.id - ) - - owner.class.connection.insert stmt - end - - record - end - - private - - def count_records - load_target.size - end - - def delete_records(records, method) - if sql = options[:delete_sql] - records = load_target if records == :all - records.each { |record| owner.class.connection.delete(interpolate(sql, record)) } - else - 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.class.connection.delete(relation.where(condition).compile_delete) - end - end - - def invertible_for?(record) - false - end - end - 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 f59565ae77..0a23109b9b 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -9,7 +9,7 @@ module ActiveRecord def handle_dependency case options[:dependent] - when :restrict, :restrict_with_exception + when :restrict_with_exception raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty? when :restrict_with_error @@ -22,15 +22,17 @@ module ActiveRecord 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) + load_target.each { |t| t.destroyed_by_association = reflection } + destroy_all + else + delete_all end - - delete_all end end def insert_record(record, validate = true, raise = false) set_owner_attributes(record) + set_inverse_instance(record) if raise record.save!(:validate => validate) @@ -57,8 +59,6 @@ module ActiveRecord def count_records count = if has_cached_counter? owner.send(:read_attribute, cached_counter_attribute_name) - elsif options[:counter_sql] || options[:finder_sql] - reflection.klass.count_by_sql(custom_counter_sql) else scope.count end @@ -114,8 +114,7 @@ module ActiveRecord 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) + scope = self.scope.where(reflection.klass.primary_key => records) end if method == :delete_all @@ -127,7 +126,11 @@ module ActiveRecord end def foreign_key_present? - owner.attribute_present?(reflection.association_primary_key) + if reflection.klass.primary_key + owner.attribute_present?(reflection.association_primary_key) + else + false + end end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index d1458f30ba..56331bbb0b 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -29,7 +29,7 @@ module ActiveRecord def concat(*records) unless owner.new_record? records.flatten.each do |record| - raise_on_type_mismatch(record) + raise_on_type_mismatch!(record) record.save! if record.new_record? end end @@ -140,7 +140,21 @@ module ActiveRecord case method when :destroy - count = scope.destroy_all.length + if scope.klass.primary_key + count = scope.destroy_all.length + else + scope.to_a.each do |record| + record.run_callbacks :destroy + end + + arel = scope.arel + + stmt = Arel::DeleteManager.new arel.engine + stmt.from scope.klass.arel_table + stmt.wheres = arel.constraints + + count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values) + end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) else diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index ee816d2392..0008600418 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -6,7 +6,7 @@ module ActiveRecord def handle_dependency case options[:dependent] - when :restrict, :restrict_with_exception + when :restrict_with_exception raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target when :restrict_with_error @@ -22,12 +22,13 @@ module ActiveRecord end def replace(record, save = true) - raise_on_type_mismatch(record) if record + raise_on_type_mismatch!(record) if record load_target - # If target and record are nil, or target is equal to record, - # we don't need to have transaction. - if (target || record) && target != record + return self.target if !(target || record) + if (target != record) || record.changed? + save &&= owner.persisted? + transaction_if(save) do remove_target!(options[:dependent]) if target && !target.destroyed? @@ -35,7 +36,7 @@ module ActiveRecord set_owner_attributes(record) set_inverse_instance(record) - if owner.persisted? && save && !record.save + if save && !record.save nullify_owner_attributes(record) set_owner_attributes(target) if target raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index f40368cfeb..c3ac0680ea 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -1,213 +1,273 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: - autoload :JoinPart, 'active_record/associations/join_dependency/join_part' autoload :JoinBase, 'active_record/associations/join_dependency/join_base' autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association' - attr_reader :join_parts, :reflections, :alias_tracker, :active_record + class Aliases # :nodoc: + def initialize(tables) + @tables = tables + @alias_cache = tables.each_with_object({}) { |table,h| + h[table.node] = table.columns.each_with_object({}) { |column,i| + i[column.name] = column.alias + } + } + @name_and_alias_cache = tables.each_with_object({}) { |table,h| + h[table.node] = table.columns.map { |column| + [column.name, column.alias] + } + } + end - def initialize(base, associations, joins) - @active_record = base - @table_joins = joins - @join_parts = [JoinBase.new(base)] - @associations = {} - @reflections = [] - @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 + def columns + @tables.flat_map { |t| t.column_aliases } + end - def graft(*associations) - associations.each do |association| - join_associations.detect {|a| association == a} || - build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type) + # An array of [column_name, alias] pairs for the table + def column_aliases(node) + @name_and_alias_cache[node] end - self - end - def join_associations - join_parts.last(join_parts.length - 1) - end + def column_alias(node, column) + @alias_cache[node][column] + end - def join_base - join_parts.first - end + class Table < Struct.new(:node, :columns) + def table + Arel::Nodes::TableAlias.new node.table, node.aliased_table_name + end - def columns - join_parts.collect { |join_part| - table = join_part.aliased_table - join_part.column_names_with_alias.collect{ |column_name, aliased_name| - table[column_name].as Arel.sql(aliased_name) - } - }.flatten + def column_aliases + t = table + columns.map { |column| t[column.name].as Arel.sql column.alias } + end + end + Column = Struct.new(:name, :alias) end - def instantiate(rows) - primary_key = join_base.aliased_primary_key - parents = {} - - records = rows.map { |model| - primary_id = model[primary_key] - parent = parents[primary_id] ||= join_base.instantiate(model) - construct(parent, @associations, join_associations, model) - parent - }.uniq + attr_reader :alias_tracker, :base_klass, :join_root - remove_duplicate_results!(active_record, records, @associations) - records + def self.make_tree(associations) + hash = {} + walk_tree associations, hash + hash end - def remove_duplicate_results!(base, records, associations) + def self.walk_tree(associations, hash) case associations when Symbol, String - reflection = base.reflections[associations] - remove_uniq_by_reflection(reflection, records) + hash[associations.to_sym] ||= {} when Array - associations.each do |association| - remove_duplicate_results!(base, records, association) + associations.each do |assoc| + walk_tree assoc, hash end when Hash - associations.each_key do |name| - reflection = base.reflections[name] - remove_uniq_by_reflection(reflection, records) - - parent_records = [] - records.each do |record| - if descendant = record.send(reflection.name) - if reflection.collection? - parent_records.concat descendant.target.uniq - else - parent_records << descendant - end - end - end - - remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty? + associations.each do |k,v| + cache = hash[k] ||= {} + walk_tree v, cache end + else + raise ConfigurationError, associations.inspect end end - protected + # base is the base class on which operation is taking place. + # associations is the list of associations which are joined using hash, symbol or array. + # joins is the list of all string join commnads and arel nodes. + # + # Example : + # + # class Physician < ActiveRecord::Base + # has_many :appointments + # has_many :patients, through: :appointments + # end + # + # If I execute `@physician.patients.to_a` then + # base #=> Physician + # associations #=> [] + # joins #=> [#<Arel::Nodes::InnerJoin: ...] + # + # However if I execute `Physician.joins(:appointments).to_a` then + # base #=> Physician + # associations #=> [:appointments] + # joins #=> [] + # + def initialize(base, associations, 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 + tree = self.class.make_tree associations + @join_root = JoinBase.new base, build(tree, base) + @join_root.children.each { |child| construct_tables! @join_root, child } + end - def cache_joined_association(association) - associations = [] - parent = association.parent - while parent != join_base - associations.unshift(parent.reflection.name) - parent = parent.parent - end - ref = @associations - associations.each do |key| - ref = ref[key] - end - ref[association.reflection.name] ||= {} + def reflections + join_root.drop(1).map!(&:reflection) end - def build(associations, parent = nil, join_type = Arel::InnerJoin) - parent ||= join_parts.last - case associations - when Symbol, String - reflection = parent.reflections[associations.to_s.intern] or - raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" - unless join_association = find_join_association(reflection, parent) - @reflections << reflection - join_association = build_join_association(reflection, parent) - join_association.join_type = join_type - @join_parts << join_association - cache_joined_association(join_association) - end - join_association - when Array - associations.each do |association| - build(association, parent, join_type) - end - when Hash - associations.keys.sort_by { |a| a.to_s }.each do |name| - join_association = build(name, parent, join_type) - build(associations[name], join_association, join_type) + def join_constraints(outer_joins) + joins = join_root.children.flat_map { |child| + make_inner_joins join_root, child + } + + joins.concat outer_joins.flat_map { |oj| + if join_root.match? oj.join_root + walk join_root, oj.join_root + else + oj.join_root.children.flat_map { |child| + make_outer_joins join_root, child + } end - else - raise ConfigurationError, associations.inspect - end + } end - def find_join_association(name_or_reflection, parent) - if String === name_or_reflection - name_or_reflection = name_or_reflection.to_sym - end + def aliases + Aliases.new join_root.each_with_index.map { |join_part,i| + columns = join_part.column_names.each_with_index.map { |column_name,j| + Aliases::Column.new column_name, "t#{i}_r#{j}" + } + Aliases::Table.new(join_part, columns) + } + end + + def instantiate(result_set, aliases) + primary_key = aliases.column_alias(join_root, join_root.primary_key) + type_caster = result_set.column_type primary_key + + seen = Hash.new { |h,parent_klass| + h[parent_klass] = Hash.new { |i,parent_id| + i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} } + } + } + + model_cache = Hash.new { |h,klass| h[klass] = {} } + parents = model_cache[join_root] + column_aliases = aliases.column_aliases join_root - join_associations.detect { |j| - j.reflection == name_or_reflection && j.parent == parent + result_set.each { |row_hash| + primary_id = type_caster.type_cast row_hash[primary_key] + parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases) + construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } + + parents.values end - def remove_uniq_by_reflection(reflection, records) - if reflection && reflection.collection? - records.each { |record| record.send(reflection.name).target.uniq! } - end + private + + def make_constraints(parent, child, tables, join_type) + chain = child.reflection.chain + foreign_table = parent.table + foreign_klass = parent.base_klass + child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain) end - def build_join_association(reflection, parent) - JoinAssociation.new(reflection, self, parent) + def make_outer_joins(parent, child) + tables = table_aliases_for(parent, child) + join_type = Arel::OuterJoin + joins = make_constraints parent, child, tables, join_type + + joins.concat child.children.flat_map { |c| make_outer_joins(child, c) } end - def construct(parent, associations, join_parts, row) - case associations - when Symbol, String - name = associations.to_s + def make_inner_joins(parent, child) + tables = child.tables + join_type = Arel::InnerJoin + joins = make_constraints parent, child, tables, join_type - join_part = join_parts.detect { |j| - j.reflection.name.to_s == name && - j.parent_table_name == parent.class.table_name } + joins.concat child.children.flat_map { |c| make_inner_joins(child, c) } + end - raise(ConfigurationError, "No such association") unless join_part + def table_aliases_for(parent, node) + node.reflection.chain.map { |reflection| + alias_tracker.aliased_table_for( + reflection.table_name, + table_alias_for(reflection, parent, reflection != node.reflection) + ) + } + end - join_parts.delete(join_part) - construct_association(parent, join_part, row) - when Array - associations.each do |association| - construct(parent, association, join_parts, row) - end - when Hash - associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc| - association = construct(parent, association_name, join_parts, row) - construct(association, assoc, join_parts, row) if association + def construct_tables!(parent, node) + node.tables = table_aliases_for(parent, node) + node.children.each { |child| construct_tables! node, child } + end + + def table_alias_for(reflection, parent, join) + name = "#{reflection.plural_name}_#{parent.table_name}" + name << "_join" if join + name + end + + def walk(left, right) + intersection, missing = right.children.map { |node1| + [left.children.find { |node2| node1.match? node2 }, node1] + }.partition(&:first) + + ojs = missing.flat_map { |_,n| make_outer_joins left, n } + intersection.flat_map { |l,r| walk l, r }.concat ojs + end + + def find_reflection(klass, name) + klass.reflect_on_association(name) or + raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?" + end + + def build(associations, base_klass) + associations.map do |name, right| + reflection = find_reflection base_klass, name + reflection.check_validity! + + if reflection.options[:polymorphic] + raise EagerLoadPolymorphicError.new(reflection) end - else - raise ConfigurationError, associations.inspect + + JoinAssociation.new reflection, build(right, reflection.klass) end end - def construct_association(record, join_part, row) - return if record.id.to_s != join_part.parent.record_id(row).to_s + def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + primary_id = ar_parent.id - macro = join_part.reflection.macro - if macro == :has_one - return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name) - association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? - set_target_and_inverse(join_part, association, record) - else - association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? - case macro - when :has_many, :has_and_belongs_to_many - other = record.association(join_part.reflection.name) + parent.children.each do |node| + if node.reflection.collection? + other = ar_parent.association(node.reflection.name) other.loaded! - other.target.push(association) if association - other.set_inverse_instance(association) - when :belongs_to - set_target_and_inverse(join_part, association, record) else - raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}" + if ar_parent.association_cache.key?(node.reflection.name) + model = ar_parent.association(node.reflection.name).target + construct(model, node, row, rs, seen, model_cache, aliases) + next + end + end + + key = aliases.column_alias(node, node.primary_key) + id = row[key] + next if id.nil? + + model = seen[parent.base_klass][primary_id][node.base_klass][id] + + if model + construct(model, node, row, rs, seen, model_cache, aliases) + else + model = construct_model(ar_parent, node, row, model_cache, id, aliases) + seen[parent.base_klass][primary_id][node.base_klass][id] = model + construct(model, node, row, rs, seen, model_cache, aliases) end end - association end - def set_target_and_inverse(join_part, association, record) - other = record.association(join_part.reflection.name) - other.target = association - other.set_inverse_instance(association) + def construct_model(record, node, row, model_cache, id, aliases) + model = model_cache[node][id] ||= node.instantiate(row, + aliases.column_aliases(node)) + other = record.association(node.reflection.name) + + if node.reflection.collection? + other.target.push(model) + else + other.target = model + end + + other.set_inverse_instance(model) + model end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 0d3b4dbab1..191d430636 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -1,153 +1,117 @@ +require 'active_record/associations/join_dependency/join_part' + module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinAssociation < JoinPart # :nodoc: - include JoinHelper - # The reflection of the association represented attr_reader :reflection - # The JoinDependency object which this JoinAssociation exists within. This is mainly - # relevant for generating aliases which do not conflict with other joins which are - # part of the query. - attr_reader :join_dependency - - # A JoinBase instance representing the active record we are joining onto. - # (So in Author.has_many :posts, the Author would be that base record.) - attr_reader :parent - - # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin - attr_accessor :join_type - - # These implement abstract methods from the superclass - attr_reader :aliased_prefix - - attr_reader :tables - - delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection - delegate :table, :table_name, :to => :parent, :prefix => :parent - delegate :alias_tracker, :to => :join_dependency + attr_accessor :tables - alias :alias_suffix :parent_table_name - - def initialize(reflection, join_dependency, parent = nil) - reflection.check_validity! - - if reflection.options[:polymorphic] - raise EagerLoadPolymorphicError.new(reflection) - end - - super(reflection.klass) + def initialize(reflection, children) + super(reflection.klass, children) @reflection = reflection - @join_dependency = join_dependency - @parent = parent - @join_type = Arel::InnerJoin - @aliased_prefix = "t#{ join_dependency.join_parts.size }" - @tables = construct_tables.reverse + @tables = nil end - def ==(other) - other.class == self.class && - other.reflection == reflection && - other.parent == parent + def match?(other) + return true if self == other + super && reflection == other.reflection end - def find_parent_in(other_join_dependency) - other_join_dependency.join_parts.detect do |join_part| - parent == join_part - end - end + def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) + joins = [] + tables = tables.reverse - def join_to(relation) - tables = @tables.dup - foreign_table = parent_table - foreign_klass = parent.active_record + scope_chain_iter = scope_chain.reverse_each # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse - chain.reverse.each_with_index do |reflection, i| + chain.reverse_each do |reflection| table = tables.shift + klass = reflection.klass case reflection.source_macro when :belongs_to key = reflection.association_primary_key foreign_key = reflection.foreign_key - when :has_and_belongs_to_many - # Join the join table first... - relation.from(join( - table, - table[reflection.foreign_key]. - eq(foreign_table[reflection.active_record_primary_key]) - )) - - foreign_table, table = table, tables.shift - - key = reflection.association_primary_key - foreign_key = reflection.association_foreign_key else key = reflection.foreign_key foreign_key = reflection.active_record_primary_key end - constraint = build_constraint(reflection, table, key, foreign_table, foreign_key) + constraint = build_constraint(klass, table, key, foreign_table, foreign_key) - scope_chain_items = scope_chain[i] + scope_chain_items = scope_chain_iter.next.map do |item| + if item.is_a?(Relation) + item + else + ActiveRecord::Relation.create(klass, table).instance_exec(node, &item) + end + end if reflection.type - scope_chain_items += [ - ActiveRecord::Relation.new(reflection.klass, table) + scope_chain_items << + ActiveRecord::Relation.create(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 + scope_chain_items.concat [klass.send(:build_default_scope)].compact - constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty? + rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| + left.merge right end - relation.from(join(table, constraint)) + if rel && !rel.arel.constraints.empty? + constraint = constraint.and rel.arel.constraints + end + + joins << table.create_join(table, table.create_on(constraint), join_type) # The current table in this iteration becomes the foreign table in the next - foreign_table, foreign_klass = table, reflection.klass + foreign_table, foreign_klass = table, klass end - relation + joins end - def build_constraint(reflection, table, key, foreign_table, foreign_key) + # Builds equality condition. + # + # Example: + # + # class Physician < ActiveRecord::Base + # has_many :appointments + # end + # + # If I execute `Physician.joins(:appointments).to_a` then + # reflection #=> #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...> + # table #=> #<Arel::Table @name="appointments" ...> + # key #=> physician_id + # foreign_table #=> #<Arel::Table @name="physicians" ...> + # foreign_key #=> id + # + def build_constraint(klass, table, key, foreign_table, foreign_key) constraint = table[key].eq(foreign_table[foreign_key]) - if reflection.klass.finder_needs_type_condition? + if klass.finder_needs_type_condition? constraint = table.create_and([ constraint, - reflection.klass.send(:type_condition, table) + klass.send(:type_condition, table) ]) end constraint end - def join_relation(joining_relation) - self.join_type = Arel::OuterJoin - joining_relation.joins(self) - end - def table - tables.last + tables.first end def aliased_table_name table.table_alias || table.name end - - def scope_chain - @scope_chain ||= reflection.scope_chain.reverse - end - end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb index 3920e84976..3a26c25737 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -1,22 +1,20 @@ +require 'active_record/associations/join_dependency/join_part' + module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinBase < JoinPart # :nodoc: - def ==(other) - other.class == self.class && - other.active_record == active_record - end - - def aliased_prefix - "t0" + def match?(other) + return true if self == other + super && base_klass == other.base_klass end def table - Arel::Table.new(table_name, arel_engine) + base_klass.arel_table end def aliased_table_name - active_record.table_name + base_klass.table_name end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 5604687b57..91e1c6a9d7 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -1,41 +1,43 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: - # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited + # A JoinPart represents a part of a JoinDependency. It is inherited # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which # everything else is being joined onto. A JoinAssociation represents an association which # is joining to the base. A JoinAssociation may result in more than one actual join # operations (for example a has_and_belongs_to_many JoinAssociation would result in # two; one for the join table and one for the target table). class JoinPart # :nodoc: + include Enumerable + # The Active Record class which this join part is associated 'about'; for a JoinBase # this is the actual base model, for a JoinAssociation this is the target model of the # association. - attr_reader :active_record + attr_reader :base_klass, :children - delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record + delegate :table_name, :column_names, :primary_key, :to => :base_klass - def initialize(active_record) - @active_record = active_record - @cached_record = {} + def initialize(base_klass, children) + @base_klass = base_klass @column_names_with_alias = nil + @children = children end - def aliased_table - Arel::Nodes::TableAlias.new table, aliased_table_name + def name + reflection.name end - def ==(other) - raise NotImplementedError + def match?(other) + self.class == other.class end - # An Arel::Table for the active_record - def table - raise NotImplementedError + def each(&block) + yield self + children.each { |child| child.each(&block) } end - # The prefix to be used when aliasing columns in the active_record's table - def aliased_prefix + # An Arel::Table for the active_record + def table raise NotImplementedError end @@ -44,33 +46,25 @@ module ActiveRecord raise NotImplementedError end - # The alias for the primary key of the active_record's table - def aliased_primary_key - "#{aliased_prefix}_r0" - end + def extract_record(row, column_names_with_alias) + # This code is performance critical as it is called per row. + # see: https://github.com/rails/rails/pull/12185 + hash = {} - # An array of [column_name, alias] pairs for the table - def column_names_with_alias - unless @column_names_with_alias - @column_names_with_alias = [] + index = 0 + length = column_names_with_alias.length - ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i| - @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] - end + while index < length + column_name, alias_name = column_names_with_alias[index] + hash[column_name] = row[alias_name] + index += 1 end - @column_names_with_alias - end - - def extract_record(row) - Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}] - end - def record_id(row) - row[aliased_primary_key] + hash end - def instantiate(row) - @cached_record[record_id(row)] ||= active_record.instantiate(extract_record(row)) + def instantiate(row, aliases) + base_klass.instantiate(extract_record(row, aliases)) end end end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index 5a41b40c8f..f345d16841 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -10,21 +10,12 @@ module ActiveRecord private def construct_tables - tables = [] - chain.each do |reflection| - tables << alias_tracker.aliased_table_for( + chain.map do |reflection| + alias_tracker.aliased_table_for( table_name_for(reflection), table_alias_for(reflection, reflection != self.reflection) ) - - if reflection.source_macro == :has_and_belongs_to_many - tables << alias_tracker.aliased_table_for( - (reflection.source_reflection || reflection).join_table, - table_alias_for(reflection, true) - ) - end end - tables end def table_name_for(reflection) diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 82bf426b22..2393667ac8 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -42,12 +42,9 @@ module ActiveRecord autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through' autoload :HasOne, 'active_record/associations/preloader/has_one' autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through' - autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many' autoload :BelongsTo, 'active_record/associations/preloader/belongs_to' end - attr_reader :records, :associations, :preload_scope, :model - # Eager loads the named associations for the given Active Record record(s). # # In this description, 'association name' shall refer to the name passed @@ -82,38 +79,47 @@ module ActiveRecord # [ :books, :author ] # { author: :avatar } # [ :books, { author: :avatar } ] - 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 - unless records.empty? - associations.each { |association| preload(association) } + NULL_RELATION = Struct.new(:values).new({}) + + def preload(records, associations, preload_scope = nil) + records = Array.wrap(records).compact.uniq + associations = Array.wrap(associations) + preload_scope = preload_scope || NULL_RELATION + + if records.empty? + [] + else + associations.flat_map { |association| + preloaders_on association, records, preload_scope + } end end private - def preload(association) + def preloaders_on(association, records, scope) case association when Hash - preload_hash(association) + preloaders_for_hash(association, records, scope) when Symbol - preload_one(association) + preloaders_for_one(association, records, scope) when String - preload_one(association.to_sym) + preloaders_for_one(association.to_sym, records, scope) else raise ArgumentError, "#{association.inspect} was not recognised for preload" end end - def preload_hash(association) - association.each do |parent, child| - Preloader.new(records, parent, preload_scope).run - Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run - end + def preloaders_for_hash(association, records, scope) + parent, child = association.to_a.first # hash should only be of length 1 + + loaders = preloaders_for_one parent, records, scope + + recs = loaders.flat_map(&:preloaded_records).uniq + loaders.concat Array.wrap(child).flat_map { |assoc| + preloaders_on assoc, recs, scope + } end # Not all records have the same class, so group then preload group on the reflection @@ -123,52 +129,81 @@ module ActiveRecord # Additionally, polymorphic belongs_to associations can have multiple associated # classes, depending on the polymorphic_type field. So we group by the classes as # well. - def preload_one(association) - grouped_records(association).each do |reflection, klasses| - klasses.each do |klass, records| - preloader_for(reflection).new(klass, records, reflection, preload_scope).run + def preloaders_for_one(association, records, scope) + grouped_records(association, records).flat_map do |reflection, klasses| + klasses.map do |rhs_klass, rs| + loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope) + loader.run self + loader end end end - def grouped_records(association) - Hash[ - records_by_reflection(association).map do |reflection, records| - [reflection, records.group_by { |record| association_klass(reflection, record) }] - end - ] + def grouped_records(association, records) + reflection_records = records_by_reflection(association, records) + + reflection_records.each_with_object({}) do |(reflection, r_records),h| + h[reflection] = r_records.group_by { |record| + association_klass(reflection, record) + } + end end - def records_by_reflection(association) + def records_by_reflection(association, records) records.group_by do |record| - reflection = record.class.reflections[association] + reflection = record.class.reflect_on_association(association) - unless reflection - raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \ - "perhaps you misspelled it?" - end - - reflection + reflection || raise_config_error(record, association) end end + def raise_config_error(record, association) + raise ActiveRecord::ConfigurationError, + "Association named '#{association}' was not found on #{record.class.name}; " \ + "perhaps you misspelled it?" + end + def association_klass(reflection, record) if reflection.macro == :belongs_to && reflection.options[:polymorphic] - klass = record.send(reflection.foreign_type) + klass = record.read_attribute(reflection.foreign_type.to_s) klass && klass.constantize else reflection.klass end end - def preloader_for(reflection) + class AlreadyLoaded + attr_reader :owners, :reflection + + def initialize(klass, owners, reflection, preload_scope) + @owners = owners + @reflection = reflection + end + + def run(preloader); end + + def preloaded_records + owners.flat_map { |owner| owner.read_attribute reflection.name } + end + end + + class NullPreloader + def self.new(klass, owners, reflection, preload_scope); self; end + def self.run(preloader); end + end + + def preloader_for(reflection, owners, rhs_klass) + return NullPreloader unless rhs_klass + + if owners.first.association(reflection.name).loaded? + return AlreadyLoaded + end + case reflection.macro when :has_many reflection.options[:through] ? HasManyThrough : HasMany when :has_one reflection.options[:through] ? HasOneThrough : HasOne - when :has_and_belongs_to_many - HasAndBelongsToMany when :belongs_to BelongsTo end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 82588905c6..69b65982b3 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -3,6 +3,7 @@ module ActiveRecord class Preloader class Association #:nodoc: attr_reader :owners, :reflection, :preload_scope, :model, :klass + attr_reader :preloaded_records def initialize(klass, owners, reflection, preload_scope) @klass = klass @@ -12,15 +13,14 @@ module ActiveRecord @model = owners.first && owners.first.class @scope = nil @owners_by_key = nil + @preloaded_records = [] end - def run - unless owners.first.association(reflection.name).loaded? - preload - end + def run(preloader) + preload(preloader) end - def preload + def preload(preloader) raise NotImplementedError end @@ -29,6 +29,10 @@ module ActiveRecord end def records_for(ids) + query_scope(ids) + end + + def query_scope(ids) scope.where(association_key.in(ids)) end @@ -52,12 +56,9 @@ module ActiveRecord raise NotImplementedError end - # We're converting to a string here because postgres will return the aliased association - # key in a habtm as a string (for whatever reason) def owners_by_key @owners_by_key ||= owners.group_by do |owner| - key = owner[owner_key_name] - key && key.to_s + owner[owner_key_name] end end @@ -67,38 +68,47 @@ module ActiveRecord private - def associated_records_by_owner + def associated_records_by_owner(preloader) owners_map = owners_by_key owner_keys = owners_map.keys.compact - if klass.nil? || owner_keys.empty? - records = [] - else + # Each record may have multiple owners, and vice-versa + records_by_owner = owners.each_with_object({}) do |owner,h| + h[owner] = [] + end + + if owner_keys.any? # 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(klass.connection.in_clause_length || owner_keys.size) - records = sliced.map { |slice| records_for(slice).to_a }.flatten - end - - # Each record may have multiple owners, and vice-versa - records_by_owner = Hash[owners.map { |owner| [owner, []] }] - records.each do |record| - owner_key = record[association_key_name].to_s - owners_map[owner_key].each do |owner| - records_by_owner[owner] << record + records = load_slices sliced + records.each do |record, owner_key| + owners_map[owner_key].each do |owner| + records_by_owner[owner] << record + end end end + records_by_owner end + def load_slices(slices) + @preloaded_records = slices.flat_map { |slice| + records_for(slice) + } + + @preloaded_records.map { |record| + [record, record[association_key_name]] + } + end + def reflection_scope @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped end def build_scope scope = klass.unscoped - scope.default_scoped = true values = reflection_scope.values preload_values = preload_scope.values @@ -109,11 +119,19 @@ module ActiveRecord scope.select! preload_values[:select] || values[:select] || table[Arel.star] scope.includes! preload_values[:includes] || values[:includes] + if preload_values.key? :order + scope.order! preload_values[:order] + else + if values.key? :order + scope.order! values[:order] + end + end + if options[:as] scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end - scope + klass.default_scoped.merge(scope) 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 e6cd35e7a1..5adffcd831 100644 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -9,8 +9,8 @@ module ActiveRecord super.order(preload_scope.values[:order] || reflection_scope.values[:order]) end - def preload - associated_records_by_owner.each do |owner, records| + def preload(preloader) + associated_records_by_owner(preloader).each do |owner, records| association = owner.association(reflection.name) association.loaded! association.target.concat(records) 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 deleted file mode 100644 index 8e8925f0a9..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +++ /dev/null @@ -1,60 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasAndBelongsToMany < CollectionAssociation #:nodoc: - attr_reader :join_table - - def initialize(klass, records, reflection, preload_options) - super - @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 - # access the aliased column on the join table - def records_for(ids) - scope = super - klass.connection.select_all(scope.arel, 'SQL', scope.bind_values) - end - - def owner_key_name - reflection.active_record_primary_key - end - - def association_key_name - 'ar_association_key_name' - end - - def association_key - join_table[reflection.foreign_key] - end - - private - - # Once we have used the join table column (in super), we manually instantiate the - # actual records, ensuring that we don't create more than one instances of the same - # record - def associated_records_by_owner - records = {} - super.each do |owner_key, rows| - rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) } - end - end - - def build_scope - super.joins(join).select(join_select) - end - - def join_select - association_key.as(Arel.sql(association_key_name)) - end - - def join - condition = table[reflection.association_primary_key].eq( - join_table[reflection.association_foreign_key]) - - table.create_join(join_table, table.create_on(condition)) - end - end - end - end -end 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 9a662d3f53..7b37b5942d 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -4,10 +4,14 @@ module ActiveRecord class HasManyThrough < CollectionAssociation #:nodoc: include ThroughAssociation - def associated_records_by_owner - super.each do |owner, records| - records.uniq! if reflection_scope.uniq_value + def associated_records_by_owner(preloader) + records_by_owner = super + + if reflection_scope.distinct_value + records_by_owner.each_value { |records| records.uniq! } end + + records_by_owner end end end diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb index 44e804d785..2b5cfda8ce 100644 --- a/activerecord/lib/active_record/associations/preloader/singular_association.rb +++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb @@ -5,8 +5,8 @@ module ActiveRecord private - def preload - associated_records_by_owner.each do |owner, associated_records| + def preload(preloader) + associated_records_by_owner(preloader).each do |owner, associated_records| record = associated_records.first association = owner.association(reflection.name) diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index c4b50ab306..2a8530af62 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -2,7 +2,6 @@ module ActiveRecord module Associations class Preloader module ThroughAssociation #:nodoc: - def through_reflection reflection.through_reflection end @@ -11,51 +10,84 @@ module ActiveRecord reflection.source_reflection end - def associated_records_by_owner - through_records = through_records_by_owner + def associated_records_by_owner(preloader) + preloader.preload(owners, + through_reflection.name, + through_scope) - Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run + through_records = owners.map do |owner| + association = owner.association through_reflection.name - through_records.each do |owner, records| - records.map! { |r| r.send(source_reflection.name) }.flatten! - records.compact! + [owner, Array(association.reader)] end - end - private + reset_association owners, through_reflection.name + + middle_records = through_records.map { |(_,rec)| rec }.flatten + + preloaders = preloader.preload(middle_records, + source_reflection.name, + reflection_scope) - def through_records_by_owner - Preloader.new(owners, through_reflection.name, through_scope).run + @preloaded_records = preloaders.flat_map(&:preloaded_records) + + middle_to_pl = preloaders.each_with_object({}) do |pl,h| + pl.owners.each { |middle| + h[middle] = pl + } + end + + record_offset = {} + @preloaded_records.each_with_index do |record,i| + record_offset[record] = i + end - Hash[owners.map do |owner| - through_records = Array.wrap(owner.send(through_reflection.name)) + through_records.each_with_object({}) { |(lhs,center),records_by_owner| + pl_to_middle = center.group_by { |record| middle_to_pl[record] } - # Dont cache the association - we would only be caching a subset - if (through_scope != through_reflection.klass.unscoped) || - (reflection.options[:source_type] && through_reflection.collection?) - owner.association(through_reflection.name).reset + records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles| + rhs_records = middles.flat_map { |r| + association = r.association source_reflection.name + + association.reader + }.compact + + rhs_records.sort_by { |rhs| record_offset[rhs] } end + } + end + + private + + def reset_association(owners, association_name) + should_reset = (through_scope != through_reflection.klass.unscoped) || + (reflection.options[:source_type] && through_reflection.collection?) - [owner, through_records] - end] + # Dont cache the association - we would only be caching a subset + if should_reset + owners.each { |owner| + owner.association(association_name).reset + } + end end + def through_scope - through_scope = through_reflection.klass.unscoped + scope = through_reflection.klass.unscoped if options[:source_type] - through_scope.where! reflection.foreign_type => options[:source_type] + scope.where! reflection.foreign_type => options[:source_type] else unless reflection_scope.where_values.empty? - through_scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) - through_scope.where_values = reflection_scope.values[:where] + scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) + scope.where_values = reflection_scope.values[:where] end - through_scope.references! reflection_scope.values[:references] - through_scope.order! reflection_scope.values[:order] if through_scope.eager_loading? + scope.references! reflection_scope.values[:references] + scope.order! reflection_scope.values[:order] if scope.eager_loading? end - through_scope + 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 10238555f0..02dc464536 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -42,7 +42,6 @@ module ActiveRecord scope.first.tap { |record| set_inverse_instance(record) } end - # Implemented by subclasses def replace(record) raise NotImplementedError, "Subclasses must implement a replace(record) method" end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 43520142bf..ba7d2a3782 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -13,9 +13,9 @@ module ActiveRecord # 2. To get the type conditions for any STI models in the chain def target_scope scope = super - chain[1..-1].each do |reflection| - scope = scope.merge( - reflection.klass.all.with_default_scope. + chain.drop(1).each do |reflection| + scope.merge!( + reflection.klass.all. except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index ecfa556ab4..30fa2c8ba5 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -1,8 +1,8 @@ +require 'active_model/forbidden_attributes_protection' module ActiveRecord module AttributeAssignment extend ActiveSupport::Concern - include ActiveModel::DeprecatedMassAssignmentSecurity include ActiveModel::ForbiddenAttributesProtection # Allows you to set all the attributes by passing in a hash of attributes with @@ -12,6 +12,9 @@ module ActiveRecord # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> # exception is raised. def assign_attributes(new_attributes) + if !new_attributes.respond_to?(:stringify_keys) + raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." + end return if new_attributes.blank? attributes = new_attributes.stringify_keys @@ -44,7 +47,7 @@ module ActiveRecord if respond_to?("#{k}=") raise else - raise UnknownAttributeError, "unknown attribute: #{k}" + raise UnknownAttributeError.new(self, k) end end @@ -81,7 +84,7 @@ module ActiveRecord end def extract_callstack_for_multiparameter_attributes(pairs) - attributes = { } + attributes = {} pairs.each do |(multiparameter_name, value)| attribute_name = multiparameter_name.split("(").first @@ -146,7 +149,7 @@ module ActiveRecord end else # else column is a timestamp, so if Date bits were not provided, error - validate_missing_parameters!([1,2,3]) + validate_required_parameters!([1,2,3]) # If Date bits were provided but blank, then return nil return if blank_date_parameter? @@ -172,14 +175,14 @@ module ActiveRecord def read_other(klass) max_position = extract_max_param positions = (1..max_position) - validate_missing_parameters!(positions) + validate_required_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 + # than the validate_required_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? @@ -187,7 +190,7 @@ module ActiveRecord end # If some position is not provided, it errors out a missing parameter exception. - def validate_missing_parameters!(positions) + def validate_required_parameters!(positions) if missing_parameter = positions.detect { |position| !values.key?(position) } raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})") end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index e0bfdb8f3e..3924eec872 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,4 +1,6 @@ require 'active_support/core_ext/enumerable' +require 'mutex_m' +require 'thread_safe' module ActiveRecord # = Active Record Attribute Methods @@ -7,6 +9,7 @@ module ActiveRecord include ActiveModel::AttributeMethods included do + initialize_generated_modules include Read include Write include BeforeTypeCast @@ -17,27 +20,66 @@ module ActiveRecord include Serialization end + AttrNames = Module.new { + def self.set_name_cache(name, value) + const_name = "ATTR_#{name}" + unless const_defined? const_name + const_set const_name, value.dup.freeze + end + end + } + + class AttributeMethodCache + def initialize + @module = Module.new + @method_cache = ThreadSafe::Cache.new + end + + def [](name) + @method_cache.compute_if_absent(name) do + safe_name = name.unpack('h*').first + temp_method = "__temp__#{safe_name}" + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ + @module.instance_method temp_method + end + end + + private + def method_body; raise NotImplementedError; end + end + module ClassMethods + def inherited(child_class) #:nodoc: + child_class.initialize_generated_modules + super + end + + def initialize_generated_modules # :nodoc: + @generated_attribute_methods = Module.new { extend Mutex_m } + @attribute_methods_generated = false + include @generated_attribute_methods + end + # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods # :nodoc: - # Use a mutex; we don't want two thread simaltaneously trying to define + # Use a mutex; we don't want two thread simultaneously trying to define # attribute methods. - @attribute_methods_mutex.synchronize do - return if attribute_methods_generated? + generated_attribute_methods.synchronize do + return false if @attribute_methods_generated superclass.define_attribute_methods unless self == base_class super(column_names) @attribute_methods_generated = true end - end - - def attribute_methods_generated? # :nodoc: - @attribute_methods_generated ||= false + true end def undefine_attribute_methods # :nodoc: - super if attribute_methods_generated? - @attribute_methods_generated = false + generated_attribute_methods.synchronize do + super if @attribute_methods_generated + @attribute_methods_generated = false + end end # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an @@ -56,7 +98,7 @@ module ActiveRecord # # => false def instance_method_already_implemented?(method_name) if dangerous_attribute_method?(method_name) - raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" + raise DangerousAttributeError, "#{method_name} is defined by Active Record" end if superclass == Base @@ -119,33 +161,14 @@ module ActiveRecord # If we haven't generated any methods yet, generate them, then # see if we've created the method we're looking for. def method_missing(method, *args, &block) # :nodoc: - unless self.class.attribute_methods_generated? - self.class.define_attribute_methods - - if respond_to_without_attributes?(method) - send(method, *args, &block) - else - super - end + self.class.define_attribute_methods + if respond_to_without_attributes?(method) + send(method, *args, &block) else super end end - def attribute_missing(match, *args, &block) # :nodoc: - if self.class.columns_hash[match.attr_name] - ActiveSupport::Deprecation.warn( - "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \ - "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \ - "is a column of the table. If this error has happened through normal usage of Active " \ - "Record (rather than through your own code or external libraries), please report it as " \ - "a bug." - ) - end - - super - end - # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> # which will all return +true+. It also define the attribute methods if they have @@ -163,8 +186,22 @@ module ActiveRecord # person.respond_to('age?') # => true # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) - self.class.define_attribute_methods unless self.class.attribute_methods_generated? - super + name = name.to_s + self.class.define_attribute_methods + result = super + + # If the result is false the answer is false. + return false unless result + + # If the result is true then check for the select case. + # For queries selecting a subset of columns, return false for unselected columns. + # We check defined?(@attributes) not to issue warnings if called on objects that + # have been allocated but not yet initialized. + if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name) + return has_attribute?(name) + end + + return true end # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+. @@ -208,24 +245,31 @@ module ActiveRecord # Returns an <tt>#inspect</tt>-like string for the value of the # attribute +attr_name+. String attributes are truncated upto 50 - # characters, and Date and Time attributes are returned in the - # <tt>:db</tt> format. Other attributes return the value of - # <tt>#inspect</tt> without modification. + # characters, Date and Time attributes are returned in the + # <tt>:db</tt> format, Array attributes are truncated upto 10 values. + # Other attributes return the value of <tt>#inspect</tt> without + # modification. # # person = Person.create!(name: 'David Heinemeier Hansson ' * 3) # # person.attribute_for_inspect(:name) - # # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\"" + # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\"" # # person.attribute_for_inspect(:created_at) # # => "\"2012-10-22 00:15:07\"" + # + # person.attribute_for_inspect(:tag_ids) + # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]" def attribute_for_inspect(attr_name) value = read_attribute(attr_name) if value.is_a?(String) && value.length > 50 - "#{value[0..50]}...".inspect + "#{value[0, 50]}...".inspect elsif value.is_a?(Date) || value.is_a?(Time) %("#{value.to_s(:db)}") + elsif value.is_a?(Array) && value.size > 10 + inspected = value.first(10).inspect + %(#{inspected[0...-1]}, ...]) else value.inspect end @@ -328,13 +372,14 @@ module ActiveRecord end def attribute_method?(attr_name) # :nodoc: + # We check defined? because Syck calls respond_to? before actually calling initialize. defined?(@attributes) && @attributes.include?(attr_name) end private # Returns a Hash of the Arel::Attributes and attribute values that have been - # type casted for use in an Arel insert/update method. + # typecasted for use in an Arel insert/update method. def arel_attributes_with_values(attribute_names) attrs = {} arel_table = self.class.arel_table @@ -348,7 +393,7 @@ module ActiveRecord # 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) + column_for_attribute(name) && !readonly_attribute?(name) 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 a23baeaced..f596a8b02e 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -41,8 +41,9 @@ module ActiveRecord # task.read_attribute_before_type_cast('id') # => '1' # task.read_attribute('completed_on') # => Sun, 21 Oct 2012 # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" + # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) - @attributes[attr_name] + @attributes[attr_name.to_s] end # Returns a hash of attributes before typecasting and deserialization. diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 6315dd9549..19e81abba5 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -14,24 +14,12 @@ module ActiveRecord class_attribute :partial_writes, instance_writer: false self.partial_writes = true - - def self.partial_updates=(v); self.partial_writes = v; end - def self.partial_updates?; partial_writes?; end - def self.partial_updates; partial_writes; end - - ActiveSupport::Deprecation.deprecate_methods( - singleton_class, - :partial_updates= => :partial_writes=, - :partial_updates? => :partial_writes?, - :partial_updates => :partial_writes - ) end # Attempts to +save+ the record and clears changed attributes if successful. def save(*) if status = super - @previously_changed = changes - @changed_attributes.clear + changes_applied end status end @@ -39,16 +27,14 @@ module ActiveRecord # Attempts to <tt>save!</tt> the record and clears changed attributes if successful. def save!(*) super.tap do - @previously_changed = changes - @changed_attributes.clear + changes_applied end end # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - @previously_changed.clear - @changed_attributes.clear + reset_changes end end @@ -59,11 +45,11 @@ 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) + old = changed_attributes[attr] + changed_attributes.delete(attr) unless _field_changed?(attr, old, value) else old = clone_attribute_value(:read_attribute, attr) - @changed_attributes[attr] = old if _field_changed?(attr, old, value) + changed_attributes[attr] = old if _field_changed?(attr, old, value) end # Carry on. diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 3e454b713a..931209b07b 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -90,7 +90,7 @@ module ActiveRecord base_name.foreign_key else if ActiveRecord::Base != self && table_exists? - connection.schema_cache.primary_keys[table_name] + connection.schema_cache.primary_keys(table_name) else 'id' end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 506f5d75f9..c152a246b5 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,6 +1,38 @@ +require 'active_support/core_ext/module/method_transplanting' + module ActiveRecord module AttributeMethods module Read + ReaderMethodCache = Class.new(AttributeMethodCache) { + private + # We want to generate the methods via module_eval rather than + # define_method, because define_method is slower on dispatch. + # Evaluating many similar methods may use more memory as the instruction + # sequences are duplicated and cached (in MRI). define_method may + # be slower on dispatch, but if you're careful about the closure + # created, then define_method will consume much less memory. + # + # But sometimes the database might return columns with + # characters that are not allowed in normal method names (like + # 'my_column(omg)'. So to work around this we first define with + # the __temp__ identifier, and then use alias method to rename + # it to what we want. + # + # We are also defining a constant to hold the frozen string of + # the attribute name. Using a constant means that we do not have + # to allocate an object on each call to the attribute method. + # Making it frozen means that it doesn't get duped when used to + # key the @attributes_cache in read_attribute. + def method_body(method_name, const_name) + <<-EOMETHOD + def #{method_name} + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} + read_attribute(name) { |n| missing_attribute(n, caller) } + end + EOMETHOD + end + }.new + extend ActiveSupport::Concern ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] @@ -32,30 +64,30 @@ module ActiveRecord 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). - # - # But sometimes the database might return columns with - # characters that are not allowed in normal method names (like - # 'my_column(omg)'. So to work around this we first define with - # the __temp__ identifier, and then use alias method to rename - # it to what we want. - # - # We are also defining a constant to hold the frozen string of - # the attribute name. Using a constant means that we do not have - # to allocate an object on each call to the attribute method. - # Making it frozen means that it doesn't get duped when used to - # key the @attributes_cache in read_attribute. - def define_method_attribute(name) - safe_name = name.unpack('h*').first - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name} - read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) } + if Module.methods_transplantable? + def define_method_attribute(name) + method = ReaderMethodCache[name] + generated_attribute_methods.module_eval { define_method name, method } + end + else + def define_method_attribute(name) + safe_name = name.unpack('h*').first + temp_method = "__temp__#{safe_name}" + + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def #{temp_method} + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + read_attribute(name) { |n| missing_attribute(n, caller) } + end + STR + + generated_attribute_methods.module_eval do + alias_method name, temp_method + undef_method temp_method end - alias_method #{name.inspect}, :__temp__#{safe_name} - undef_method :__temp__#{safe_name} - STR + end end private @@ -77,13 +109,14 @@ module ActiveRecord # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829. name = attr_name.to_s @attributes_cache[name] || @attributes_cache.fetch(name) { - column = @columns_hash.fetch(name) { - return @attributes.fetch(name) { - if name == 'id' && self.class.primary_key != name - read_attribute(self.class.primary_key) - end - } - } + column = @column_types_override[name] if @column_types_override + column ||= @column_types[name] + + return @attributes.fetch(name) { + if name == 'id' && self.class.primary_key != name + read_attribute(self.class.primary_key) + end + } unless column value = @attributes.fetch(name) { return block_given? ? yield(name) : nil diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 25d62fdb85..5701804168 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -11,6 +11,12 @@ module ActiveRecord end module ClassMethods + ## + # :method: serialized_attributes + # + # Returns a hash of all the attributes that have been specified for + # serialization as keys and their class restriction as values. + # If you have an attribute that needs to be saved to the database as an # object, and retrieved as the same object, then specify the name of that # attribute using this method and it will be handled automatically. The @@ -44,38 +50,40 @@ module ActiveRecord end end - def serialized_attributes - message = "Instance level serialized_attributes method is deprecated, please use class level method." - ActiveSupport::Deprecation.warn message - 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 + if value.state == :serialized + value.unserialized_value @column.type_cast value.value + else + value.unserialized_value + end end def type @column.type end + + def accessor + ActiveRecord::Store::IndifferentHashAccessor + end end class Attribute < Struct.new(:coder, :value, :state) # :nodoc: - def unserialized_value - state == :serialized ? unserialize : value + def unserialized_value(v = value) + state == :serialized ? unserialize(v) : value end def serialized_value state == :unserialized ? serialize : value end - def unserialize + def unserialize(v) self.state = :unserialized - self.value = coder.load(value) + self.value = coder.load(v) end def serialize @@ -86,10 +94,10 @@ module ActiveRecord # 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: + module Behavior # :nodoc: extend ActiveSupport::Concern - module ClassMethods + module ClassMethods # :nodoc: def initialize_attributes(attributes, options = {}) serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized super(attributes, options) 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 41b5a6e926..f168282ea3 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -51,7 +51,7 @@ module ActiveRecord 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) + (:datetime == column.type || :timestamp == column.type) end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index cd33494cc3..c853fc0917 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,6 +1,21 @@ +require 'active_support/core_ext/module/method_transplanting' + module ActiveRecord module AttributeMethods module Write + WriterMethodCache = Class.new(AttributeMethodCache) { + private + + def method_body(method_name, const_name) + <<-EOMETHOD + def #{method_name}(value) + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} + write_attribute(name, value) + end + EOMETHOD + end + }.new + extend ActiveSupport::Concern included do @@ -10,17 +25,29 @@ module ActiveRecord module ClassMethods protected - # See define_method_attribute in read.rb for an explanation of - # this code. - def define_method_attribute=(name) - safe_name = name.unpack('h*').first - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name}=(value) - write_attribute(AttrNames::ATTR_#{safe_name}, value) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR + if Module.methods_transplantable? + # See define_method_attribute in read.rb for an explanation of + # this code. + def define_method_attribute=(name) + method = WriterMethodCache[name] + generated_attribute_methods.module_eval { + define_method "#{name}=", method + } + end + else + def define_method_attribute=(name) + safe_name = name.unpack('h*').first + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__#{safe_name}=(value) + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + write_attribute(name, value) + end + alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= + undef_method :__temp__#{safe_name}= + STR + end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 55542262b0..e9622ca0c1 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -17,7 +17,8 @@ module ActiveRecord # be destroyed directly. They will however still be marked for destruction. # # Note that <tt>autosave: false</tt> is not same as not declaring <tt>:autosave</tt>. - # When the <tt>:autosave</tt> option is not present new associations are saved. + # When the <tt>:autosave</tt> option is not present then new association records are + # saved but the updated association records are not saved. # # == Validation # @@ -62,14 +63,14 @@ module ActiveRecord # Note that the model is _not_ yet removed from the database: # # id = post.author.id - # Author.find_by_id(id).nil? # => false + # Author.find_by(id: id).nil? # => false # # post.save # post.reload.author # => nil # # Now it _is_ removed from the database: # - # Author.find_by_id(id).nil? # => true + # Author.find_by(id: id).nil? # => true # # === One-to-many Example # @@ -113,105 +114,106 @@ module ActiveRecord # Note that the model is _not_ yet removed from the database: # # id = post.comments.last.id - # Comment.find_by_id(id).nil? # => false + # Comment.find_by(id: id).nil? # => false # # post.save # post.reload.comments.length # => 1 # # Now it _is_ removed from the database: # - # Comment.find_by_id(id).nil? # => true + # Comment.find_by(id: id).nil? # => true module AutosaveAssociation extend ActiveSupport::Concern module AssociationBuilderExtension #:nodoc: - def build + def self.build(model, reflection) model.send(:add_autosave_association_callbacks, reflection) - super + end + + def self.valid_options + [ :autosave ] end end included do - Associations::Builder::Association.class_eval do - self.valid_options << :autosave - include AssociationBuilderExtension - end + Associations::Builder::Association.extensions << AssociationBuilderExtension end module ClassMethods private - def define_non_cyclic_method(name, reflection, &block) - define_method(name) do |*args| - result = true; @_already_called ||= {} - # Loop prevention for validation of associations - unless @_already_called[[name, reflection.name]] - begin - @_already_called[[name, reflection.name]]=true - result = instance_eval(&block) - ensure - @_already_called[[name, reflection.name]]=false + def define_non_cyclic_method(name, &block) + define_method(name) do |*args| + result = true; @_already_called ||= {} + # Loop prevention for validation of associations + unless @_already_called[name] + begin + @_already_called[name]=true + result = instance_eval(&block) + ensure + @_already_called[name]=false + end end - end - result + result + end end - end - # Adds validation and save callbacks for the association as specified by - # the +reflection+. - # - # For performance reasons, we don't check whether to validate at runtime. - # However the validation and callback methods are lazy and those methods - # get created when they are invoked for the very first time. However, - # this can change, for instance, when using nested attributes, which is - # called _after_ the association has been defined. Since we don't want - # the callbacks to get defined multiple times, there are guards that - # check if the save or validation methods have already been defined - # before actually defining them. - def add_autosave_association_callbacks(reflection) - save_method = :"autosave_associated_records_for_#{reflection.name}" - validation_method = :"validate_associated_records_for_#{reflection.name}" - collection = reflection.collection? - - unless method_defined?(save_method) - if collection - before_save :before_save_collection_association - - define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) } - # 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 - define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) } - before_save save_method + # Adds validation and save callbacks for the association as specified by + # the +reflection+. + # + # For performance reasons, we don't check whether to validate at runtime. + # However the validation and callback methods are lazy and those methods + # get created when they are invoked for the very first time. However, + # this can change, for instance, when using nested attributes, which is + # called _after_ the association has been defined. Since we don't want + # the callbacks to get defined multiple times, there are guards that + # check if the save or validation methods have already been defined + # before actually defining them. + def add_autosave_association_callbacks(reflection) + save_method = :"autosave_associated_records_for_#{reflection.name}" + validation_method = :"validate_associated_records_for_#{reflection.name}" + collection = reflection.collection? + + unless method_defined?(save_method) + if collection + before_save :before_save_collection_association + + define_non_cyclic_method(save_method) { save_collection_association(reflection) } + # 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 + define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } + before_save save_method + end end - end - if reflection.validate? && !method_defined?(validation_method) - method = (collection ? :validate_collection_association : :validate_single_association) - define_non_cyclic_method(validation_method, reflection) { send(method, reflection) } - validate validation_method + if reflection.validate? && !method_defined?(validation_method) + method = (collection ? :validate_collection_association : :validate_single_association) + define_non_cyclic_method(validation_method) { send(method, reflection) } + validate validation_method + end end - end end # Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag. def reload(options = nil) @marked_for_destruction = false + @destroyed_by_association = nil super end @@ -231,6 +233,19 @@ module ActiveRecord @marked_for_destruction end + # Records the association that is being destroyed and destroying this + # record in the process. + def destroyed_by_association=(reflection) + @destroyed_by_association = reflection + end + + # Returns the association for the parent being destroyed. + # + # Used to avoid updating the counter cache unnecessarily. + def destroyed_by_association + @destroyed_by_association + end + # Returns whether or not this record has been changed in any way (including whether # any of its nested autosave associations are likewise changed) def changed_for_autosave? @@ -239,173 +254,179 @@ module ActiveRecord private - # Returns the record for an association collection that should be validated - # or saved. If +autosave+ is +false+ only new records will be returned, - # unless the parent is/was a new record itself. - def associated_records_to_validate_or_save(association, new_record, autosave) - if new_record - association && association.target - elsif autosave - association.target.find_all { |record| record.changed_for_autosave? } - else - association.target.find_all { |record| record.new_record? } + # Returns the record for an association collection that should be validated + # or saved. If +autosave+ is +false+ only new records will be returned, + # unless the parent is/was a new record itself. + def associated_records_to_validate_or_save(association, new_record, autosave) + if new_record + association && association.target + elsif autosave + association.target.find_all { |record| record.changed_for_autosave? } + else + association.target.find_all { |record| record.new_record? } + end end - end - # go through nested autosave associations that are loaded in memory (without loading - # any new ones), and return true if is changed for autosave - def nested_records_changed_for_autosave? - self.class.reflect_on_all_autosave_associations.any? do |reflection| - association = association_instance_get(reflection.name) - association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + # go through nested autosave associations that are loaded in memory (without loading + # any new ones), and return true if is changed for autosave + def nested_records_changed_for_autosave? + self.class.reflect_on_all_autosave_associations.any? do |reflection| + association = association_instance_get(reflection.name) + association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + end end - end - # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is - # turned on for the association. - def validate_single_association(reflection) - association = association_instance_get(reflection.name) - record = association && association.reader - association_valid?(reflection, record) if record - end + # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is + # turned on for the association. + def validate_single_association(reflection) + association = association_instance_get(reflection.name) + record = association && association.reader + association_valid?(reflection, record) if record + end - # Validate the associated records if <tt>:validate</tt> or - # <tt>:autosave</tt> is turned on for the association specified by - # +reflection+. - def validate_collection_association(reflection) - if association = association_instance_get(reflection.name) - if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) - records.each { |record| association_valid?(reflection, record) } + # Validate the associated records if <tt>:validate</tt> or + # <tt>:autosave</tt> is turned on for the association specified by + # +reflection+. + def validate_collection_association(reflection) + if association = association_instance_get(reflection.name) + if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) + records.each { |record| association_valid?(reflection, record) } + end end end - end - # Returns whether or not the association is valid and applies any errors to - # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> - # enabled records if they're marked_for_destruction? or destroyed. - def association_valid?(reflection, record) - return true if record.destroyed? || record.marked_for_destruction? - - unless valid = record.valid?(validation_context) - if reflection.options[:autosave] - record.errors.each do |attribute, message| - attribute = "#{reflection.name}.#{attribute}" - errors[attribute] << message - errors[attribute].uniq! + # Returns whether or not the association is valid and applies any errors to + # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> + # enabled records if they're marked_for_destruction? or destroyed. + def association_valid?(reflection, record) + return true if record.destroyed? || record.marked_for_destruction? + + unless valid = record.valid? + if reflection.options[:autosave] + record.errors.each do |attribute, message| + attribute = "#{reflection.name}.#{attribute}" + errors[attribute] << message + errors[attribute].uniq! + end + else + errors.add(reflection.name) end - else - errors.add(reflection.name) end + valid end - valid - end - # Is used as a before_save callback to check while saving a collection - # association whether or not the parent was a new record before saving. - def before_save_collection_association - @new_record_before_save = new_record? - true - end + # Is used as a before_save callback to check while saving a collection + # association whether or not the parent was a new record before saving. + def before_save_collection_association + @new_record_before_save = new_record? + true + end - # Saves any new associated records, or all loaded autosave associations if - # <tt>:autosave</tt> is enabled on the association. - # - # In addition, it destroys all children that were marked for destruction - # with mark_for_destruction. - # - # This all happens inside a transaction, _if_ the Transactions module is included into - # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. - def save_collection_association(reflection) - if association = association_instance_get(reflection.name) - autosave = reflection.options[:autosave] - - if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) - records_to_destroy = [] - records.each do |record| - next if record.destroyed? - - saved = true - - if autosave && record.marked_for_destruction? - records_to_destroy << record - elsif autosave != false && (@new_record_before_save || record.new_record?) - if autosave - saved = association.insert_record(record, false) - else - association.insert_record(record) unless reflection.nested? - end - elsif autosave - saved = record.save(:validate => false) + # Saves any new associated records, or all loaded autosave associations if + # <tt>:autosave</tt> is enabled on the association. + # + # In addition, it destroys all children that were marked for destruction + # with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_collection_association(reflection) + if association = association_instance_get(reflection.name) + autosave = reflection.options[:autosave] + + if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) + + if autosave + records_to_destroy = records.select(&:marked_for_destruction?) + records_to_destroy.each { |record| association.destroy(record) } + records -= records_to_destroy end - raise ActiveRecord::Rollback unless saved - end + records.each do |record| + next if record.destroyed? + + saved = true + + if autosave != false && (@new_record_before_save || record.new_record?) + if autosave + saved = association.insert_record(record, false) + else + association.insert_record(record) unless reflection.nested? + end + elsif autosave + saved = record.save(:validate => false) + end - records_to_destroy.each do |record| - association.destroy(record) + raise ActiveRecord::Rollback unless saved + end end - end - # reconstruct the scope now that we know the owner's id - association.reset_scope if association.respond_to?(:reset_scope) + # reconstruct the scope now that we know the owner's id + association.reset_scope if association.respond_to?(:reset_scope) + end end - end - # Saves the associated record if it's new or <tt>:autosave</tt> is enabled - # on the association. - # - # In addition, it will destroy the association if it was marked for - # destruction with mark_for_destruction. - # - # This all happens inside a transaction, _if_ the Transactions module is included into - # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. - def save_has_one_association(reflection) - association = association_instance_get(reflection.name) - record = association && association.load_target - if record && !record.destroyed? - autosave = reflection.options[:autosave] - - if autosave && record.marked_for_destruction? - record.destroy - else - key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave) - unless reflection.through_reflection - record[reflection.foreign_key] = key - end + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled + # on the association. + # + # In addition, it will destroy the association if it was marked for + # destruction with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_has_one_association(reflection) + association = association_instance_get(reflection.name) + record = association && association.load_target + if record && !record.destroyed? + autosave = reflection.options[:autosave] - saved = record.save(:validate => !autosave) - raise ActiveRecord::Rollback if !saved && autosave - saved + if autosave && record.marked_for_destruction? + record.destroy + else + key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id + if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key)) + + unless reflection.through_reflection + record[reflection.foreign_key] = key + end + + saved = record.save(:validate => !autosave) + raise ActiveRecord::Rollback if !saved && autosave + saved + end end end end - end - # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. - # - # In addition, it will destroy the association if it was marked for destruction. - def save_belongs_to_association(reflection) - association = association_instance_get(reflection.name) - record = association && association.load_target - if record && !record.destroyed? - autosave = reflection.options[:autosave] - - if autosave && record.marked_for_destruction? - self[reflection.foreign_key] = nil - record.destroy - elsif autosave != false - saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) - - if association.updated? - association_id = record.send(reflection.options[:primary_key] || :id) - self[reflection.foreign_key] = association_id - association.loaded! - end + # If the record is new or it has changed, returns true. + def record_changed?(reflection, record, key) + record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key) + end + + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. + # + # In addition, it will destroy the association if it was marked for destruction. + def save_belongs_to_association(reflection) + association = association_instance_get(reflection.name) + record = association && association.load_target + if record && !record.destroyed? + autosave = reflection.options[:autosave] + + if autosave && record.marked_for_destruction? + self[reflection.foreign_key] = nil + record.destroy + elsif autosave != false + saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) + + if association.updated? + association_id = record.send(reflection.options[:primary_key] || :id) + self[reflection.foreign_key] = association_id + association.loaded! + end - saved if autosave + saved if autosave + end end end - end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index e262401da6..04e3dd49e7 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -18,6 +18,7 @@ require 'arel' require 'active_record/errors' require 'active_record/log_subscriber' require 'active_record/explain_subscriber' +require 'active_record/relation/delegation' module ActiveRecord #:nodoc: # = Active Record @@ -160,10 +161,10 @@ module ActiveRecord #:nodoc: # # == Dynamic attribute-based finders # - # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects + # Dynamic attribute-based finders are a mildly deprecated way of getting (and/or creating) objects # by simple queries without turning to SQL. They work by appending the name of an attribute # to <tt>find_by_</tt> like <tt>Person.find_by_user_name</tt>. - # Instead of writing <tt>Person.where(user_name: user_name).first</tt>, you just do + # Instead of writing <tt>Person.find_by(user_name: user_name)</tt>, you can use # <tt>Person.find_by_user_name(user_name)</tt>. # # It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an @@ -172,7 +173,7 @@ module ActiveRecord #:nodoc: # # It's also possible to use multiple attributes in the same find by separating them with "_and_". # - # Person.where(user_name: user_name, password: password).first + # Person.find_by(user_name: user_name, password: password) # Person.find_by_user_name_and_password(user_name, password) # with dynamic finder # # It's even possible to call these dynamic finder methods on relations and named scopes. @@ -290,6 +291,7 @@ module ActiveRecord #:nodoc: extend Translation extend DynamicMatchers extend Explain + extend Delegation::DelegateCache include Persistence include ReadonlyAttributes diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 22226b2f4f..128a9377c1 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -23,11 +23,14 @@ module ActiveRecord # Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and # <tt>after_rollback</tt>. # + # Additionally, an <tt>after_touch</tt> callback is triggered whenever an + # object is touched. + # # Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that # is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects # are instantiated as well. # - # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the + # There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the # Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, # except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback. # @@ -83,7 +86,7 @@ module ActiveRecord # # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. # So, use the callback macros when you want to ensure that a certain callback is called for the entire - # hierarchy, and use the regular overwriteable methods when you want to leave it up to each descendant + # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant # to decide whether they want to call +super+ and trigger the inherited callbacks. # # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index f6cdc67b4d..d3d7396c91 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -3,7 +3,6 @@ require 'yaml' module ActiveRecord module Coders # :nodoc: class YAMLColumn # :nodoc: - RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] attr_accessor :object_class @@ -24,19 +23,15 @@ module ActiveRecord def load(yaml) return object_class.new if object_class != Object && yaml.nil? return yaml unless yaml.is_a?(String) && yaml =~ /^---/ - begin - obj = YAML.load(yaml) - - unless obj.is_a?(object_class) || obj.nil? - raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" - end - obj ||= object_class.new if object_class != Object - - obj - rescue *RESCUE_ERRORS - yaml + obj = YAML.load(yaml) + + unless obj.is_a?(object_class) || obj.nil? + raise SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" end + obj ||= object_class.new if object_class != Object + + obj end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 1754e424b8..cfdcae7f63 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -253,14 +253,6 @@ module ActiveRecord @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 # #checkout to obtain one if necessary. # @@ -340,11 +332,6 @@ module ActiveRecord end end - def clear_stale_cached_connections! # :nodoc: - reap - end - 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. # @@ -406,7 +393,9 @@ module ActiveRecord synchronize do stale = Time.now - @dead_connection_timeout connections.dup.each do |conn| - remove conn if conn.in_use? && stale > conn.last_use && !conn.active? + if conn.in_use? && stale > conn.last_use && !conn.active? + remove conn + end end end end @@ -635,7 +624,7 @@ module ActiveRecord end response - rescue + rescue Exception ActiveRecord::Base.clear_active_connections! unless testing raise end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 2859fb31e8..c0a2111571 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -21,7 +21,7 @@ module ActiveRecord # limit is enforced by rails and Is less than or equal to # <tt>index_name_length</tt>. The gap between # <tt>index_name_length</tt> is to allow internal rails - # opreations to use prefixes in temporary opreations. + # operations to use prefixes in temporary operations. def allowed_index_name_length index_name_length 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 c64b542286..e1f29ea03a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -18,8 +18,7 @@ module ActiveRecord end end - # Returns an array of record hashes with the column names as keys and - # column values as values. + # Returns an ActiveRecord::Result instance. def select_all(arel, name = nil, binds = []) select(to_sql(arel, binds), name, binds) end @@ -27,8 +26,7 @@ module ActiveRecord # Returns a record hash with the column names as keys and column values # as values. def select_one(arel, name = nil, binds = []) - result = select_all(arel, name, binds) - result.first if result + select_all(arel, name, binds).first end # Returns a single value from a record @@ -41,8 +39,8 @@ 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.map { |v| v[0] } + select_rows(to_sql(arel, []), name) + .map { |v| v[0] } end # Returns an array of arrays containing the field values. @@ -355,8 +353,7 @@ module ActiveRecord subselect end - # Returns an array of record hashes with the column names as keys and - # column values as values. + # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) end undef_method :select @@ -377,14 +374,14 @@ module ActiveRecord update_sql(sql, name) end - def sql_for_insert(sql, pk, id_value, sequence_name, binds) - [sql, binds] - end + def sql_for_insert(sql, pk, id_value, sequence_name, binds) + [sql, binds] + end - def last_inserted_id(result) - row = result.rows.first - row && row.first - end + def last_inserted_id(result) + row = result.rows.first + row && row.first + end end 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 41e07fbda9..8399232d73 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -9,10 +9,10 @@ module ActiveRecord def dirties_query_cache(base, *method_names) method_names.each do |method_name| base.class_eval <<-end_code, __FILE__, __LINE__ + 1 - def #{method_name}(*) # def update_with_query_dirty(*) - clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled - super # super - end # end + def #{method_name}(*) + clear_query_cache if @query_cache_enabled + super + end end_code end end @@ -20,6 +20,12 @@ module ActiveRecord attr_reader :query_cache, :query_cache_enabled + def initialize(*) + super + @query_cache = Hash.new { |h,sql| h[sql] = {} } + @query_cache_enabled = false + end + # Enable the query cache within the block. def cache old, @query_cache_enabled = @query_cache_enabled, true @@ -75,14 +81,7 @@ module ActiveRecord else @query_cache[sql][binds] = yield end - - # FIXME: we should guarantee that all cached items are Result - # objects. Then we can avoid this conditional - if ActiveRecord::Result === result - result.dup - else - result.collect { |row| row.dup } - end + result.dup end # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index d18b9c991f..552a22d28a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -15,7 +15,6 @@ module ActiveRecord return "'#{quote_string(value)}'" unless column case column.type - when :binary then "'#{quote_string(column.string_to_binary(value))}'" when :integer then value.to_i.to_s when :float then value.to_f.to_s else @@ -52,7 +51,6 @@ module ActiveRecord return value unless column case column.type - when :binary then value when :integer then value.to_i when :float then value.to_f else diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb new file mode 100644 index 0000000000..25c17ce971 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module ConnectionAdapters + module Savepoints #:nodoc: + def supports_savepoints? + true + end + + def create_savepoint(name = current_savepoint_name) + execute("SAVEPOINT #{name}") + end + + def rollback_to_savepoint(name = current_savepoint_name) + execute("ROLLBACK TO SAVEPOINT #{name}") + end + + def release_savepoint(name = current_savepoint_name) + execute("RELEASE SAVEPOINT #{name}") + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 42206de8fc..063b19871a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -8,37 +8,21 @@ module ActiveRecord # 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: + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc: end # Abstract representation of a column definition. Instances of this type # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key) #:nodoc: - def string_to_binary(value) - value + def primary_key? + primary_key || type.to_sym == :primary_key end + end - def sql_type - base.type_to_sql(type.to_sym, limit, precision, scale) - end - - def to_sql - column_sql = "#{base.quote_column_name(name)} #{sql_type}" - column_options = {} - column_options[:null] = null unless null.nil? - column_options[:default] = default unless default.nil? - add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key - column_sql - end - - private - - def add_column_options!(sql, options) - base.add_column_options!(sql, options.merge(:column => self)) - end + class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc: end # Represents the schema of an SQL table in an abstract way. This class @@ -64,28 +48,24 @@ module ActiveRecord class TableDefinition # An array of ColumnDefinition objects, representing the column changes # that have been defined. - attr_accessor :columns, :indexes + attr_accessor :indexes + attr_reader :name, :temporary, :options - def initialize(base) - @columns = [] + def initialize(types, name, temporary, options) @columns_hash = {} @indexes = {} - @base = base + @native = types + @temporary = temporary + @options = options + @name = name end - def xml(*args) - raise NotImplementedError unless %w{ - sqlite mysql mysql2 - }.include? @base.adapter_name.downcase - - options = args.extract_options! - column(args[0], :text, options) - end + def columns; @columns_hash.values; end # Appends a primary key definition to the table definition. # Can be called multiple times, but this is probably not a good idea. - def primary_key(name) - column(name, :primary_key) + def primary_key(name, type = :primary_key, options = {}) + column(name, type, options.merge(:primary_key => true)) end # Returns a ColumnDefinition for the column with name +name+. @@ -238,20 +218,14 @@ module ActiveRecord raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." end - column = self[name] || new_column_definition(@base, name, type) - - limit = options.fetch(:limit) do - native[type][:limit] if native[type].is_a?(Hash) - end - - column.limit = limit - column.precision = options[:precision] - column.scale = options[:scale] - column.default = options[:default] - column.null = options[:null] + @columns_hash[name] = new_column_definition(name, type, options) self end + def remove_column(name) + @columns_hash.delete name.to_s + end + [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| define_method column_type do |*args| options = args.extract_options! @@ -283,33 +257,58 @@ module ActiveRecord args.each do |col| column("#{col}_id", :integer, options) 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 + index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options end end alias :belongs_to :references - # Returns a String whose contents are the column definitions - # concatenated together. This string can then be prepended and appended to - # to generate the final SQL to create the table. - def to_sql - @columns.map { |c| c.to_sql } * ', ' + def new_column_definition(name, type, options) # :nodoc: + column = create_column_definition name, type + limit = options.fetch(:limit) do + native[type][:limit] if native[type].is_a?(Hash) + end + + column.limit = limit + column.array = options[:array] if column.respond_to?(:array) + column.precision = options[:precision] + column.scale = options[:scale] + column.default = options[:default] + column.null = options[:null] + column.first = options[:first] + column.after = options[:after] + column.primary_key = type == :primary_key || options[:primary_key] + column end private - def new_column_definition(base, name, type) - definition = ColumnDefinition.new base, name, type - @columns << definition - @columns_hash[name] = definition - definition + def create_column_definition(name, type) + ColumnDefinition.new name, type end def primary_key_column_name - primary_key_column = columns.detect { |c| c.type == :primary_key } + primary_key_column = columns.detect { |c| c.primary_key? } primary_key_column && primary_key_column.name end def native - @base.native_database_types + @native + end + end + + class AlterTable # :nodoc: + attr_reader :adds + + def initialize(td) + @td = td + @adds = [] + end + + def name; @td.name; end + + def add_column(name, type, options) + name = name.to_s + type = type.to_sym + @adds << @td.new_column_definition(name, type, options) end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index f587bf8140..cdf0cbe218 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -5,7 +5,7 @@ module ActiveRecord # The goal of this module is to move Adapter specific column # definitions to the Adapter instead of having it in the schema # dumper itself. This code represents the normal case. - # We can then redefine how certain data types may be handled in the schema dumper on the + # We can then redefine how certain data types may be handled in the schema dumper on the # Adapter level by over-writing this code inside the database specific adapters module ColumnDumper def column_spec(column, types) 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 e2deb6bfcd..4b425494d0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -5,7 +5,7 @@ module ActiveRecord module SchemaStatements include ActiveRecord::Migration::JoinTable - # Returns a Hash of mappings from the abstract data types to the native + # Returns a hash of mappings from the abstract data types to the native # database types. See TableDefinition#column for details on the recognized # abstract data types. def native_database_types @@ -20,6 +20,7 @@ module ActiveRecord # Checks to see if the table +table_name+ exists on the database. # # table_exists?(:developers) + # def table_exists?(table_name) tables.include?(table_name.to_s) end @@ -29,17 +30,18 @@ module ActiveRecord # Checks to see if an index exists on a table for a given index definition. # - # # Check an index exists - # index_exists?(:suppliers, :company_id) + # # Check an index exists + # index_exists?(:suppliers, :company_id) + # + # # Check an index on multiple columns exists + # index_exists?(:suppliers, [:company_id, :company_type]) # - # # Check an index on multiple columns exists - # index_exists?(:suppliers, [:company_id, :company_type]) + # # Check a unique index exists + # index_exists?(:suppliers, :company_id, unique: true) # - # # Check a unique index exists - # index_exists?(:suppliers, :company_id, unique: true) + # # Check an index with a custom name exists + # index_exists?(:suppliers, :company_id, name: "idx_company_id" # - # # Check an index with a custom name exists - # index_exists?(:suppliers, :company_id, name: "idx_company_id" def index_exists?(table_name, column_name, options = {}) column_names = Array(column_name) index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names) @@ -56,17 +58,18 @@ module ActiveRecord # Checks to see if a column exists in a given table. # - # # Check a column exists - # column_exists?(:suppliers, :name) + # # Check a column exists + # column_exists?(:suppliers, :name) + # + # # Check a column exists of a particular type + # column_exists?(:suppliers, :name, :string) # - # # Check a column exists of a particular type - # 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, default: 'default') + # column_exists?(:suppliers, :name, :string, null: false) + # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) # - # # Check a column exists with a specific definition - # 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) && @@ -84,27 +87,30 @@ module ActiveRecord # form or the regular form, like this: # # === Block form - # # create_table() passes a TableDefinition object to the block. - # # This form will not only create the table, but also columns for the - # # table. # - # create_table(:suppliers) do |t| - # t.column :name, :string, limit: 60 - # # Other fields here - # end + # # create_table() passes a TableDefinition object to the block. + # # This form will not only create the table, but also columns for the + # # table. + # + # create_table(:suppliers) do |t| + # t.column :name, :string, limit: 60 + # # Other fields here + # end # # === Block form, with shorthand - # # You can also use the column types as method calls, rather than calling the column method. - # create_table(:suppliers) do |t| - # t.string :name, limit: 60 - # # Other fields here - # end + # + # # You can also use the column types as method calls, rather than calling the column method. + # create_table(:suppliers) do |t| + # t.string :name, limit: 60 + # # Other fields here + # end # # === Regular form - # # Creates a table called 'suppliers' with no columns. - # create_table(:suppliers) - # # Add a column to 'suppliers'. - # add_column(:suppliers, :name, :string, {limit: 60}) + # + # # Creates a table called 'suppliers' with no columns. + # create_table(:suppliers) + # # Add a column to 'suppliers'. + # add_column(:suppliers, :name, :string, {limit: 60}) # # The +options+ hash can include the following keys: # [<tt>:id</tt>] @@ -127,37 +133,53 @@ module ActiveRecord # Defaults to false. # # ====== Add a backend specific option to the generated SQL (MySQL) - # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # + # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # # generates: - # CREATE TABLE suppliers ( - # id int(11) DEFAULT NULL auto_increment PRIMARY KEY - # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # + # CREATE TABLE suppliers ( + # id int(11) DEFAULT NULL auto_increment PRIMARY KEY + # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # # ====== Rename the primary key column - # create_table(:objects, primary_key: 'guid') do |t| - # t.column :name, :string, limit: 80 - # end + # + # create_table(:objects, primary_key: 'guid') do |t| + # t.column :name, :string, limit: 80 + # end + # # generates: - # CREATE TABLE objects ( - # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, - # name varchar(80) - # ) + # + # CREATE TABLE objects ( + # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, + # name varchar(80) + # ) # # ====== Do not add a primary key column - # create_table(:categories_suppliers, id: false) do |t| - # t.column :category_id, :integer - # t.column :supplier_id, :integer - # end + # + # create_table(:categories_suppliers, id: false) do |t| + # t.column :category_id, :integer + # t.column :supplier_id, :integer + # end + # # generates: - # CREATE TABLE categories_suppliers ( - # category_id int, - # supplier_id int - # ) + # + # CREATE TABLE categories_suppliers ( + # category_id int, + # supplier_id int + # ) # # See also TableDefinition#column for details on how to create columns. def create_table(table_name, options = {}) - td = create_table_definition - td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false + td = create_table_definition table_name, options[:temporary], options[:options] + + unless options[:id] == false + pk = options.fetch(:primary_key) { + Base.get_primary_key table_name.to_s.singularize + } + + td.primary_key pk, options.fetch(:id, :primary_key), options + end yield td if block_given? @@ -165,19 +187,15 @@ module ActiveRecord drop_table(table_name, options) end - create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " - create_sql << "#{quote_table_name(table_name)} (" - create_sql << td.to_sql - create_sql << ") #{options[:options]}" - execute create_sql + execute schema_creation.accept td 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 a String or a Symbol. # - # # Creates a table called 'assemblies_parts' with no id. - # create_join_table(:assemblies, :parts) + # # Creates a table called 'assemblies_parts' with no id. + # create_join_table(:assemblies, :parts) # # You can pass a +options+ hash can include the following keys: # [<tt>:table_name</tt>] @@ -196,17 +214,21 @@ module ActiveRecord # its block form to do so yourself: # # create_join_table :products, :categories do |t| - # t.index :products - # t.index :categories + # t.index :product_id + # t.index :category_id # end # # ====== Add a backend specific option to the generated SQL (MySQL) - # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # + # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # # generates: - # CREATE TABLE assemblies_parts ( - # assembly_id int NOT NULL, - # part_id int NOT NULL, - # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # + # CREATE TABLE assemblies_parts ( + # assembly_id int NOT NULL, + # part_id int NOT NULL, + # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # def create_join_table(table_1, table_2, options = {}) join_table_name = find_join_table_name(table_1, table_2, options) @@ -223,7 +245,7 @@ module ActiveRecord end # Drops the join table specified by the given arguments. - # See create_join_table for details. + # See +create_join_table+ for details. # # Although this command ignores the block if one is given, it can be helpful # to provide one in a migration's +change+ method so it can be reverted. @@ -235,66 +257,74 @@ module ActiveRecord # A block for changing columns in +table+. # - # # change_table() yields a Table instance - # change_table(:suppliers) do |t| - # t.column :name, :string, limit: 60 - # # Other column alterations here - # end + # # change_table() yields a Table instance + # change_table(:suppliers) do |t| + # t.column :name, :string, limit: 60 + # # Other column alterations here + # end # # The +options+ hash can include the following keys: # [<tt>:bulk</tt>] # Set this to true to make this a bulk alter query, such as - # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... + # + # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... # # Defaults to false. # # ====== Add a column - # change_table(:suppliers) do |t| - # t.column :name, :string, limit: 60 - # end + # + # change_table(:suppliers) do |t| + # t.column :name, :string, limit: 60 + # end # # ====== Add 2 integer columns - # change_table(:suppliers) do |t| - # t.integer :width, :height, null: false, default: 0 - # end + # + # change_table(:suppliers) do |t| + # t.integer :width, :height, null: false, default: 0 + # end # # ====== Add created_at/updated_at columns - # change_table(:suppliers) do |t| - # t.timestamps - # end + # + # change_table(:suppliers) do |t| + # t.timestamps + # end # # ====== Add a foreign key column - # change_table(:suppliers) do |t| - # t.references :company - # end # - # Creates a <tt>company_id(integer)</tt> column + # change_table(:suppliers) do |t| + # t.references :company + # end + # + # Creates a <tt>company_id(integer)</tt> column. # # ====== Add a polymorphic foreign key column + # # change_table(:suppliers) do |t| # t.belongs_to :company, polymorphic: true # end # - # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns + # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns. # # ====== Remove a column + # # change_table(:suppliers) do |t| # t.remove :company # end # # ====== Remove several columns + # # change_table(:suppliers) do |t| # t.remove :company_id # t.remove :width, :height # end # # ====== Remove an index + # # change_table(:suppliers) do |t| # t.remove_index :company_id # end # - # See also Table for details on - # all of the various column transformation + # See also Table for details on all of the various column transformation. def change_table(table_name, options = {}) if supports_bulk_alter? && options[:bulk] recorder = ActiveRecord::Migration::CommandRecorder.new(self) @@ -307,7 +337,8 @@ module ActiveRecord # Renames a table. # - # rename_table('octopuses', 'octopi') + # rename_table('octopuses', 'octopi') + # def rename_table(table_name, new_name) raise NotImplementedError, "rename_table is not implemented" end @@ -324,14 +355,15 @@ module ActiveRecord # 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 = {}) - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{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) + at = create_alter_table table_name + at.add_column(column_name, type, options) + execute schema_creation.accept at end # Removes the given columns from the table definition. # - # remove_columns(:suppliers, :qualification, :experience) + # remove_columns(:suppliers, :qualification, :experience) + # def remove_columns(table_name, *column_names) raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.empty? column_names.each do |column_name| @@ -341,7 +373,7 @@ module ActiveRecord # Removes the column from the table definition. # - # remove_column(:suppliers, :qualification) + # remove_column(:suppliers, :qualification) # # The +type+ and +options+ parameters will be ignored if present. It can be helpful # to provide these in a migration's +change+ method so it can be reverted. @@ -353,24 +385,50 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. # - # change_column(:suppliers, :name, :string, limit: 80) - # change_column(:accounts, :description, :text) + # change_column(:suppliers, :name, :string, limit: 80) + # change_column(:accounts, :description, :text) + # def change_column(table_name, column_name, type, options = {}) raise NotImplementedError, "change_column is not implemented" end - # Sets a new default value for a column. + # Sets a new default value for a column: + # + # change_column_default(:suppliers, :qualification, 'new') + # change_column_default(:accounts, :authorized, 1) + # + # Setting the default to +nil+ effectively drops the default: + # + # change_column_default(:users, :email, nil) # - # change_column_default(:suppliers, :qualification, 'new') - # change_column_default(:accounts, :authorized, 1) - # change_column_default(:users, :email, nil) def change_column_default(table_name, column_name, default) raise NotImplementedError, "change_column_default is not implemented" end + # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag + # indicates whether the value can be +NULL+. For example + # + # change_column_null(:users, :nickname, false) + # + # says nicknames cannot be +NULL+ (adds the constraint), whereas + # + # change_column_null(:users, :nickname, true) + # + # allows them to be +NULL+ (drops the constraint). + # + # The method accepts an optional fourth argument to replace existing + # +NULL+s with some other value. Use that one when enabling the + # constraint if needed, since otherwise those rows would not be valid. + # + # Please note the fourth argument does not set a column's default. + def change_column_null(table_name, column_name, null, default = nil) + raise NotImplementedError, "change_column_null is not implemented" + end + # Renames a column. # - # rename_column(:suppliers, :description, :name) + # rename_column(:suppliers, :description, :name) + # def rename_column(table_name, column_name, new_column_name) raise NotImplementedError, "rename_column is not implemented" end @@ -382,60 +440,106 @@ module ActiveRecord # you pass <tt>:name</tt> as an option. # # ====== Creating a simple index - # add_index(:suppliers, :name) - # generates - # CREATE INDEX suppliers_name_index ON suppliers(name) + # + # add_index(:suppliers, :name) + # + # generates: + # + # CREATE INDEX suppliers_name_index ON suppliers(name) # # ====== Creating a unique index - # add_index(:accounts, [:branch_id, :party_id], unique: true) - # generates - # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id) + # + # add_index(:accounts, [:branch_id, :party_id], unique: true) + # + # generates: + # + # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id) # # ====== Creating a named index - # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party') - # generates + # + # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party') + # + # generates: + # # CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id) # # ====== Creating an index with specific key length - # add_index(:accounts, :name, name: 'by_name', length: 10) - # generates - # CREATE INDEX by_name ON accounts(name(10)) # - # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) - # generates - # CREATE INDEX by_name_surname ON accounts(name(10), surname(15)) + # add_index(:accounts, :name, name: 'by_name', length: 10) + # + # generates: + # + # CREATE INDEX by_name ON accounts(name(10)) # - # Note: SQLite doesn't support index length + # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) + # + # generates: + # + # CREATE INDEX by_name_surname ON accounts(name(10), surname(15)) + # + # 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, 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) + # 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 + # 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 + # + # ====== Creating an index with a specific method + # + # add_index(:developers, :name, using: 'btree') + # + # generates: + # + # CREATE INDEX index_developers_on_name ON developers USING btree (name) -- PostgreSQL + # CREATE INDEX index_developers_on_name USING btree ON developers (name) -- MySQL + # + # Note: only supported by PostgreSQL and MySQL # + # ====== Creating an index with a specific type + # + # add_index(:developers, :name, type: :fulltext) + # + # generates: + # + # CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL + # + # Note: only supported by MySQL. Supported: <tt>:fulltext</tt> and <tt>:spatial</tt> on MyISAM tables. def add_index(table_name, column_name, options = {}) 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. + # Removes the given index from the table. + # + # Removes the +index_accounts_on_column+ in the +accounts+ table. # - # Remove the index_accounts_on_column in the accounts table. # remove_index :accounts, :column - # Remove the index named index_accounts_on_branch_id in the accounts table. + # + # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table. + # # remove_index :accounts, column: :branch_id - # Remove the index named index_accounts_on_branch_id_and_party_id in the accounts table. + # + # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table. + # # remove_index :accounts, column: [:branch_id, :party_id] - # Remove the index named by_branch_party in the accounts table. + # + # Removes the index named +by_branch_party+ in the +accounts+ table. + # # remove_index :accounts, name: :by_branch_party + # def remove_index(table_name, options = {}) remove_index!(table_name, index_name_for_remove(table_name, options)) end @@ -444,10 +548,12 @@ module ActiveRecord execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" end - # Rename an index. + # Renames an index. + # + # Rename the +index_people_on_last_name+ index to +index_users_on_last_name+: # - # Rename the index_people_on_last_name index to index_users_on_last_name # rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name' + # def rename_index(table_name, old_name, new_name) # this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance) old_index_def = indexes(table_name).detect { |i| i.name == old_name } @@ -470,7 +576,7 @@ module ActiveRecord end end - # Verify the existence of an index with a given name. + # Verifies the existence of an index with a given name. # # The default argument is returned if the underlying implementation does not define the indexes method, # as there's no way to determine the correct answer in that case. @@ -484,20 +590,23 @@ module ActiveRecord # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. # # ====== Create a user_id column - # add_reference(:products, :user) + # + # add_reference(:products, :user) # # ====== Create a supplier_id and supplier_type columns - # add_belongs_to(:products, :supplier, polymorphic: true) + # + # 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) + # + # 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 + add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options end alias :add_belongs_to :add_reference @@ -505,10 +614,12 @@ module ActiveRecord # <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_reference(:products, :user, index: true) # # ====== Remove polymorphic reference - # remove_reference(:products, :supplier, polymorphic: true) + # + # remove_reference(:products, :supplier, polymorphic: true) # def remove_reference(table_name, ref_name, options = {}) remove_column(table_name, "#{ref_name}_id") @@ -583,33 +694,28 @@ module ActiveRecord end end - def add_column_options!(sql, options) #:nodoc: - sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options) - # must explicitly check for :null to allow change_column to work on migrations - if options[:null] == false - sql << " NOT NULL" - end - end - - # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. - # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax. + # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT. + # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they + # require the order columns appear in the SELECT. # - # distinct("posts.id", "posts.created_at desc") - def distinct(columns, order_by) - "DISTINCT #{columns}" + # columns_for_distinct("posts.id", ["posts.created_at desc"]) + def columns_for_distinct(columns, orders) # :nodoc: + columns end - # Adds timestamps (created_at and updated_at) columns to the named table. + # Adds timestamps (+created_at+ and +updated_at+) columns to the named table. + # + # add_timestamps(:suppliers) # - # add_timestamps(:suppliers) def add_timestamps(table_name) 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. + # Removes the timestamp columns (+created_at+ and +updated_at+) from the table definition. # # remove_timestamps(:suppliers) + # def remove_timestamps(table_name) remove_column table_name, :updated_at remove_column table_name, :created_at @@ -649,27 +755,23 @@ module ActiveRecord column_names = Array(column_name) index_name = index_name(table_name, column: column_names) - if Hash === options # legacy support, since this param was a string - options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal) + options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) - index_type = options[:unique] ? "UNIQUE" : "" - index_name = options[:name].to_s if options.key?(:name) - max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length + index_type = options[:unique] ? "UNIQUE" : "" + index_type = options[:type].to_s if options.key?(:type) + index_name = options[:name].to_s if options.key?(:name) + max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length - if supports_partial_index? - index_options = options[:where] ? " WHERE #{options[:where]}" : "" - end - else - if options - message = "Passing a string as third argument of `add_index` is deprecated and will" + - " be removed in Rails 4.1." + - " Use add_index(#{table_name.inspect}, #{column_name.inspect}, unique: true) instead" + if options.key?(:algorithm) + algorithm = index_algorithms.fetch(options[:algorithm]) { + raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") + } + end - ActiveSupport::Deprecation.warn message - end + using = "USING #{options[:using]}" if options[:using].present? - index_type = options - max_index_length = allowed_index_name_length + if supports_partial_index? + index_options = options[:where] ? " WHERE #{options[:where]}" : "" end if index_name.length > max_index_length @@ -680,7 +782,7 @@ module ActiveRecord end index_columns = quoted_columns_for_index(column_names, options).join(", ") - [index_name, index_type, index_columns, index_options] + [index_name, index_type, index_columns, index_options, algorithm, using] end def index_name_for_remove(table_name, options = {}) @@ -701,12 +803,6 @@ module ActiveRecord index_name end - def columns_for_remove(table_name, *column_names) - ActiveSupport::Deprecation.warn("columns_for_remove is deprecated and will be removed in the future") - raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.blank? - column_names.map {|column_name| quote_column_name(column_name) } - end - def rename_table_indexes(table_name, new_name) indexes(new_name).each do |index| generated_index_name = index_name(table_name, column: index.columns) @@ -730,8 +826,12 @@ module ActiveRecord end private - def create_table_definition - TableDefinition.new(self) + def create_table_definition(name, temporary, options) + TableDefinition.new native_database_types, name, temporary, options + end + + def create_alter_table(name) + AlterTable.new create_table_definition(name, false, {}) end def update_table_definition(table_name, base) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 73c80a3220..2b6685499a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -78,7 +78,7 @@ module ActiveRecord @joinable = options.fetch(:joinable, true) end - # This state is necesarry so that we correctly handle stuff that might + # This state is necessary so that we correctly handle stuff that might # happen in a commit/rollback. But it's kinda distasteful. Maybe we can # find a better way to structure it in the future. def finishing? diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 7949bcb5ce..cbe563676b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -18,6 +18,7 @@ module ActiveRecord autoload :ColumnDefinition autoload :TableDefinition autoload :Table + autoload :AlterTable end autoload_at 'active_record/connection_adapters/abstract/connection_pool' do @@ -32,6 +33,7 @@ module ActiveRecord autoload :Quoting autoload :ConnectionPool autoload :QueryCache + autoload :Savepoints end autoload_at 'active_record/connection_adapters/abstract/transaction' do @@ -94,10 +96,95 @@ module ActiveRecord @last_use = false @logger = logger @pool = pool - @query_cache = Hash.new { |h,sql| h[sql] = {} } - @query_cache_enabled = false @schema_cache = SchemaCache.new self @visitor = nil + @prepared_statements = false + end + + def valid_type?(type) + true + end + + class SchemaCreation + def initialize(conn) + @conn = conn + @cache = {} + end + + def accept(o) + m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}" + send m, o + end + + def visit_AddColumn(o) + sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql = "ADD #{quote_column_name(o.name)} #{sql_type}" + add_column_options!(sql, column_options(o)) + end + + private + + def visit_AlterTable(o) + sql = "ALTER TABLE #{quote_table_name(o.name)} " + sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + end + + def visit_ColumnDefinition(o) + sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + column_sql = "#{quote_column_name(o.name)} #{sql_type}" + add_column_options!(column_sql, column_options(o)) unless o.primary_key? + column_sql + end + + def visit_TableDefinition(o) + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " + create_sql << "#{quote_table_name(o.name)} (" + create_sql << o.columns.map { |c| accept c }.join(', ') + create_sql << ") #{o.options}" + create_sql + end + + def column_options(o) + column_options = {} + column_options[:null] = o.null unless o.null.nil? + column_options[:default] = o.default unless o.default.nil? + column_options[:column] = o + column_options[:first] = o.first + column_options[:after] = o.after + column_options + end + + def quote_column_name(name) + @conn.quote_column_name name + end + + def quote_table_name(name) + @conn.quote_table_name name + end + + def type_to_sql(type, limit, precision, scale) + @conn.type_to_sql type.to_sym, limit, precision, scale + end + + def add_column_options!(sql, options) + sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options) + # must explicitly check for :null to allow change_column to work on migrations + if options[:null] == false + sql << " NOT NULL" + end + if options[:auto_increment] == true + sql << " AUTO_INCREMENT" + end + sql + end + + def options_include_default?(options) + options.include?(:default) && !(options[:null] == false && options[:default].nil?) + end + end + + def schema_creation + SchemaCreation.new self end def lease @@ -123,10 +210,11 @@ module ActiveRecord end def unprepared_statement - old, @visitor = @visitor, unprepared_visitor + old_prepared_statements, @prepared_statements = @prepared_statements, false + old_visitor, @visitor = @visitor, unprepared_visitor yield ensure - @visitor = old + @visitor, @prepared_statements = old_visitor, old_prepared_statements end # Returns the human-readable name of the adapter. Use mixed case - one @@ -206,16 +294,30 @@ module ActiveRecord false end + # This is meant to be implemented by the adapters that support extensions + def disable_extension(name) + end + + # This is meant to be implemented by the adapters that support extensions + def enable_extension(name) + end + # A list of extensions, to be filled in by adapters that support them. At # the moment only postgresql does. def extensions [] end + # A list of index algorithms, to be filled by adapters that support them. + # MySQL and PostgreSQL have support for them right now. + def index_algorithms + {} + end + # QUOTING ================================================== - # Returns a bind substitution value given a +column+ and list of current - # +binds+. + # Returns a bind substitution value given a bind +index+ and +column+ + # NOTE: The column param is currently being used by the sqlserver-adapter def substitute_at(column, index) Arel::Nodes::BindParam.new '?' end @@ -294,27 +396,13 @@ module ActiveRecord @transaction.number end - def increment_open_transactions - ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect" + def create_savepoint(name = nil) end - def decrement_open_transactions - ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect" + def rollback_to_savepoint(name = nil) end - def transaction_joinable=(joinable) - message = "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead." - ActiveSupport::Deprecation.warn message - @transaction.joinable = joinable - end - - def create_savepoint - end - - def rollback_to_savepoint - end - - def release_savepoint + def release_savepoint(name = nil) end def case_sensitive_modifier(node) @@ -336,13 +424,14 @@ module ActiveRecord protected - def log(sql, name = "SQL", binds = []) + def log(sql, name = "SQL", binds = [], statement_name = nil) @instrumenter.instrument( "sql.active_record", - :sql => sql, - :name => name, - :connection_id => object_id, - :binds => binds) { yield } + :sql => sql, + :name => name, + :connection_id => object_id, + :statement_name => statement_name, + :binds => binds) { yield } rescue => e message = "#{e.class.name}: #{e.message}: #{sql}" @logger.error message if @logger @@ -353,7 +442,11 @@ module ActiveRecord def translate_exception(exception, message) # override in derived class - ActiveRecord::StatementInvalid.new(message) + ActiveRecord::StatementInvalid.new(message, exception) + end + + def without_prepared_statement?(binds) + !@prepared_statements || binds.empty? 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 f88f5742a8..138ab811dc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -3,13 +3,45 @@ require 'arel/visitors/bind_visitor' module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter + include Savepoints + + class SchemaCreation < AbstractAdapter::SchemaCreation + + def visit_AddColumn(o) + add_column_position!(super, column_options(o)) + end + + private + def visit_ChangeColumnDefinition(o) + column = o.column + options = o.options + sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale]) + change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}" + add_column_options!(change_column_sql, options) + add_column_position!(change_column_sql, options) + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + sql + end + end + + def schema_creation + SchemaCreation.new self + end + class Column < ConnectionAdapters::Column # :nodoc: - attr_reader :collation, :strict + attr_reader :collation, :strict, :extra - def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false) + def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false, extra = "") @strict = strict @collation = collation - + @extra = extra super(name, default, sql_type, null) end @@ -61,6 +93,8 @@ module ActiveRecord def extract_limit(sql_type) case sql_type + when /^enum\((.+)\)/i + $1.split(',').map{|enum| enum.strip.length - 2}.max when /blob|text/i case sql_type when /tiny/i @@ -77,8 +111,6 @@ 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 @@ -130,6 +162,9 @@ module ActiveRecord :boolean => { :name => "tinyint", :limit => 1 } } + INDEX_TYPES = [:fulltext, :spatial] + INDEX_USINGS = [:btree, :hash] + class BindSubstitution < Arel::Visitors::MySQL # :nodoc: include Arel::Visitors::BindVisitor end @@ -141,6 +176,7 @@ module ActiveRecord @quoted_column_names, @quoted_table_names = {}, {} if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) + @prepared_statements = true @visitor = Arel::Visitors::MySQL.new self else @visitor = unprepared_visitor @@ -160,11 +196,6 @@ module ActiveRecord true end - # Returns true, since this connection adapter supports savepoints. - def supports_savepoints? - true - end - def supports_bulk_alter? #:nodoc: true end @@ -187,6 +218,10 @@ module ActiveRecord NATIVE_DATABASE_TYPES end + def index_algorithms + { default: 'ALGORITHM = DEFAULT', copy: 'ALGORITHM = COPY', inplace: 'ALGORITHM = INPLACE' } + end + # HELPER METHODS =========================================== # The two drivers have slightly different ways of yielding hashes of results, so @@ -196,8 +231,8 @@ module ActiveRecord end # Overridden by the adapters to instantiate their specific Column type. - def new_column(field, default, type, null, collation) # :nodoc: - Column.new(field, default, type, null, collation) + def new_column(field, default, type, null, collation, extra = "") # :nodoc: + Column.new(field, default, type, null, collation, extra) end # Must return the Mysql error number from the exception, if the exception has an @@ -209,8 +244,8 @@ module ActiveRecord # 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] + if value.kind_of?(String) && column && column.type == :binary + s = value.unpack("H*")[0] "x'#{s}'" elsif value.kind_of?(BigDecimal) value.to_s("F") @@ -259,7 +294,7 @@ module ActiveRecord end rescue ActiveRecord::StatementInvalid => exception if exception.message.split(":").first =~ /Packets out of order/ - raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." + raise ActiveRecord::StatementInvalid.new("'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings.", exception.original_exception) else raise end @@ -302,18 +337,6 @@ module ActiveRecord # Transactions aren't supported 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 - # 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. @@ -336,7 +359,9 @@ module ActiveRecord # and creates it again using the provided +options+. def recreate_database(name, options = {}) drop_database(name) - create_database(name, options) + sql = create_database(name, options) + reconnect! + sql end # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. @@ -410,7 +435,11 @@ module ActiveRecord if current_index != row[:Key_name] next if row[:Key_name] == 'PRIMARY' # skip the primary key current_index = row[:Key_name] - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], []) + + mysql_index_type = row[:Index_type].downcase.to_sym + index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil + index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil + indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using) end indexes.last.columns << row[:Column_name] @@ -426,7 +455,8 @@ module ActiveRecord sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}" execute_and_free(sql, 'SCHEMA') do |result| each_hash(result).map do |field| - new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation]) + field_name = set_field_encoding(field[:Field]) + new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra]) end end end @@ -459,10 +489,6 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end - def add_column(table_name, column_name, type, options = {}) - execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}") - end - def change_column_default(table_name, column_name, default) column = column_for(table_name, column_name) change_column table_name, column_name, column.sql_type, :default => default @@ -487,6 +513,11 @@ module ActiveRecord rename_column_indexes(table_name, column_name, new_column_name) end + def add_index(table_name, column_name, options = {}) #:nodoc: + index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" + end + # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s @@ -572,6 +603,10 @@ module ActiveRecord self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end + def valid_type?(type) + !native_database_types[type].nil? + end + protected # MySQL is too stupid to create a temporary table for use subquery, so we have @@ -622,10 +657,9 @@ module ActiveRecord end def add_column_sql(table_name, column_name, type, options = {}) - add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - add_column_position!(add_column_sql, options) - add_column_sql + td = create_table_definition table_name, options[:temporary], options[:options] + cd = td.new_column_definition(column_name, type, options) + schema_creation.visit_AddColumn cd end def change_column_sql(table_name, column_name, type, options = {}) @@ -639,26 +673,23 @@ module ActiveRecord options[:null] = column.null end - change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(change_column_sql, options) - add_column_position!(change_column_sql, options) - change_column_sql + options[:name] = column.name + schema_creation.accept ChangeColumnDefinition.new column, type, options end def rename_column_sql(table_name, column_name, new_column_name) - options = {} + options = { name: new_column_name } if column = columns(table_name).find { |c| c.name == column_name.to_s } options[:default] = column.default options[:null] = column.null + options[:auto_increment] = (column.extra == "auto_increment") else 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}'", '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 + schema_creation.accept ChangeColumnDefinition.new column, current_type, options end def remove_column_sql(table_name, column_name, type = nil, options = {}) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index a4b3a0c584..f2fbd5a8f2 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -13,7 +13,7 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale + attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale, :default_function attr_accessor :primary, :coder alias :encoded? :coder @@ -27,16 +27,17 @@ module ActiveRecord # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. # +null+ determines if this column allows +NULL+ values. def initialize(name, default, sql_type = nil, null = true) - @name = name - @sql_type = sql_type - @null = null - @limit = extract_limit(sql_type) - @precision = extract_precision(sql_type) - @scale = extract_scale(sql_type) - @type = simplified_type(sql_type) - @default = extract_default(default) - @primary = nil - @coder = nil + @name = name + @sql_type = sql_type + @null = null + @limit = extract_limit(sql_type) + @precision = extract_precision(sql_type) + @scale = extract_scale(sql_type) + @type = simplified_type(sql_type) + @default = extract_default(default) + @default_function = nil + @primary = nil + @coder = nil end # Returns +true+ if the column is either of type string or text. @@ -107,30 +108,6 @@ module ActiveRecord end end - def type_cast_code(var_name) - message = "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." - ActiveSupport::Deprecation.warn message - - klass = self.class.name - - case type - when :string, :text then var_name - when :integer then "#{klass}.value_to_integer(#{var_name})" - when :float then "#{var_name}.to_f" - when :decimal then "#{klass}.value_to_decimal(#{var_name})" - when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" - when :time then "#{klass}.string_to_dummy_time(#{var_name})" - 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}.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 - # Returns the human name of the column name. # # ===== Examples @@ -143,17 +120,7 @@ module ActiveRecord type_cast(default) end - # Used to convert from Strings to BLOBs - def string_to_binary(value) - self.class.string_to_binary(value) - end - class << self - # Used to convert from Strings to BLOBs - def string_to_binary(value) - value - end - # Used to convert from BLOBs to Strings def binary_to_string(value) value @@ -161,7 +128,7 @@ module ActiveRecord def value_to_date(value) if value.is_a?(String) - return nil if value.blank? + return nil if value.empty? fast_string_to_date(value) || fallback_string_to_date(value) elsif value.respond_to?(:to_date) value.to_date @@ -172,14 +139,14 @@ module ActiveRecord def string_to_time(string) return string unless string.is_a?(String) - return nil if string.blank? + return nil if string.empty? 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.blank? + return nil if string.empty? dummy_time_string = "2000-01-01 #{string}" @@ -192,7 +159,7 @@ module ActiveRecord # convert something to a boolean def value_to_boolean(value) - if value.is_a?(String) && value.blank? + if value.is_a?(String) && value.empty? nil else TRUE_VALUES.include?(value) @@ -237,11 +204,19 @@ module ActiveRecord end end - def new_time(year, mon, mday, hour, min, sec, microsec) + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) # Treat 0000-00-00 00:00:00 as nil. return nil if year.nil? || (year == 0 && mon == 0 && mday == 0) - Time.send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + if offset + time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return nil unless time + + time -= offset + Base.default_timezone == :utc ? time : time.getlocal + else + Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end end def fast_string_to_date(string) @@ -266,7 +241,7 @@ module ActiveRecord time_hash = Date._parse(string) time_hash[:sec_fraction] = microseconds(time_hash) - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) end end @@ -306,7 +281,7 @@ module ActiveRecord :text when /blob/i, /binary/i :binary - when /char/i, /string/i + when /char/i :string when /boolean/i :boolean diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 8bad7d0cf5..64fc9e95d8 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -55,7 +55,7 @@ module ActiveRecord begin require path_to_adapter rescue Gem::LoadError => e - raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile." + raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)." rescue LoadError => e raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 20a5ca2baa..e790f731ea 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,10 +1,10 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' -gem 'mysql2', '~> 0.3.10' +gem 'mysql2', '~> 0.3.13' require 'mysql2' module ActiveRecord - module ConnectionHandling + module ConnectionHandling # :nodoc: # Establishes a connection to the database that's used by all Active Record objects. def mysql2_connection(config) config = config.symbolize_keys @@ -38,6 +38,15 @@ module ActiveRecord configure_connection end + MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191 + def initialize_schema_migrations_table + if @config[:encoding] == 'utf8mb4' + ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4) + else + ActiveRecord::SchemaMigration.create_table + end + end + def supports_explain? true end @@ -54,8 +63,8 @@ module ActiveRecord end end - def new_column(field, default, type, null, collation) # :nodoc: - Column.new(field, default, type, null, collation, strict_mode?) + def new_column(field, default, type, null, collation, extra = "") # :nodoc: + Column.new(field, default, type, null, collation, strict_mode?, extra) end def error_number(exception) @@ -204,9 +213,11 @@ module ActiveRecord # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) - # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been - # made since we established the connection - @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + if @connection + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + end super end @@ -218,8 +229,7 @@ module ActiveRecord alias exec_without_stmt exec_query - # Returns an array of record hashes with the column names as keys and - # column values as values. + # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) exec_query(sql, name) end @@ -259,6 +269,10 @@ module ActiveRecord def version @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } end + + def set_field_encoding field_name + field_name + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 7544c2a783..88c9494fc6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -16,9 +16,9 @@ class Mysql end module ActiveRecord - module ConnectionHandling + module ConnectionHandling # :nodoc: # Establishes a connection to the database that's used by all Active Record objects. - def mysql_connection(config) # :nodoc: + def mysql_connection(config) config = config.symbolize_keys host = config[:host] port = config[:port] @@ -150,8 +150,8 @@ module ActiveRecord end end - def new_column(field, default, type, null, collation) # :nodoc: - Column.new(field, default, type, null, collation, strict_mode?) + def new_column(field, default, type, null, collation, extra = "") # :nodoc: + Column.new(field, default, type, null, collation, strict_mode?, extra) end def error_number(exception) # :nodoc: @@ -279,11 +279,7 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - # 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? + if without_prepared_statement?(binds) result_set, affected_rows = exec_without_stmt(sql, name) else result_set, affected_rows = exec_stmt(sql, name, binds) @@ -383,7 +379,7 @@ module ActiveRecord TYPES = {} - # Register an MySQL +type_id+ with a typcasting object in + # Register an MySQL +type_id+ with a typecasting object in # +type+. def self.register_type(type_id, type) TYPES[type_id] = type @@ -393,6 +389,14 @@ module ActiveRecord TYPES[new] = TYPES[old] end + def self.find_type(field) + if field.type == Mysql::Field::TYPE_TINY && field.length > 1 + TYPES[Mysql::Field::TYPE_LONG] + else + TYPES.fetch(field.type) { Fields::Identity.new } + end + 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 @@ -425,9 +429,7 @@ module ActiveRecord if field.decimals > 0 types[field.name] = Fields::Decimal.new else - types[field.name] = Fields::TYPES.fetch(field.type) { - Fields::Identity.new - } + types[field.name] = Fields.find_type field end } result_set = ActiveRecord::Result.new(types.keys, result.to_a, types) @@ -501,12 +503,12 @@ module ActiveRecord cols = cache[:cols] ||= metadata.fetch_fields.map { |field| field.name } + metadata.free end 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? @@ -553,6 +555,14 @@ module ActiveRecord def version @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } end + + def set_field_encoding field_name + field_name.force_encoding(client_encoding) + if internal_enc = Encoding.default_internal + field_name = field_name.encode!(internal_enc) + end + field_name + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb index b7d24f2bb3..20de8d1982 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -2,6 +2,13 @@ module ActiveRecord module ConnectionAdapters class PostgreSQLColumn < Column module ArrayParser + + DOUBLE_QUOTE = '"' + BACKSLASH = "\\" + COMMA = ',' + BRACKET_OPEN = '{' + BRACKET_CLOSE = '}' + private # Loads pg_array_parser if available. String parsing can be # performed quicker by a native extension, which will not create @@ -12,18 +19,18 @@ module ActiveRecord include PgArrayParser rescue LoadError def parse_pg_array(string) - parse_data(string, 0) + parse_data(string) end end - def parse_data(string, index) - local_index = index + def parse_data(string) + local_index = 0 array = [] while(local_index < string.length) case string[local_index] - when '{' + when BRACKET_OPEN local_index,array = parse_array_contents(array, string, local_index + 1) - when '}' + when BRACKET_CLOSE return array end local_index += 1 @@ -33,9 +40,9 @@ module ActiveRecord end def parse_array_contents(array, string, index) - is_escaping = false - is_quoted = false - was_quoted = false + is_escaping = false + is_quoted = false + was_quoted = false current_item = '' local_index = index @@ -47,29 +54,29 @@ module ActiveRecord else if is_quoted case token - when '"' + when DOUBLE_QUOTE is_quoted = false was_quoted = true - when "\\" + when BACKSLASH is_escaping = true else current_item << token end else case token - when "\\" + when BACKSLASH is_escaping = true - when ',' + when COMMA add_item_to_array(array, current_item, was_quoted) current_item = '' was_quoted = false - when '"' + when DOUBLE_QUOTE is_quoted = true - when '{' + when BRACKET_OPEN internal_items = [] local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) array.push(internal_items) - when '}' + when BRACKET_CLOSE add_item_to_array(array, current_item, was_quoted) return local_index,array else diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index 3d8f0b575c..ea44e818e5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -2,12 +2,23 @@ module ActiveRecord module ConnectionAdapters class PostgreSQLColumn < Column module Cast + def point_to_string(point) + "(#{point[0]},#{point[1]})" + end + + def string_to_point(string) + if string[0] == '(' && string[-1] == ')' + string = string[1...-1] + end + string.split(',').map{ |v| Float(v) } + end + def string_to_time(string) return string unless String === string case string - when 'infinity'; 1.0 / 0.0 - when '-infinity'; -1.0 / 0.0 + when 'infinity'; Float::INFINITY + when '-infinity'; -Float::INFINITY when / BC$/ super("-" + string.sub(/ BC$/, "")) else @@ -15,6 +26,15 @@ module ActiveRecord end end + def string_to_bit(value) + case value + when /^0x/i + value[2..-1].hex.to_s(2) # Hexadecimal notation + else + value # Bit-string notation + end + end + def hstore_to_string(object) if Hash === object object.map { |k,v| @@ -30,8 +50,8 @@ module ActiveRecord 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') + v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') [k,v] }] else @@ -40,7 +60,7 @@ module ActiveRecord end def json_to_string(object) - if Hash === object + if Hash === object || Array === object ActiveSupport::JSON.encode(object) else object @@ -80,7 +100,11 @@ module ActiveRecord if string.nil? nil elsif String === string - IPAddr.new(string) + begin + IPAddr.new(string) + rescue ArgumentError + nil + end else string end @@ -95,7 +119,7 @@ module ActiveRecord end def string_to_array(string, oid) - parse_pg_array(string).map{|val| oid.type_cast val} + parse_pg_array(string).map {|val| type_cast_array(oid, val)} end private @@ -126,6 +150,14 @@ module ActiveRecord "\"#{value.gsub(/"/,"\\\"")}\"" end end + + def type_cast_array(oid, value) + if ::Array === value + value.map {|item| type_cast_array(oid, item)} + else + oid.type_cast value + 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 index 9b5170f657..fa173d13a2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -134,34 +134,31 @@ module ActiveRecord 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 + result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : + exec_cache(sql, name, binds) + + types = {} + fields = result.fields + 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(fields, result.values, types) + result.clear + return ret 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 + result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : + exec_cache(sql, name, binds) + affected = result.cmd_tuples + result.clear + affected end alias :exec_update :exec_delete @@ -217,25 +214,6 @@ module ActiveRecord def rollback_db_transaction execute "ROLLBACK" end - - def outside_transaction? - message = "#outside_transaction? is deprecated. This method was only really used " \ - "internally, but you can use #transaction_open? instead." - ActiveSupport::Deprecation.warn message - @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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 68f2f2ca7b..6c5792954f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -6,20 +6,27 @@ module ActiveRecord module OID class Type def type; end + end - def type_cast_for_write(value) + class Identity < Type + def type_cast(value) value end end - class Identity < Type + class Bit < Type def type_cast(value) - value + if String === value + ConnectionAdapters::PostgreSQLColumn.string_to_bit value + else + value + end end end class Bytea < Type def type_cast(value) + return if value.nil? PGconn.unescape_bytea value end end @@ -27,12 +34,17 @@ module ActiveRecord class Money < Type def type_cast(value) return if value.nil? + return value unless String === value # 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 + # Negative values are represented as follows: + # (3) -$2.55 + # (4) ($2.55) + value.sub!(/^\((.+)\)$/, '-\1') # (4) case value when /^-?\D+[\d,]+\.\d{2}$/ # (1) value.gsub!(/[^-\d.]/, '') @@ -63,6 +75,16 @@ module ActiveRecord end end + class Point < Type + def type_cast(value) + if String === value + ConnectionAdapters::PostgreSQLColumn.string_to_point value + else + value + end + end + end + class Array < Type attr_reader :subtype def initialize(subtype) @@ -203,11 +225,19 @@ module ActiveRecord end class Hstore < Type + def type_cast_for_write(value) + ConnectionAdapters::PostgreSQLColumn.hstore_to_string value + end + def type_cast(value) return if value.nil? ConnectionAdapters::PostgreSQLColumn.string_to_hstore value end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end end class Cidr < Type @@ -219,11 +249,19 @@ module ActiveRecord end class Json < Type + def type_cast_for_write(value) + ConnectionAdapters::PostgreSQLColumn.json_to_string value + end + def type_cast(value) return if value.nil? ConnectionAdapters::PostgreSQLColumn.string_to_json value end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end end class TypeMap @@ -312,14 +350,14 @@ module ActiveRecord # 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' register_type 'money', OID::Money.new register_type 'bytea', OID::Bytea.new register_type 'bool', OID::Boolean.new + register_type 'bit', OID::Bit.new + register_type 'varbit', OID::Bit.new register_type 'float4', OID::Float.new alias_type 'float8', 'float4' @@ -330,6 +368,7 @@ module ActiveRecord register_type 'time', OID::Time.new register_type 'path', OID::Identity.new + register_type 'point', OID::Point.new register_type 'polygon', OID::Identity.new register_type 'circle', OID::Identity.new register_type 'hstore', OID::Hstore.new diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 47e2e3928f..e9daa5d7ff 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -18,27 +18,34 @@ module ActiveRecord def quote(value, column = nil) #:nodoc: return super unless column + sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale) + case value when Range - if /range$/ =~ column.sql_type - "'#{PostgreSQLColumn.range_to_string(value)}'::#{column.sql_type}" + if /range$/ =~ sql_type + "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}" else super end when Array - if column.array - "'#{PostgreSQLColumn.array_to_string(value, column, self)}'" + case sql_type + when 'point' then super(PostgreSQLColumn.point_to_string(value)) + when 'json' then super(PostgreSQLColumn.json_to_string(value)) else - super + if column.array + "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'" + else + super + end end when Hash - case column.sql_type + case 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 + case sql_type when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) else super end @@ -51,11 +58,14 @@ module ActiveRecord 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}'" + if sql_type == 'money' || [:string, :text].include?(column.type) + # Not truly string input, so doesn't require (or allow) escape string syntax. + "'#{value}'" + else + super + end when String - case column.sql_type + case sql_type when 'bytea' then "'#{escape_bytea(value)}'" when 'xml' then "xml '#{quote_string(value)}'" when /^bit/ @@ -87,8 +97,13 @@ module ActiveRecord super(value, column) end when Array - return super(value, column) unless column.array - PostgreSQLColumn.array_to_string(value, column, self) + case column.sql_type + when 'point' then PostgreSQLColumn.point_to_string(value) + when 'json' then PostgreSQLColumn.json_to_string(value) + else + return super(value, column) unless column.array + PostgreSQLColumn.array_to_string(value, column, self) + end when String return super(value, column) unless 'bytea' == column.sql_type { :value => value, :format => 1 } diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 3bc61c5e0c..5dc70a5ad1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -1,6 +1,42 @@ module ActiveRecord module ConnectionAdapters class PostgreSQLAdapter < AbstractAdapter + class SchemaCreation < AbstractAdapter::SchemaCreation + private + + def visit_AddColumn(o) + sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}" + add_column_options!(sql, column_options(o)) + end + + def visit_ColumnDefinition(o) + sql = super + if o.primary_key? && o.type == :uuid + sql << " PRIMARY KEY " + add_column_options!(sql, column_options(o)) + end + sql + end + + def add_column_options!(sql, options) + if options[:array] || options[:column].try(:array) + sql << '[]' + end + + column = options.fetch(:column) { return super } + if column.type == :uuid && options[:default] =~ /\(\)/ + sql << " DEFAULT #{options[:default]}" + else + super + end + end + end + + def schema_creation + SchemaCreation.new self + end + module SchemaStatements # Drops the database specified on the +name+ attribute # and creates it again using the provided +options+. @@ -10,7 +46,7 @@ module ActiveRecord end # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, - # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>, + # <tt>:encoding</tt> (defaults to utf8), <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>). # @@ -120,12 +156,15 @@ module ActiveRecord 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] + unless column_names.empty? + # 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] + using = inddef.scan(/USING (.+?) /).flatten[0].to_sym - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) + IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using) + end end.compact end @@ -282,6 +321,7 @@ module ActiveRecord result = query(<<-end_sql, 'SCHEMA')[0] SELECT attr.attname, CASE + WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1) @@ -293,7 +333,7 @@ module ActiveRecord JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) WHERE t.oid = '#{quote_table_name(table)}'::regclass AND cons.contype = 'p' - AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval' + AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate' end_sql end @@ -337,18 +377,16 @@ module ActiveRecord # 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 + super 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])}" + sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale]) + sql_type << "[]" if options[:array] + execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" 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) @@ -375,6 +413,11 @@ module ActiveRecord rename_column_indexes(table_name, column_name, new_column_name) end + def add_index(table_name, column_name, options = {}) #:nodoc: + index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}" + end + def remove_index!(table_name, index_name) #:nodoc: execute "DROP INDEX #{quote_table_name(index_name)}" end @@ -425,22 +468,17 @@ module ActiveRecord 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"]) - # # => "DISTINCT posts.id, posts.created_at AS alias_0" - def distinct(columns, orders) #:nodoc: - order_columns = orders.map{ |s| + def columns_for_distinct(columns, orders) #:nodoc: + order_columns = orders.reject(&:blank?).map{ |s| # Convert Arel node to string s = s.to_sql unless s.is_a?(String) # Remove any ASC/DESC modifiers s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } - [super].concat(order_columns).join(', ') + [super, *order_columns].join(', ') 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 cfcc783904..3668aecd4b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -16,15 +16,15 @@ require 'pg' require 'ipaddr' module ActiveRecord - module ConnectionHandling + module ConnectionHandling # :nodoc: VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout, :client_encoding, :options, :application_name, :fallback_application_name, :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count, - :tty, :sslmode, :requiressl, :sslcert, :sslkey, :sslrootcert, :sslcrl, - :requirepeer, :krbsrvname, :gsslib, :service] + :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey, + :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service] # Establishes a connection to the database that's used by all Active Record objects - def postgresql_connection(config) # :nodoc: + def postgresql_connection(config) conn_params = config.symbolize_keys conn_params.delete_if { |_, v| v.nil? } @@ -49,13 +49,17 @@ module ActiveRecord # Instantiates a new PostgreSQL column definition in a table. def initialize(name, default, oid_type, sql_type = nil, null = true) @oid_type = oid_type + default_value = self.class.extract_value_from_default(default) + if sql_type =~ /\[\]$/ @array = true - super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null) + super(name, default_value, sql_type[0..sql_type.length - 3], null) else @array = false - super(name, self.class.extract_value_from_default(default), sql_type, null) + super(name, default_value, sql_type, null) end + + @default_function = default if has_default_function?(default_value, default) end # :stopdoc: @@ -80,11 +84,11 @@ module ActiveRecord when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m $1 # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?)\z/ + when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ $1 # Character types when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m - $1 + $1.gsub(/''/, "'") # Binary data types when /\A'(.*)'::bytea\z/m $1 @@ -129,6 +133,14 @@ module ActiveRecord end end + def type_cast_for_write(value) + if @oid_type.respond_to?(:type_cast_for_write) + @oid_type.type_cast_for_write(value) + else + super + end + end + def type_cast(value) return if value.nil? return super if encoded? @@ -136,8 +148,16 @@ module ActiveRecord @oid_type.type_cast value end + def accessor + @oid_type.accessor + end + private + def has_default_function?(default_value, default) + !default_value && (%r{\w+\(.*\)} === default) + end + def extract_limit(sql_type) case sql_type when /^bigint/i; 8 @@ -330,6 +350,41 @@ module ActiveRecord class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods + # Defines the primary key field. + # Use of the native PostgreSQL UUID type is supported, and can be used + # by defining your tables as such: + # + # create_table :stuffs, id: :uuid do |t| + # t.string :content + # t.timestamps + # end + # + # By default, this will use the +uuid_generate_v4()+ function from the + # +uuid-ossp+ extension, which MUST be enabled on your database. To enable + # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your + # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can + # set the +:default+ option to +nil+: + # + # create_table :stuffs, id: false do |t| + # t.primary_key :id, :uuid, default: nil + # t.uuid :foo_id + # t.timestamps + # end + # + # You may also pass a different UUID generation function from +uuid-ossp+ + # or another library. + # + # Note that setting the UUID primary key default value to +nil+ will + # require you to assure that you always provide a UUID value before saving + # a record (as primary keys cannot be +nil+). This might be done via the + # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. + def primary_key(name, type = :primary_key, options = {}) + return super unless type == :uuid + options[:default] = options.fetch(:default, 'uuid_generate_v4()') + options[:primary_key] = true + column name, type, options + end + def column(name, type = nil, options = {}) super column = self[name] @@ -340,12 +395,9 @@ module ActiveRecord private - def new_column_definition(base, name, type) - definition = ColumnDefinition.new base, name, type - @columns << definition - @columns_hash[name] = definition - definition - end + def create_column_definition(name, type) + ColumnDefinition.new name, type + end end class Table < ActiveRecord::ConnectionAdapters::Table @@ -388,6 +440,7 @@ module ActiveRecord include ReferentialIntegrity include SchemaStatements include DatabaseStatements + include Savepoints # Returns 'PostgreSQL' as adapter name for identification purposes. def adapter_name @@ -399,6 +452,7 @@ module ActiveRecord def prepare_column_options(column, types) spec = super spec[:array] = 'true' if column.respond_to?(:array) && column.array + spec[:default] = "\"#{column.default_function}\"" if column.default_function spec end @@ -425,6 +479,10 @@ module ActiveRecord true end + def index_algorithms + { concurrently: 'CONCURRENTLY' } + end + class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) super @@ -487,6 +545,7 @@ module ActiveRecord super(connection, logger) if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) + @prepared_statements = true @visitor = Arel::Visitors::PostgreSQL.new self else @visitor = unprepared_visitor @@ -518,8 +577,7 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - @connection.query 'SELECT 1' - true + @connection.connect_poll != PG::PGRES_POLLING_FAILED rescue PGError false end @@ -573,19 +631,14 @@ module ActiveRecord true end - # Returns true, since this connection adapter supports savepoints. - def supports_savepoints? - true - end - # Returns true. def supports_explain? true end - # Returns true if pg > 9.2 + # Returns true if pg > 9.1 def supports_extensions? - postgresql_version >= 90200 + postgresql_version >= 90100 end # Range datatypes weren't introduced until PostgreSQL 9.2 @@ -600,16 +653,16 @@ module ActiveRecord end def disable_extension(name) - exec_query("DROP EXTENSION IF EXISTS #{name} CASCADE").tap { + exec_query("DROP EXTENSION IF EXISTS \"#{name}\" CASCADE").tap { reload_type_map } end def extension_enabled?(name) if supports_extensions? - res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL)", + res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", 'SCHEMA' - res.column_types['exists'].type_cast res.rows.first.first + res.column_types['enabled'].type_cast res.rows.first.first end end @@ -627,13 +680,6 @@ module ActiveRecord @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i end - def add_column_options!(sql, options) - if options[:array] || options[:column].try(:array) - sql << '[]' - end - super - end - # Set the authorized user for this session def session_auth=(user) clear_cache! @@ -663,6 +709,10 @@ module ActiveRecord @use_insert_returning end + def valid_type?(type) + !native_database_types[type].nil? + end + protected # Returns the version of the connected PostgreSQL server. @@ -707,7 +757,14 @@ module ActiveRecord # 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] + if OID.registered_type? row['typname'] + # this composite type is explicitly registered + vector = OID::NAMES[row['typname']] + else + # use the default for composite types + vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] + end + OID::TYPE_MAP[row['oid'].to_i] = vector end @@ -720,27 +777,29 @@ module ActiveRecord FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: - def exec_no_cache(sql, binds) - @connection.async_exec(sql) + def exec_no_cache(sql, name, binds) + log(sql, name, binds) { @connection.async_exec(sql) } end - def exec_cache(sql, binds) - stmt_key = prepare_statement sql + def exec_cache(sql, name, binds) + stmt_key = prepare_statement(sql) + + log(sql, name, binds, stmt_key) do + @connection.send_query_prepared(stmt_key, binds.map { |col, val| + type_cast(val, col) + }) + @connection.block + @connection.get_last_result + end + rescue ActiveRecord::StatementInvalid => e + pgerror = e.original_exception - # Clear the queue - @connection.get_last_result - @connection.send_query_prepared(stmt_key, binds.map { |col, val| - type_cast(val, col) - }) - @connection.block - @connection.get_last_result - rescue PGError => e # Get the PG code for the failure. Annoyingly, the code for # 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 begin - code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) rescue raise e end @@ -765,6 +824,8 @@ module ActiveRecord unless @statements.key? sql_key nextkey = @statements.next_key @connection.prepare nextkey, sql + # Clear the queue + @connection.get_last_result @statements[sql_key] = nextkey end @statements[sql_key] @@ -894,8 +955,8 @@ module ActiveRecord $1.strip if $1 end - def create_table_definition - TableDefinition.new(self) + def create_table_definition(name, temporary, options) + TableDefinition.new native_database_types, name, temporary, options end def update_table_definition(table_name, base) diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 5839d1d3b4..e5c9f6f54a 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,7 +1,8 @@ + module ActiveRecord module ConnectionAdapters class SchemaCache - attr_reader :primary_keys, :tables, :version + attr_reader :version attr_accessor :connection def initialize(conn) @@ -14,6 +15,10 @@ module ActiveRecord prepare_default_proc end + def primary_keys(table_name) + @primary_keys[table_name] + end + # A cached lookup for table existence. def table_exists?(name) return @tables[name] if @tables.key? name @@ -30,23 +35,19 @@ module ActiveRecord end end + def tables(name) + @tables[name] + end + # Get the columns for a table - def columns(table = nil) - if table - @columns[table] - else - @columns - end + def columns(table) + @columns[table] end # Get the columns for a table as a hash, key is the column name # value is the column object. - def columns_hash(table = nil) - if table - @columns_hash[table] - else - @columns_hash - end + def columns_hash(table) + @columns_hash[table] end # Clears out internal caches @@ -58,6 +59,12 @@ module ActiveRecord @version = nil end + def size + [@columns, @columns_hash, @primary_keys, @tables].map { |x| + x.size + }.inject :+ + end + # Clear out internal caches for table with +table_name+. def clear_table_cache!(table_name) @columns.delete table_name @@ -69,9 +76,9 @@ module ActiveRecord 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 + [@version] + [@columns, @columns_hash, @primary_keys, @tables].map { |val| + Hash[val] + } end def marshal_load(array) @@ -87,7 +94,7 @@ module ActiveRecord end @columns_hash.default_proc = Proc.new do |h, table_name| - h[table_name] = Hash[columns[table_name].map { |col| + h[table_name] = Hash[columns(table_name).map { |col| [col.name, col] }] end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 84fa1c7d5a..e5ad08b6b0 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -6,9 +6,9 @@ gem 'sqlite3', '~> 1.3.6' require 'sqlite3' module ActiveRecord - module ConnectionHandling + module ConnectionHandling # :nodoc: # sqlite3 adapter reuses sqlite_connection. - def sqlite3_connection(config) # :nodoc: + def sqlite3_connection(config) # Require database. unless config[:database] raise ArgumentError, "No database file specified. Missing argument: database" @@ -17,12 +17,14 @@ module ActiveRecord # Allow database path relative to Rails.root, but only if # the database path is not the special path that tells # Sqlite to build a database only in memory. - if defined?(Rails.root) && ':memory:' != config[:database] - config[:database] = File.expand_path(config[:database], Rails.root) + if ':memory:' != config[:database] + config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root) + dirname = File.dirname(config[:database]) + Dir.mkdir(dirname) unless File.directory?(dirname) end db = SQLite3::Database.new( - config[:database], + config[:database].to_s, :results_as_hash => true ) @@ -51,6 +53,23 @@ module ActiveRecord # # * <tt>:database</tt> - Path to the database file. class SQLite3Adapter < AbstractAdapter + include Savepoints + + NATIVE_DATABASE_TYPES = { + primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL', + 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" } + } + class Version include Comparable @@ -111,6 +130,7 @@ module ActiveRecord @config = config if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) + @prepared_statements = true @visitor = Arel::Visitors::SQLite.new self else @visitor = unprepared_visitor @@ -178,11 +198,6 @@ module ActiveRecord true end - # Returns true - def supports_autoincrement? #:nodoc: - true - end - def supports_index_sort_order? true end @@ -195,20 +210,7 @@ module ActiveRecord 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" } - } + NATIVE_DATABASE_TYPES end # Returns the current database encoding format as a string, eg: 'UTF-8' @@ -224,8 +226,8 @@ module ActiveRecord # 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] + if value.kind_of?(String) && column && column.type == :binary + s = value.unpack("H*")[0] "x'#{s}'" else super @@ -291,8 +293,8 @@ module ActiveRecord def exec_query(sql, name = nil, binds = []) log(sql, name, binds) do - # Don't cache statements without bind values - if binds.empty? + # Don't cache statements if they are not prepared + if without_prepared_statement?(binds) stmt = @connection.prepare(sql) cols = stmt.columns records = stmt.to_a @@ -348,18 +350,6 @@ module ActiveRecord 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 @@ -458,7 +448,7 @@ module ActiveRecord def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc: alter_table(table_name) do |definition| - definition.columns.delete(definition[column_name]) + definition.remove_column column_name end end @@ -583,9 +573,17 @@ module ActiveRecord quoted_columns = columns.map { |col| quote_column_name(col) } * ',' quoted_to = quote_table_name(to) + + raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }] + 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]]} * ', ' + + column_values = columns.map do |col| + quote(row[column_mappings[col]], raw_column_mappings[col]) + end + + sql << column_values * ', ' sql << ')' exec_query sql end @@ -595,14 +593,6 @@ module ActiveRecord @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/ diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index d6d998c7be..a1943dfcb0 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -15,15 +15,15 @@ module ActiveRecord # Example for SQLite database: # # ActiveRecord::Base.establish_connection( - # adapter: "sqlite", - # database: "path/to/dbfile" + # adapter: "sqlite", + # database: "path/to/dbfile" # ) # # Also accepts keys as strings (for parsing from YAML for example): # # ActiveRecord::Base.establish_connection( - # "adapter" => "sqlite", - # "database" => "path/to/dbfile" + # "adapter" => "sqlite", + # "database" => "path/to/dbfile" # ) # # Or a URL: @@ -54,11 +54,11 @@ module ActiveRecord end def connection_id - Thread.current['ActiveRecord::Base.connection_id'] + ActiveRecord::RuntimeRegistry.connection_id end def connection_id=(connection_id) - Thread.current['ActiveRecord::Base.connection_id'] = connection_id + ActiveRecord::RuntimeRegistry.connection_id = connection_id end # Returns the configuration of the associated connection as a hash: @@ -79,7 +79,7 @@ module ActiveRecord connection_handler.retrieve_connection(self) end - # Returns true if Active Record is connected. + # Returns +true+ if Active Record is connected. def connected? connection_handler.connected?(self) end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 72371be657..366ebde418 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -69,25 +69,27 @@ module ActiveRecord mattr_accessor :timestamped_migrations, instance_writer: false self.timestamped_migrations = true - class_attribute :connection_handler, instance_writer: false - self.connection_handler = ConnectionAdapters::ConnectionHandler.new - end + def self.disable_implicit_join_references=(value) + ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \ + "Make sure to remove this configuration because it does nothing.") + end - module ClassMethods - def inherited(child_class) #:nodoc: - child_class.initialize_generated_modules - super + class_attribute :default_connection_handler, instance_writer: false + + def self.connection_handler + ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler end - def initialize_generated_modules - @attribute_methods_mutex = Mutex.new + def self.connection_handler=(handler) + ActiveRecord::RuntimeRegistry.connection_handler = handler + end - # force attribute methods to be higher in inheritance hierarchy than other generated methods - generated_attribute_methods.const_set(:AttrNames, Module.new { - def self.const_missing(name) - const_set(name, [name.to_s.sub(/ATTR_/, '')].pack('h*').freeze) - end - }) + self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new + end + + module ClassMethods + def initialize_generated_modules + super generated_feature_methods end @@ -106,6 +108,8 @@ module ActiveRecord super elsif abstract_class? "#{super}(abstract)" + elsif !connected? + "#{super}(no database connection)" elsif table_exists? attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', ' "#{super}(#{attr_list})" @@ -130,19 +134,18 @@ module ActiveRecord # Returns the Arel engine. def arel_engine - @arel_engine ||= begin + @arel_engine ||= if Base == self || connection_handler.retrieve_connection_pool(self) self else superclass.arel_engine end - end end private def relation #:nodoc: - relation = Relation.new(self, arel_table) + relation = Relation.create(self, arel_table) if finder_needs_type_condition? relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) @@ -160,18 +163,22 @@ module ActiveRecord # ==== Example: # # Instantiates a single new object # User.new(first_name: 'Jamie') - def initialize(attributes = nil) + def initialize(attributes = nil, options = {}) 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 + @column_types_override = nil + @column_types = self.class.column_types init_internals + init_changed_attributes ensure_proper_type populate_with_current_scope_attributes - assign_attributes(attributes) if attributes + # +options+ argument is only needed to make protected_attributes gem easier to hook. + # Remove it when we drop support to this gem. + init_attributes(attributes, options) if attributes yield self if block_given? run_callbacks :initialize unless _initialize_callbacks.empty? @@ -189,7 +196,8 @@ 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'] || {}) + @column_types_override = coder['column_types'] + @column_types = self.class.column_types init_internals @@ -238,9 +246,7 @@ module ActiveRecord run_callbacks(:initialize) unless _initialize_callbacks.empty? @changed_attributes = {} - self.class.column_defaults.each do |attr, orig_value| - @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) - end + init_changed_attributes @aggregation_cache = {} @association_cache = {} @@ -280,7 +286,7 @@ module ActiveRecord def ==(comparison_object) super || comparison_object.instance_of?(self.class) && - id.present? && + id && comparison_object.id == id end alias :eql? :== @@ -291,9 +297,11 @@ module ActiveRecord id.hash end - # Freeze the attributes hash such that associations are still accessible, even on destroyed records. + # Clone and freeze the attributes hash such that associations are still + # accessible, even on destroyed records, but cloned models will not be + # frozen. def freeze - @attributes.freeze + @attributes = @attributes.clone.freeze self end @@ -302,13 +310,6 @@ module ActiveRecord @attributes.frozen? end - # Allows sort on objects - def <=>(other_object) - if other_object.is_a?(self.class) - self.to_key <=> other_object.to_key - end - end - # Returns +true+ if the record is read only. Records loaded through joins with piggy-back # attributes will be marked as read only since they cannot be saved. def readonly? @@ -320,17 +321,15 @@ module ActiveRecord @readonly = true end - # Returns the connection currently associated with the class. This can - # also be used to "borrow" the connection to do database work that isn't - # easily done without going straight to SQL. - def connection - ActiveSupport::Deprecation.warn("#connection is deprecated in favour of accessing it via the class") - self.class.connection + def connection_handler + self.class.connection_handler end # Returns the contents of the record as a nicely formatted string. def inspect - inspection = if @attributes + # We check defined?(@attributes) not to issue warnings if the object is + # allocated but not initialized. + inspection = if defined?(@attributes) && @attributes self.class.column_names.collect { |name| if has_attribute?(name) "#{name}: #{attribute_for_inspect(name)}" @@ -344,7 +343,7 @@ module ActiveRecord # 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 + Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access end def set_transaction_state(state) # :nodoc: @@ -414,16 +413,30 @@ module ActiveRecord @aggregation_cache = {} @association_cache = {} @attributes_cache = {} - @previously_changed = {} - @changed_attributes = {} @readonly = false @destroyed = false @marked_for_destruction = false + @destroyed_by_association = nil @new_record = true @txn = nil @_start_transaction_state = {} @transaction_state = nil @reflects_state = [false] end + + def init_changed_attributes + # Intentionally avoid using #column_defaults since overridden defaults (as is done in + # optimistic locking) won't get written unless they get marked as changed + self.class.columns.each do |c| + attr, orig_value = c.name, c.default + changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) + end + end + + # This method is needed to make protected_attributes gem easier to hook. + # Remove it when we drop support to this gem. + def init_attributes(attributes, options) + assign_attributes(attributes) + end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 81f92db271..e1faadf1ab 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -11,7 +11,7 @@ module ActiveRecord # ==== Parameters # # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more counter names to reset + # * +counters+ - One or more association counters to reset # # ==== Examples # @@ -21,6 +21,7 @@ module ActiveRecord object = find(id) counters.each do |association| has_many_association = reflect_on_association(association.to_sym) + raise ArgumentError, "'#{self.name}' has no association called '#{association}'" unless has_many_association if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection has_many_association = has_many_association.through_reflection @@ -49,7 +50,7 @@ module ActiveRecord # ==== 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 + # * +counters+ - A Hash containing the names of the fields # to update as keys and the amount to update the field by as values. # # ==== Examples diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 3bac31c6aa..e650ebcf64 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -35,7 +35,7 @@ module ActiveRecord end def pattern - /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/ + @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/ end def prefix diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index c615d59725..7e38719811 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -57,26 +57,23 @@ module ActiveRecord 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). + # Superclass for all database execution errors. + # + # Wraps the underlying database error as +original_exception+. class StatementInvalid < ActiveRecordError - end - - # Raised when SQL statement is invalid and the application gets a blank result. - class ThrowResult < ActiveRecordError - end - - # Parent class for all specific exceptions which wrap database driver exceptions - # provides access to the original exception also. - class WrappedDatabaseException < StatementInvalid attr_reader :original_exception - def initialize(message, original_exception) + def initialize(message, original_exception = nil) super(message) @original_exception = original_exception end end + # Defunct wrapper class kept for compatibility. + # +StatementInvalid+ wraps the original exception now. + class WrappedDatabaseException < StatementInvalid + end + # Raised when a record cannot be inserted because it would violate a uniqueness constraint. class RecordNotUnique < WrappedDatabaseException end @@ -158,6 +155,15 @@ module ActiveRecord # Raised when unknown attributes are supplied via mass assignment. class UnknownAttributeError < NoMethodError + + attr_reader :record, :attribute + + def initialize(record, attribute) + @record = record + @attribute = attribute.to_s + super("unknown attribute: #{attribute}") + end + end # Raised when an error occurred while doing a mass assignment to an attribute through the diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 3135465dfe..e65dab07ba 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,22 +1,22 @@ require 'active_support/lazy_load_hooks' +require 'active_record/explain_registry' module ActiveRecord module Explain - # Relation#explain needs to be able to collect the queries. + # Executes the block with the collect flag enabled. Queries are collected + # asynchronously by the subscriber and returned. def collecting_queries_for_explain # :nodoc: - current = Thread.current - original, current[:available_queries_for_explain] = current[:available_queries_for_explain], [] + ExplainRegistry.collect = true yield - return current[:available_queries_for_explain] + ExplainRegistry.queries ensure - # Note that the return value above does not depend on this assigment. - current[:available_queries_for_explain] = original + ExplainRegistry.reset end # 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: - str = queries && queries.map do |sql, bind| + str = queries.map do |sql, bind| [].tap do |msg| msg << "EXPLAIN for: #{sql}" unless bind.empty? @@ -31,6 +31,7 @@ module ActiveRecord def str.inspect self end + str end end diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb new file mode 100644 index 0000000000..f5cd57e075 --- /dev/null +++ b/activerecord/lib/active_record/explain_registry.rb @@ -0,0 +1,30 @@ +require 'active_support/per_thread_registry' + +module ActiveRecord + # This is a thread locals registry for EXPLAIN. For example + # + # ActiveRecord::ExplainRegistry.queries + # + # returns the collected queries local to the current thread. + # + # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # for further details. + class ExplainRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + attr_accessor :queries, :collect + + def initialize + reset + end + + def collect? + @collect + end + + def reset + @collect = false + @queries = [] + end + end +end diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 0f927496fb..6a49936644 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -1,4 +1,5 @@ require 'active_support/notifications' +require 'active_record/explain_registry' module ActiveRecord class ExplainSubscriber # :nodoc: @@ -7,8 +8,8 @@ module ActiveRecord end def finish(name, id, payload) - if queries = Thread.current[:available_queries_for_explain] - queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload) + if ExplainRegistry.collect? && !ignore_payload?(payload) + ExplainRegistry.queries << payload.values_at(:sql, :binds) end end @@ -18,7 +19,7 @@ module ActiveRecord # On the other hand, we want to monitor the performance of our real database # queries, not the performance of the access to the query cache. IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE) - EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)/i + EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i def ignore_payload?(payload) payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS end diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index 11b53275e1..fbd7a4d891 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -24,7 +24,6 @@ module ActiveRecord rows.each(&block) end - RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] # :nodoc: private def rows @@ -32,7 +31,7 @@ module ActiveRecord begin data = YAML.load(render(IO.read(@file))) - rescue *RESCUE_ERRORS => error + rescue ArgumentError, Psych::SyntaxError => 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 : [] diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 2958d08210..3bb3131bd1 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -379,22 +379,16 @@ module ActiveRecord @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.find_table_name(fixture_set_name) # :nodoc: - ActiveSupport::Deprecation.warn( - "ActiveRecord::Fixtures.find_table_name is deprecated and shall be removed from future releases. Use ActiveRecord::Fixtures.default_fixture_model_name instead.") - default_fixture_model_name(fixture_set_name) - end - - def self.default_fixture_model_name(fixture_set_name) # :nodoc: - ActiveRecord::Base.pluralize_table_names ? + def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: + config.pluralize_table_names ? fixture_set_name.singularize.camelize : fixture_set_name.camelize end - def self.default_fixture_table_name(fixture_set_name) # :nodoc: - "#{ ActiveRecord::Base.table_name_prefix }"\ + def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: + "#{ config.table_name_prefix }"\ "#{ fixture_set_name.tr('/', '_') }"\ - "#{ ActiveRecord::Base.table_name_suffix }".to_sym + "#{ config.table_name_suffix }".to_sym end def self.reset_cache @@ -442,9 +436,47 @@ module ActiveRecord cattr_accessor :all_loaded_fixtures self.all_loaded_fixtures = {} - def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}) + class ClassCache + def initialize(class_names, config) + @class_names = class_names.stringify_keys + @config = config + + # Remove string values that aren't constants or subclasses of AR + @class_names.delete_if { |k,klass| + unless klass.is_a? Class + klass = klass.safe_constantize + ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.") + end + !insert_class(@class_names, k, klass) + } + end + + def [](fs_name) + @class_names.fetch(fs_name) { + klass = default_fixture_model(fs_name, @config).safe_constantize + insert_class(@class_names, fs_name, klass) + } + end + + private + + def insert_class(class_names, name, klass) + # We only want to deal with AR objects. + if klass && klass < ActiveRecord::Base + class_names[name] = klass + else + class_names[name] = nil + end + end + + def default_fixture_model(fs_name, config) + ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config) + end + end + + def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) fixture_set_names = Array(fixture_set_names).map(&:to_s) - class_names = class_names.stringify_keys + class_names = ClassCache.new class_names, config # FIXME: Apparently JK uses this. connection = block_given? ? yield : ActiveRecord::Base.connection @@ -458,10 +490,12 @@ module ActiveRecord fixtures_map = {} fixture_sets = files_to_read.map do |fs_name| + klass = class_names[fs_name] + conn = klass ? klass.connection : connection fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new - connection, + conn, fs_name, - class_names[fs_name] || default_fixture_model_name(fs_name), + klass, ::File.join(fixtures_directory, fs_name)) end @@ -503,27 +537,31 @@ module ActiveRecord Zlib.crc32(label.to_s) % MAX_ID end - attr_reader :table_name, :name, :fixtures, :model_class + attr_reader :table_name, :name, :fixtures, :model_class, :config - def initialize(connection, name, class_name, path) - @fixtures = {} # Ordered hash + def initialize(connection, name, class_name, path, config = ActiveRecord::Base) @name = name @path = path + @config = config + @model_class = nil + + if class_name.is_a?(String) + ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.") + end 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 + @model_class = class_name.safe_constantize if class_name end - @connection = ( model_class.respond_to?(:connection) ? - model_class.connection : connection ) + @connection = connection @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : - self.class.default_fixture_table_name(name) ) + self.class.default_fixture_table_name(name, config) ) - read_fixture_files + @fixtures = read_fixture_files path, @model_class end def [](x) @@ -545,7 +583,7 @@ module ActiveRecord # Return a hash of rows to be inserted. The key is the table, the value is # a list of rows to insert to that table. def table_rows - now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now + now = config.default_timezone == :utc ? Time.now.utc : Time.now now = now.to_s(:db) # allow a standard key to be used for doing defaults in YAML @@ -557,7 +595,7 @@ module ActiveRecord rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash - if model_class && model_class < ActiveRecord::Base + if model_class # 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 |c_name| @@ -567,7 +605,7 @@ module ActiveRecord # interpolate the fixture label row.each do |key, value| - row[key] = label if value == "$LABEL" + row[key] = label if "$LABEL" == value end # generate a primary key if necessary @@ -597,14 +635,9 @@ module ActiveRecord row[fk_name] = ActiveRecord::FixtureSet.identify(value) end - 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.join_table - rows[table_name].concat targets.map { |target| - { association.foreign_key => row[primary_key_name], - association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) } - } + when :has_many + if association.options[:through] + add_join_records(rows, row, HasManyThroughProxy.new(association)) end end end @@ -615,11 +648,50 @@ module ActiveRecord rows end + class ReflectionProxy # :nodoc: + def initialize(association) + @association = association + end + + def join_table + @association.join_table + end + + def name + @association.name + end + end + + class HasManyThroughProxy < ReflectionProxy # :nodoc: + def rhs_key + @association.foreign_key + end + + def lhs_key + @association.through_reflection.foreign_key + end + end + private def primary_key_name @primary_key_name ||= model_class && model_class.primary_key end + def add_join_records(rows, row, association) + # This is the case when the join table has no fixtures file + if (targets = row.delete(association.name.to_s)) + table_name = association.join_table + lhs_key = association.lhs_key + rhs_key = association.rhs_key + + targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) + rows[table_name].concat targets.map { |target| + { lhs_key => row[primary_key_name], + rhs_key => ActiveRecord::FixtureSet.identify(target) } + } + end + end + def has_primary_key_column? @has_primary_key_column ||= primary_key_name && model_class.columns.any? { |c| c.name == primary_key_name } @@ -638,12 +710,12 @@ module ActiveRecord @column_names ||= @connection.columns(@table_name).collect { |c| c.name } end - def read_fixture_files - yaml_files = Dir["#{@path}/**/*.yml"].select { |f| + def read_fixture_files(path, model_class) + yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f| ::File.file?(f) - } + [yaml_file_path] + } + [yaml_file_path(path)] - yaml_files.each do |file| + yaml_files.each_with_object({}) do |file, fixtures| FixtureSet::File.open(file) do |fh| fh.each do |fixture_name, row| fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) @@ -652,8 +724,8 @@ module ActiveRecord end end - def yaml_file_path - "#{@path}.yml" + def yaml_file_path(path) + "#{path}.yml" end end @@ -708,24 +780,33 @@ module ActiveRecord module TestFixtures extend ActiveSupport::Concern - included do - setup :setup_fixtures - teardown :teardown_fixtures + def before_setup + setup_fixtures + super + end + + def after_teardown + super + teardown_fixtures + end - class_attribute :fixture_path + included do + class_attribute :fixture_path, :instance_writer => false 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 :pre_loaded_fixtures + class_attribute :config self.fixture_table_names = [] self.use_transactional_fixtures = true self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false + self.config = ActiveRecord::Base self.fixture_class_names = Hash.new do |h, fixture_set_name| - h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name) + h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config) end end @@ -738,35 +819,27 @@ module ActiveRecord # 'namespaced/fixture' => Another::Model # # The keys must be the fixture names, that coincide with the short paths to the fixture files. - #-- - # It is also possible to pass the class name instead of the class: - # set_fixture_class 'some_fixture' => 'SomeModel' - # I think this option is redundant, i propose to deprecate it. - # Isn't it easier to always pass the class itself? - # (2011-12-20 alexeymuranov) - #++ def set_fixture_class(class_names = {}) self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) end def fixtures(*fixture_set_names) if fixture_set_names.first == :all - fixture_set_names = Dir["#{fixture_path}/**/*.{yml}"] - fixture_set_names.map! { |f| f[(fixture_path.size + 1)..-5] } + fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"] + fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] } else fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } end self.fixture_table_names |= fixture_set_names - require_fixture_classes(fixture_set_names) + require_fixture_classes(fixture_set_names, self.config) setup_fixture_accessors(fixture_set_names) end def try_to_load_dependency(file_name) require_dependency file_name rescue LoadError => e - # Let's hope the developer has included it himself - + # Let's hope the developer has included it # Let's warn in case this is a subdependency, otherwise # subdependency error messages are totally cryptic if ActiveRecord::Base.logger @@ -774,7 +847,7 @@ module ActiveRecord end end - def require_fixture_classes(fixture_set_names = nil) + def require_fixture_classes(fixture_set_names = nil, config = ActiveRecord::Base) if fixture_set_names fixture_set_names = fixture_set_names.map { |n| n.to_s } else @@ -782,7 +855,7 @@ module ActiveRecord end fixture_set_names.each do |file_name| - file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names + file_name = file_name.singularize if config.pluralize_table_names try_to_load_dependency(file_name) end end @@ -834,9 +907,7 @@ module ActiveRecord !self.class.uses_transaction?(method_name) end - def setup_fixtures - return if ActiveRecord::Base.configurations.blank? - + def setup_fixtures(config = ActiveRecord::Base) if pre_loaded_fixtures && !use_transactional_fixtures raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' end @@ -850,7 +921,7 @@ module ActiveRecord if @@already_loaded_fixtures[self.class] @loaded_fixtures = @@already_loaded_fixtures[self.class] else - @loaded_fixtures = load_fixtures + @loaded_fixtures = load_fixtures(config) @@already_loaded_fixtures[self.class] = @loaded_fixtures end @fixture_connections = enlist_fixture_connections @@ -861,16 +932,14 @@ module ActiveRecord else ActiveRecord::FixtureSet.reset_cache @@already_loaded_fixtures[self.class] = nil - @loaded_fixtures = load_fixtures + @loaded_fixtures = load_fixtures(config) end # Instantiate fixtures for every test if requested. - instantiate_fixtures if use_instantiated_fixtures + instantiate_fixtures(config) if use_instantiated_fixtures end def teardown_fixtures - return if ActiveRecord::Base.configurations.blank? - # Rollback changes if a transaction is active. if run_in_transaction? @fixture_connections.each do |connection| @@ -889,19 +958,19 @@ module ActiveRecord end private - def load_fixtures - fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) + def load_fixtures(config) + fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config) Hash[fixtures.map { |f| [f.name, f] }] end # for pre_loaded_fixtures, only require the classes once. huge speed improvement @@required_fixture_classes = false - def instantiate_fixtures + def instantiate_fixtures(config) if pre_loaded_fixtures raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty? unless @@required_fixture_classes - self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys + self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys, config @@required_fixture_classes = true end ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?) diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index f54865c86e..7e1e120288 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -5,7 +5,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - # Determine whether to store the full constant name including namespace when using STI + # Determines whether to store the full constant name including namespace when using STI. class_attribute :store_full_sti_class, instance_writer: false self.store_full_sti_class = true end @@ -13,7 +13,7 @@ module ActiveRecord module ClassMethods # Determines if one of the attributes passed in is the inheritance column, # and if the inheritance column is attr accessible, it initializes an - # instance of the given subclass instead of the base class + # instance of the given subclass instead of the base class. def new(*args, &block) if abstract_class? || self == Base raise NotImplementedError, "#{self} is an abstract class and can not be instantiated." @@ -27,7 +27,8 @@ module ActiveRecord super end - # True if this isn't a concrete subclass needing a STI type condition. + # Returns +true+ if this does not need STI type condition. Returns + # +false+ if STI type condition needs to be applied. def descends_from_active_record? if self == Base false @@ -116,9 +117,10 @@ module ActiveRecord begin constant = ActiveSupport::Dependencies.constantize(candidate) return constant if candidate == constant.to_s - rescue NameError => e - # We don't want to swallow NoMethodError < NameError errors - raise e unless e.instance_of?(NameError) + # We don't want to swallow NoMethodError < NameError errors + rescue NoMethodError + raise + rescue NameError end end @@ -158,7 +160,7 @@ module ActiveRecord end def type_condition(table = arel_table) - sti_column = table[inheritance_column.to_sym] + sti_column = table[inheritance_column] sti_names = ([self] + descendants).map { |model| model.sti_name } sti_column.in(sti_names) @@ -174,7 +176,7 @@ module ActiveRecord if subclass_name.present? && subclass_name != self.name subclass = subclass_name.safe_constantize - unless subclasses.include?(subclass) + unless descendants.include?(subclass) raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}") end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 48c73d7781..2589b2f3da 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -21,7 +21,7 @@ module ActiveRecord # <tt>resources :users</tt> route. Normally, +user_path+ will # construct a path with the user object's 'id' in it: # - # user = User.find_by_name('Phusion') + # user = User.find_by(name: 'Phusion') # user_path(user) # => "/users/1" # # You can override +to_param+ in your model to make +user_path+ construct @@ -33,7 +33,7 @@ module ActiveRecord # end # end # - # user = User.find_by_name('Phusion') + # user = User.find_by(name: 'Phusion') # user_path(user) # => "/users/Phusion" def to_param # We can't use alias_method here, because method 'id' optimizes itself on the fly. diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 209de78898..55776a91c0 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -82,7 +82,7 @@ module ActiveRecord stmt = relation.where( relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(self.class.quote_value(previous_lock_value)) + relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) ) ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names)) @@ -138,6 +138,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) + @column_defaults = nil @locking_column = value.to_s end @@ -149,6 +150,7 @@ module ActiveRecord # Quote the column name used for optimistic locking. def quoted_locking_column + ActiveSupport::Deprecation.warn "ActiveRecord::Base.quoted_locking_column is deprecated and will be removed in Rails 4.2 or later." connection.quote_column_name(locking_column) end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 8e4ddcac82..ff7102d35b 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -3,12 +3,12 @@ module ActiveRecord # Locking::Pessimistic provides support for row-level locking using # SELECT ... FOR UPDATE and other lock types. # - # Pass <tt>lock: true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive + # Chain <tt>ActiveRecord::Base#find</tt> to <tt>ActiveRecord::QueryMethods#lock</tt> to obtain an exclusive # lock on the selected rows: # # select * from accounts where id=1 for update - # Account.find(1, lock: true) + # Account.lock.find(1) # - # Pass <tt>lock: 'some locking clause'</tt> to give a database-specific locking clause + # Call <tt>lock('some locking clause')</tt> to use a database-specific locking clause # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example: # # Account.transaction do @@ -64,7 +64,7 @@ module ActiveRecord end # Wraps the passed block in a transaction, locking the object - # before yielding. You pass can the SQL locking clause + # before yielding. You can pass the SQL locking clause # as argument (see <tt>lock!</tt>). def with_lock(lock = true) transaction do diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index c1ba524c84..927fbab8f0 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -3,11 +3,11 @@ module ActiveRecord IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"] def self.runtime=(value) - Thread.current[:active_record_sql_runtime] = value + ActiveRecord::RuntimeRegistry.sql_runtime = value end def self.runtime - Thread.current[:active_record_sql_runtime] ||= 0 + ActiveRecord::RuntimeRegistry.sql_runtime ||= 0 end def self.reset_runtime @@ -17,7 +17,7 @@ module ActiveRecord def initialize super - @odd_or_even = false + @odd = false end def render_bind(column, value) @@ -41,7 +41,7 @@ module ActiveRecord return if IGNORE_PAYLOAD_NAMES.include?(payload[:name]) name = "#{payload[:name]} (#{event.duration.round(1)}ms)" - sql = payload[:sql].squeeze(' ') + sql = payload[:sql] binds = nil unless (payload[:binds] || []).empty? @@ -60,17 +60,8 @@ module ActiveRecord debug " #{name} #{sql}#{binds}" end - def identity(event) - return unless logger.debug? - - name = color(event.payload[:name], odd? ? CYAN : MAGENTA, true) - line = odd? ? color(event.payload[:line], nil, true) : event.payload[:line] - - debug " #{name} #{line}" - end - def odd? - @odd_or_even = !@odd_or_even + @odd = !@odd end def logger diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 5d7762ec3a..5224a6b67c 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -32,7 +32,7 @@ module ActiveRecord class PendingMigrationError < ActiveRecordError#:nodoc: def initialize - super("Migrations are pending; run 'rake db:migrate RAILS_ENV=#{Rails.env}' to resolve this issue.") + super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.") end end @@ -102,7 +102,7 @@ module ActiveRecord # table definition. # * <tt>drop_table(name)</tt>: Drops the table called +name+. # * <tt>change_table(name, options)</tt>: Allows to make column alterations to - # the table called +name+. It makes the table object availabe to a block that + # the table called +name+. It makes the table object available to a block that # can then add/remove columns, indexes or foreign keys to it. # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ # to +new_name+. @@ -120,8 +120,8 @@ module ActiveRecord # a column but keeps the type and content. # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes # the column to a different type using the same parameters as add_column. - # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in - # +column_names+ from the table called +table_name+. + # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column + # named +column_name+ from the table called +table_name+. # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index # with the name of the column. Other options include # <tt>:name</tt>, <tt>:unique</tt> (e.g. @@ -357,11 +357,14 @@ module ActiveRecord class CheckPending def initialize(app) @app = app + @last_check = 0 end def call(env) - ActiveRecord::Base.logger.silence do + mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + if @last_check < mtime ActiveRecord::Migration.check_pending! + @last_check = mtime end @app.call(env) end @@ -370,23 +373,23 @@ module ActiveRecord class << self attr_accessor :delegate # :nodoc: attr_accessor :disable_ddl_transaction # :nodoc: - end - def self.check_pending! - raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration? - end + def 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 + def method_missing(name, *args, &block) # :nodoc: + (delegate || superclass.delegate).send(name, *args, &block) + end - def self.migrate(direction) - new.migrate direction - end + def migrate(direction) + new.migrate direction + end - # Disable DDL transactions for this migration. - def self.disable_ddl_transaction! - @disable_ddl_transaction = true + # Disable DDL transactions for this migration. + def disable_ddl_transaction! + @disable_ddl_transaction = true + end end def disable_ddl_transaction # :nodoc: @@ -466,7 +469,7 @@ module ActiveRecord @connection.respond_to?(:reverting) && @connection.reverting end - class ReversibleBlockHelper < Struct.new(:reverting) + class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc: def up yield unless reverting end @@ -614,8 +617,8 @@ module ActiveRecord say_with_time "#{method}(#{arg_list})" do unless @connection.respond_to? :revert unless arguments.empty? || method == :execute - arguments[0] = Migrator.proper_table_name(arguments.first) - arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table + arguments[0] = proper_table_name(arguments.first, table_name_options) + arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table end end return super unless connection.respond_to?(method) @@ -626,7 +629,7 @@ module ActiveRecord def copy(destination, sources, options = {}) copied = [] - FileUtils.mkdir_p(destination) unless File.exists?(destination) + FileUtils.mkdir_p(destination) unless File.exist?(destination) destination_migrations = ActiveRecord::Migrator.migrations(destination) last = destination_migrations.last @@ -668,6 +671,18 @@ module ActiveRecord copied end + # Finds the correct table name given an Active Record object. + # Uses the Active Record object's own table_name, or pre/suffix from the + # options passed in. + def proper_table_name(name, options = {}) + if name.respond_to? :table_name + name.table_name + else + "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}" + end + end + + # Determines the version number of the next migration. def next_migration_number(number) if ActiveRecord::Base.timestamped_migrations [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max @@ -676,6 +691,13 @@ module ActiveRecord end end + def table_name_options(config = ActiveRecord::Base) + { + table_name_prefix: config.table_name_prefix, + table_name_suffix: config.table_name_suffix + } + end + private def execute_block if connection.respond_to? :execute_block @@ -699,6 +721,10 @@ module ActiveRecord File.basename(filename) end + def mtime + File.mtime filename + end + delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration private @@ -714,6 +740,16 @@ module ActiveRecord end + class NullMigration < MigrationProxy #:nodoc: + def initialize + super(nil, 0, nil, nil) + end + + def mtime + 0 + end + end + class Migrator#:nodoc: class << self attr_writer :migrations_paths @@ -784,15 +820,23 @@ module ActiveRecord end def last_version - migrations(migrations_paths).last.try(:version)||0 + last_migration.version + end + + def last_migration #:nodoc: + migrations(migrations_paths).last || NullMigration.new 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 + def proper_table_name(name, options = {}) + ActiveSupport::Deprecation.warn "ActiveRecord::Migrator.proper_table_name is deprecated and will be removed in Rails 4.2. Use the proper_table_name instance method on ActiveRecord::Migration instead" + options = { + table_name_prefix: ActiveRecord::Base.table_name_prefix, + table_name_suffix: ActiveRecord::Base.table_name_suffix + }.merge(options) if name.respond_to? :table_name name.table_name else - "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" + "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}" end end @@ -844,13 +888,7 @@ module ActiveRecord @direction = direction @target_version = target_version @migrated_versions = nil - - if Array(migrations).grep(String).empty? - @migrations = migrations - else - ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations" - @migrations = self.class.migrations(migrations) - end + @migrations = migrations validate(@migrations) @@ -858,7 +896,7 @@ module ActiveRecord end def current_version - migrated.sort.last || 0 + migrated.max || 0 end def current_migration @@ -867,11 +905,15 @@ module ActiveRecord alias :current :current_migration def run - target = migrations.detect { |m| m.version == @target_version } - raise UnknownMigrationVersionError.new(@target_version) if target.nil? - unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i)) - target.migrate(@direction) - record_version_state_after_migrating(target.version) + migration = migrations.detect { |m| m.version == @target_version } + raise UnknownMigrationVersionError.new(@target_version) if migration.nil? + unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i)) + begin + execute_migration_in_transaction(migration, @direction) + rescue => e + canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : "" + raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace + end end end @@ -880,22 +922,11 @@ module ActiveRecord raise UnknownMigrationVersionError.new(@target_version) end - running = runnable - - if block_given? - message = "block argument to migrate is deprecated, please filter migrations before constructing the migrator" - ActiveSupport::Deprecation.warn message - running.select! { |m| yield m } - end - - running.each do |migration| + runnable.each do |migration| Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger begin - ddl_transaction(migration) do - migration.migrate(@direction) - record_version_state_after_migrating(migration.version) - end + execute_migration_in_transaction(migration, @direction) rescue => e canceled_msg = use_transaction?(migration) ? "this and " : "" raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace @@ -932,6 +963,13 @@ module ActiveRecord migrated.include?(migration.version.to_i) end + def execute_migration_in_transaction(migration, direction) + ddl_transaction(migration) do + migration.migrate(direction) + record_version_state_after_migrating(migration.version) + end + end + def target migrations.detect { |m| m.version == @target_version } end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 79c55045ba..01c73be849 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -73,7 +73,7 @@ module ActiveRecord [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column_default, :add_reference, :remove_reference, :transaction, - :drop_join_table, :drop_table, :execute_block, + :drop_join_table, :drop_table, :execute_block, :enable_extension, :change_column, :execute, :remove_columns, # irreversible methods need to be here too ].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 @@ -100,6 +100,7 @@ module ActiveRecord add_column: :remove_column, add_timestamps: :remove_timestamps, add_reference: :remove_reference, + enable_extension: :disable_extension }.each do |cmd, inv| [[inv, cmd], [cmd, inv]].uniq.each do |method, inverse| class_eval <<-EOV, __FILE__, __LINE__ + 1 @@ -144,7 +145,10 @@ module ActiveRecord def invert_remove_index(args) table, options = *args - raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." unless options && options[:column] + + unless options && options.is_a?(Hash) && options[:column] + raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." + end options = options.dup [:add_index, [table, options.delete(:column), options]] diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 85fb4be992..dc5ff02882 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -34,6 +34,12 @@ module ActiveRecord ## # :singleton-method: + # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations" + class_attribute :schema_migrations_table_name, instance_accessor: false + self.schema_migrations_table_name = "schema_migrations" + + ## + # :singleton-method: # Indicates whether table names should be the pluralized versions of the corresponding class names. # 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. @@ -124,7 +130,7 @@ module ActiveRecord @quoted_table_name = nil @arel_table = nil @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name - @relation = Relation.new(self, arel_table) + @relation = Relation.create(self, arel_table) end # Returns a quoted version of the table name, used to construct SQL statements. @@ -205,7 +211,7 @@ module ActiveRecord # Returns an array of column objects for the table associated with this class. def columns - @columns ||= connection.schema_cache.columns[table_name].map do |col| + @columns ||= connection.schema_cache.columns(table_name).map do |col| col = col.dup col.primary = (col.name == primary_key) col @@ -224,13 +230,20 @@ module ActiveRecord def decorate_columns(columns_hash) # :nodoc: return if columns_hash.empty? - columns_hash.each do |name, col| - if serialized_attributes.key?(name) - columns_hash[name] = AttributeMethods::Serialization::Type.new(col) - end - if create_time_zone_conversion_attribute?(name, col) - columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(col) - end + @serialized_column_names ||= self.columns_hash.keys.find_all do |name| + serialized_attributes.key?(name) + end + + @serialized_column_names.each do |name| + columns_hash[name] = AttributeMethods::Serialization::Type.new(columns_hash[name]) + end + + @time_zone_column_names ||= self.columns_hash.find_all do |name, col| + create_time_zone_conversion_attribute?(name, col) + end.map!(&:first) + + @time_zone_column_names.each do |name| + columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name]) end columns_hash @@ -253,19 +266,6 @@ module ActiveRecord @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } end - # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key - # 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.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 - end - end - # Resets all the cached information about columns, which will cause them # to be reloaded on the next request. # @@ -297,16 +297,19 @@ module ActiveRecord undefine_attribute_methods connection.schema_cache.clear_table_cache!(table_name) if table_exists? - @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 + @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 + @serialized_column_names = nil + @time_zone_column_names = nil + @cached_time_zone = nil end # This is a hook for use by modules that need to do extra stuff to diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 602ab9e2f4..df28451bb7 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -90,8 +90,9 @@ module ActiveRecord # accepts_nested_attributes_for :posts # end # - # You can now set or update attributes on an associated post model through - # the attribute hash. + # You can now set or update attributes on the associated posts through + # an attribute hash for a member: include the key +:posts_attributes+ + # with an array of hashes of post attributes as a value. # # For each hash that does _not_ have an <tt>id</tt> key a new record will # be instantiated, unless the hash also contains a <tt>_destroy</tt> key @@ -114,10 +115,10 @@ module ActiveRecord # hashes if they fail to pass your criteria. For example, the previous # example could be rewritten as: # - # class Member < ActiveRecord::Base - # has_many :posts - # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? } - # end + # class Member < ActiveRecord::Base + # has_many :posts + # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? } + # end # # params = { member: { # name: 'joe', posts_attributes: [ @@ -134,19 +135,19 @@ module ActiveRecord # # Alternatively, :reject_if also accepts a symbol for using methods: # - # class Member < ActiveRecord::Base - # has_many :posts - # accepts_nested_attributes_for :posts, reject_if: :new_record? - # end + # class Member < ActiveRecord::Base + # has_many :posts + # accepts_nested_attributes_for :posts, reject_if: :new_record? + # end # - # class Member < ActiveRecord::Base - # has_many :posts - # accepts_nested_attributes_for :posts, reject_if: :reject_posts + # class Member < ActiveRecord::Base + # has_many :posts + # accepts_nested_attributes_for :posts, reject_if: :reject_posts # - # def reject_posts(attributed) - # attributed['title'].blank? - # end - # end + # def reject_posts(attributed) + # attributed['title'].blank? + # end + # end # # If the hash contains an <tt>id</tt> key that matches an already # associated record, the matching record will be modified: @@ -183,6 +184,29 @@ module ActiveRecord # member.save # member.reload.posts.length # => 1 # + # Nested attributes for an associated collection can also be passed in + # the form of a hash of hashes instead of an array of hashes: + # + # Member.create(name: 'joe', + # posts_attributes: { first: { title: 'Foo' }, + # second: { title: 'Bar' } }) + # + # has the same effect as + # + # Member.create(name: 'joe', + # posts_attributes: [ { title: 'Foo' }, + # { title: 'Bar' } ]) + # + # The keys of the hash which is the value for +:posts_attributes+ are + # ignored in this case. + # However, it is not allowed to use +'id'+ or +:id+ for one of + # such keys, otherwise the hash will be wrapped in an array and + # interpreted as an attribute hash for a single post. + # + # Passing attributes for an associated collection in the form of a hash + # of hashes can be used with hashes generated from HTTP/HTML parameters, + # where there maybe no natural way to submit an array of hashes. + # # === Saving # # All changes to models, including the destruction of those marked for @@ -205,6 +229,27 @@ module ActiveRecord # belongs_to :member, inverse_of: :posts # validates_presence_of :member # end + # + # Note that if you do not specify the <tt>inverse_of</tt> option, then + # Active Record will try to automatically guess the inverse association + # based on heuristics. + # + # For one-to-one nested associations, if you build the new (in-memory) + # child object yourself before assignment, then this module will not + # overwrite it, e.g.: + # + # class Member < ActiveRecord::Base + # has_one :avatar + # accepts_nested_attributes_for :avatar + # + # def avatar + # super || build_avatar(width: 200) + # end + # end + # + # member = Member.new + # member.avatar_attributes = {icon: 'sad'} + # member.avatar.width # => 200 module ClassMethods REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } } @@ -261,7 +306,7 @@ module ActiveRecord attr_names.each do |association_name| if reflection = reflect_on_association(association_name) - reflection.options[:autosave] = true + reflection.autosave = true add_autosave_association_callbacks(reflection) nested_attributes_options = self.nested_attributes_options.dup @@ -332,20 +377,28 @@ module ActiveRecord def assign_nested_attributes_for_one_to_one_association(association_name, attributes) options = self.nested_attributes_options[association_name] attributes = attributes.with_indifferent_access + existing_record = send(association_name) - if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) && - (options[:update_only] || record.id.to_s == attributes['id'].to_s) - assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) + if (options[:update_only] || !attributes['id'].blank?) && existing_record && + (options[:update_only] || existing_record.id.to_s == attributes['id'].to_s) + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) elsif attributes['id'].present? - raise_nested_attributes_record_not_found(association_name, attributes['id']) + raise_nested_attributes_record_not_found!(association_name, attributes['id']) elsif !reject_new_record?(association_name, attributes) - method = "build_#{association_name}" - if respond_to?(method) - send(method, attributes.except(*UNASSIGNABLE_KEYS)) + assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS) + + if existing_record && existing_record.new_record? + existing_record.assign_attributes(assignable_attributes) + association(association_name).initialize_attributes(existing_record) else - raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" + method = "build_#{association_name}" + if respond_to?(method) + send(method, assignable_attributes) + else + raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" + end end end end @@ -412,23 +465,21 @@ module ActiveRecord association.build(attributes.except(*UNASSIGNABLE_KEYS)) end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } - unless association.loaded? || call_reject_if(association_name, attributes) + unless call_reject_if(association_name, attributes) # Make sure we are operating on the actual object which is in the association's # proxy_target array (either by finding it, or adding it if not found) - target_record = association.target.detect { |record| record == existing_record } - + # Take into account that the proxy_target may have changed due to callbacks + target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s } if target_record existing_record = target_record else - association.add_to_target(existing_record) + association.add_to_target(existing_record, :skip_callbacks) end - end - if !call_reject_if(association_name, attributes) assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) end else - raise_nested_attributes_record_not_found(association_name, attributes['id']) + raise_nested_attributes_record_not_found!(association_name, attributes['id']) end end end @@ -490,7 +541,7 @@ module ActiveRecord end end - def raise_nested_attributes_record_not_found(association_name, record_id) + def raise_nested_attributes_record_not_found!(association_name, record_id) raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" end end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 711fc8b883..080b20134d 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -6,7 +6,7 @@ module ActiveRecord @records = [] end - def pluck(_column_name) + def pluck(*column_names) [] end @@ -39,11 +39,7 @@ module ActiveRecord end def to_sql - @to_sql ||= "" - end - - def where_values_hash - {} + "" end def count(*) @@ -54,8 +50,12 @@ module ActiveRecord 0 end - def calculate(_operation, _column_name, _options = {}) - nil + def calculate(_operation, _column_name) + if _operation == :count + 0 + else + nil + end end def exists?(_id = false) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index b25d0601cb..a73a140ef1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -51,7 +51,7 @@ module ActiveRecord # how this "single-table" inheritance mapping is implemented. def instantiate(record, column_types = {}) klass = discriminate_class_for_record(record) - column_types = klass.decorate_columns(column_types) + column_types = klass.decorate_columns(column_types.dup) klass.allocate.init_with('attributes' => record, 'column_types' => column_types) end @@ -99,6 +99,9 @@ module ActiveRecord # <tt>before_*</tt> callbacks return +false+ the action is cancelled and # +save+ returns +false+. See ActiveRecord::Callbacks for further # details. + # + # Attributes marked as readonly are silently ignored if the record is + # being updated. def save(*) create_or_update rescue ActiveRecord::RecordInvalid @@ -118,6 +121,9 @@ module ActiveRecord # the <tt>before_*</tt> callbacks return +false+ the action is cancelled # and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See # ActiveRecord::Callbacks for further details. + # + # Attributes marked as readonly are silently ignored if the record is + # being updated. def save!(*) create_or_update || raise(RecordNotSaved) end @@ -204,6 +210,8 @@ module ActiveRecord # * updated_at/updated_on column is updated if that column is available. # * Updates all the attributes that are dirty in this object. # + # This method raises an +ActiveRecord::ActiveRecordError+ if the + # attribute is marked as readonly. def update_attribute(name, value) name = name.to_s verify_readonly_attribute(name) @@ -325,10 +333,54 @@ module ActiveRecord toggle(attribute).update_attribute(attribute, self[attribute]) end - # Reloads the attributes of this object from the database. - # The optional options argument is passed to find when reloading so you - # may do e.g. record.reload(lock: true) to reload the same record with - # an exclusive row lock. + # Reloads the record from the database. + # + # This method finds record by its primary key (which could be assigned manually) and + # modifies the receiver in-place: + # + # account = Account.new + # # => #<Account id: nil, email: nil> + # account.id = 1 + # account.reload + # # Account Load (1.2ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT 1 [["id", 1]] + # # => #<Account id: 1, email: 'account@example.com'> + # + # Attributes are reloaded from the database, and caches busted, in + # particular the associations cache. + # + # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt> + # is raised. Otherwise, in addition to the in-place modification the method + # returns +self+ for convenience. + # + # The optional <tt>:lock</tt> flag option allows you to lock the reloaded record: + # + # reload(lock: true) # reload with pessimistic locking + # + # Reloading is commonly used in test suites to test something is actually + # written to the database, or when some action modifies the corresponding + # row in the database but not the object in memory: + # + # assert account.deposit!(25) + # assert_equal 25, account.credit # check it is updated in memory + # assert_equal 25, account.reload.credit # check it is also persisted + # + # Another commom use case is optimistic locking handling: + # + # def with_optimistic_retry + # begin + # yield + # rescue ActiveRecord::StaleObjectError + # begin + # # Reload lock_version in particular. + # reload + # rescue ActiveRecord::RecordNotFound + # # If the record is gone there is nothing to do. + # else + # retry + # end + # end + # end + # def reload(options = nil) clear_aggregation_cache clear_association_cache @@ -341,14 +393,16 @@ module ActiveRecord end @attributes.update(fresh_object.instance_variable_get('@attributes')) - @columns_hash = fresh_object.instance_variable_get('@columns_hash') - @attributes_cache = {} + @column_types = self.class.column_types + @column_types_override = fresh_object.instance_variable_get('@column_types_override') + @attributes_cache = {} self end # Saves the record with the updated_at/on attributes set to the current time. - # Please note that no validation is performed and no callbacks are executed. + # Please note that no validation is performed and only the +after_touch+ + # callback is executed. # If an attribute name is passed, that attribute is updated along with # updated_at/on attributes. # @@ -391,7 +445,7 @@ module ActiveRecord changes[self.class.locking_column] = increment_lock if locking_enabled? - @changed_attributes.except!(*changes.keys) + changed_attributes.except!(*changes.keys) primary_key = self.class.primary_key self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 end @@ -428,23 +482,11 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def update_record(attribute_names = @attributes.keys) - attributes_with_values = arel_attributes_with_values_for_update(attribute_names) - if attributes_with_values.empty? + attributes_values = arel_attributes_with_values_for_update(attribute_names) + if attributes_values.empty? 0 else - klass = self.class - column_hash = klass.connection.schema_cache.columns_hash klass.table_name - db_columns_with_values = attributes_with_values.map { |attr,value| - real_column = column_hash[attr.name] - [real_column, value] - } - bind_attrs = attributes_with_values.dup - bind_attrs.keys.each_with_index do |column, i| - real_column = db_columns_with_values[i].first - bind_attrs[column] = klass.connection.substitute_at(real_column, i) - end - stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(bind_attrs) - klass.connection.update stmt, 'SQL', db_columns_with_values + self.class.unscoped.update_record attributes_values, id, id_was end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index e04a3d0976..6bee4f38e7 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,20 +1,21 @@ module ActiveRecord module Querying - 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_or_create_by, :find_or_create_by!, :find_or_initialize_by, :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 :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_or_create_by, :find_or_create_by!, :find_or_initialize_by, 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 => :all - delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all + :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all + delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all + delegate :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 # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in - # a Product object with the attributes you specified in the SQL query. + # a +Product+ object with the attributes you specified in the SQL query. # # If you call a complicated SQL query which spans multiple tables the columns specified by the # SELECT will be attributes of the model, whether or not they are columns of the corresponding @@ -29,9 +30,10 @@ module ActiveRecord # Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id" # # => [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...] # - # # You can use the same string replacement techniques as you can with ActiveRecord#find + # You can use the same string replacement techniques as you can with <tt>ActiveRecord::QueryMethods#where</tt>: + # # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] - # # => [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...] + # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] def find_by_sql(sql, binds = []) result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) column_types = {} diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 13f3bf7085..eef08aea88 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -36,6 +36,26 @@ module ActiveRecord rake_tasks do require "active_record/base" + + ActiveRecord::Tasks::DatabaseTasks.seed_loader = Rails.application + ActiveRecord::Tasks::DatabaseTasks.env = Rails.env + + namespace :db do + task :load_config do + ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first + ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration + ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths['db/migrate'].to_a + ActiveRecord::Tasks::DatabaseTasks.fixtures_path = File.join Rails.root, 'test', 'fixtures' + ActiveRecord::Tasks::DatabaseTasks.root = Rails.root + + if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH) + if engine.paths['db/migrate'].existent + ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a + end + end + end + end + load "active_record/railties/databases.rake" end @@ -49,7 +69,7 @@ module ActiveRecord Rails.logger.extend ActiveSupport::Logger.broadcast console end - runner do |app| + runner do require "active_record/base" end @@ -64,7 +84,7 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end - initializer "active_record.migration_error" do |app| + initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load config.app_middleware.insert_after "::ActionDispatch::Callbacks", "ActiveRecord::Migration::CheckPending" @@ -92,56 +112,6 @@ module ActiveRecord initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - begin - old_behavior, ActiveSupport::Deprecation.behavior = ActiveSupport::Deprecation.behavior, :stderr - whitelist_attributes = app.config.active_record.delete(:whitelist_attributes) - - if respond_to?(:mass_assignment_sanitizer=) - mass_assignment_sanitizer = nil - else - mass_assignment_sanitizer = app.config.active_record.delete(:mass_assignment_sanitizer) - end - - unless whitelist_attributes.nil? && mass_assignment_sanitizer.nil? - ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, [] - Model based mass assignment security has been extracted - out of Rails into a gem. Please use the new recommended protection model for - params or add `protected_attributes` to your Gemfile to use the old one. - - To disable this message remove the `whitelist_attributes` option from your - `config/application.rb` file and any `mass_assignment_sanitizer` options - from your `config/environments/*.rb` files. - - See http://guides.rubyonrails.org/security.html#mass-assignment for more information. - EOF - end - - unless app.config.active_record.delete(:auto_explain_threshold_in_seconds).nil? - ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, [] - The Active Record auto explain feature has been removed. - - To disable this message remove the `active_record.auto_explain_threshold_in_seconds` - option from the `config/environments/*.rb` config file. - - See http://guides.rubyonrails.org/4_0_release_notes.html for more information. - EOF - end - - unless app.config.active_record.delete(:observers).nil? - ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, [] - Active Record Observers has been extracted out of Rails into a gem. - Please use callbacks or add `rails-observers` to your Gemfile to use observers. - - To disable this message remove the `observers` option from your - `config/application.rb` or from your initializers. - - See http://guides.rubyonrails.org/4_0_release_notes.html for more information. - EOF - end - ensure - ActiveSupport::Deprecation.behavior = old_behavior - end - app.config.active_record.each do |k,v| send "#{k}=", v end @@ -158,7 +128,7 @@ module ActiveRecord end # Expose database runtime to controller for logging. - initializer "active_record.log_runtime" do |app| + initializer "active_record.log_runtime" do require "active_record/railties/controller_runtime" ActiveSupport.on_load(:action_controller) do include ActiveRecord::Railties::ControllerRuntime diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index d92e268109..52b3d3e5e6 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -2,14 +2,8 @@ require 'active_record' db_namespace = namespace :db do task :load_config do - ActiveRecord::Base.configurations = Rails.application.config.database_configuration || {} - ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a - - if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH) - if engine.paths['db/migrate'].existent - ActiveRecord::Migrator.migrations_paths += engine.paths['db/migrate'].to_a - end - end + ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {} + ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths end namespace :create do @@ -18,7 +12,7 @@ db_namespace = namespace :db do end end - desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' + desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)' task :create => [:load_config] do if ENV['DATABASE_URL'] ActiveRecord::Tasks::DatabaseTasks.create_database_url @@ -156,7 +150,7 @@ db_namespace = namespace :db 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' + $stderr.puts 'Sorry, your database adapter is not supported yet. Feel free to submit a patch.' end end @@ -166,11 +160,11 @@ db_namespace = namespace :db do end # desc "Raises an error if there are pending migrations" - task :abort_if_pending_migrations => [:environment, :load_config] do + task :abort_if_pending_migrations => :environment do pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations if pending_migrations.any? - puts "You have #{pending_migrations.size} pending migrations:" + puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" pending_migrations.each do |pending_migration| puts ' %4d %s' % [pending_migration.version, pending_migration.name] end @@ -178,13 +172,13 @@ db_namespace = namespace :db do end end - desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)' + desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)' task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed] desc 'Load the seed data from db/seeds.rb' task :seed do db_namespace['abort_if_pending_migrations'].invoke - Rails.application.load_seed + ActiveRecord::Tasks::DatabaseTasks.load_seed end namespace :fixtures do @@ -192,7 +186,15 @@ db_namespace = namespace :db do task :load => [:environment, :load_config] do require 'active_record/fixtures' - base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten + base_dir = if ENV['FIXTURES_PATH'] + STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " + + "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " + + "instead." + File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten + else + ActiveRecord::Tasks::DatabaseTasks.fixtures_path + end + fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| @@ -209,7 +211,16 @@ db_namespace = namespace :db do puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label - base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') + base_dir = if ENV['FIXTURES_PATH'] + STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " + + "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " + + "instead." + File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten + else + ActiveRecord::Tasks::DatabaseTasks.fixtures_path + end + + Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) data.keys.each do |key| @@ -225,10 +236,10 @@ db_namespace = namespace :db do end namespace :schema do - desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR' + desc 'Create a db/schema.rb file that is portable against any DB supported by AR' task :dump => [:environment, :load_config] do require 'active_record/schema_dumper' - filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" + filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb') File.open(filename, "w:utf-8") do |file| ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end @@ -237,12 +248,9 @@ db_namespace = namespace :db do desc 'Load a schema.rb file into the database' task :load => [:environment, :load_config] do - file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" - if File.exists?(file) - load(file) - else - abort %{#{file} doesn't exist yet. Run `rake db:migrate` to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded} - end + file = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb') + ActiveRecord::Tasks::DatabaseTasks.check_schema_file(file) + load(file) end task :load_if_ruby => ['db:create', :environment] do @@ -253,7 +261,7 @@ db_namespace = namespace :db 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") + filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") con.schema_cache.clear! con.tables.each { |table| con.schema_cache.add(table) } @@ -262,44 +270,24 @@ db_namespace = namespace :db do 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) + filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") + FileUtils.rm(filename) if File.exist?(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, :load_config] do - filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") + filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config - case current_config['adapter'] - when 'oci', 'oracle' - ActiveRecord::Base.establish_connection(current_config) - File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump } - when 'sqlserver' - `smoscript -s #{current_config['host']} -d #{current_config['database']} -u #{current_config['username']} -p #{current_config['password']} -f #{filename} -A -U` - when "firebird" - set_firebird_env(current_config) - db_string = firebird_db_string(current_config) - sh "isql -a #{db_string} > #{filename}" - else - ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) - end + ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) if ActiveRecord::Base.connection.supports_migrations? File.open(filename, "a") do |f| f.puts ActiveRecord::Base.connection.dump_schema_information + f.print "\n" end end db_namespace['structure:dump'].reenable @@ -307,23 +295,10 @@ db_namespace = namespace :db do # desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do + filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") + ActiveRecord::Tasks::DatabaseTasks.check_schema_file(filename) current_config = ActiveRecord::Tasks::DatabaseTasks.current_config - filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql") - case current_config['adapter'] - when 'sqlserver' - `sqlcmd -S #{current_config['host']} -d #{current_config['database']} -U #{current_config['username']} -P #{current_config['password']} -i #{filename}` - when 'oci', 'oracle' - ActiveRecord::Base.establish_connection(current_config) - IO.read(filename).split(";\n\n").each do |ddl| - ActiveRecord::Base.connection.execute(ddl) - end - when 'firebird' - set_firebird_env(current_config) - db_string = firebird_db_string(current_config) - sh "isql -i #{filename} #{db_string}" - else - ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename) - end + ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename) end task :load_if_sql => ['db:create', :environment] do @@ -345,9 +320,16 @@ db_namespace = namespace :db do # 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 + begin + should_reconnect = ActiveRecord::Base.connection_pool.active_connection? + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) + ActiveRecord::Schema.verbose = false + db_namespace["schema:load"].invoke + ensure + if should_reconnect + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) + end + end end # desc "Recreate the test database from an existent structure.sql file" @@ -378,29 +360,11 @@ db_namespace = namespace :db do # desc "Empty the test database" task :purge => [:environment, :load_config] do - abcs = ActiveRecord::Base.configurations - case abcs['test']['adapter'] - when 'sqlserver' - test = abcs.deep_dup['test'] - test_database = test['database'] - test['database'] = 'master' - ActiveRecord::Base.establish_connection(test) - ActiveRecord::Base.connection.recreate_database!(test_database) - when "oci", "oracle" - ActiveRecord::Base.establish_connection(:test) - ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl| - ActiveRecord::Base.connection.execute(ddl) - end - when 'firebird' - ActiveRecord::Base.establish_connection(:test) - ActiveRecord::Base.connection.recreate_database! - else - ActiveRecord::Tasks::DatabaseTasks.purge abcs['test'] - end + ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] end # desc 'Check for pending migrations and load the test schema' - task :prepare => 'db:abort_if_pending_migrations' do + task :prepare => :load_config do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke end @@ -426,7 +390,7 @@ namespace :railties do puts "NOTE: Migration #{migration.basename} from #{name} has been skipped. Migration with the same name already exists." end - on_copy = Proc.new do |name, migration, old_path| + on_copy = Proc.new do |name, migration| puts "Copied migration #{migration.basename} from #{name}" end @@ -436,5 +400,5 @@ namespace :railties do end end -task 'test:prepare' => 'db:test:prepare' +task 'test:prepare' => ['db:test:prepare', 'db:test:load', 'db:abort_if_pending_migrations'] diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 8499bb16e7..b3ddfd63d4 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -20,11 +20,5 @@ module ActiveRecord self._attr_readonly end end - - def _attr_readonly - message = "Instance level _attr_readonly method is deprecated, please use class level method." - ActiveSupport::Deprecation.warn message - 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 0995750ecd..bce7766501 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -5,10 +5,31 @@ module ActiveRecord included do class_attribute :reflections + class_attribute :aggregate_reflections self.reflections = {} + self.aggregate_reflections = {} end - # Reflection enables to interrogate Active Record classes and objects + def self.create(macro, name, scope, options, ar) + case macro + when :has_many, :belongs_to, :has_one + klass = options[:through] ? ThroughReflection : AssociationReflection + when :composed_of + klass = AggregateReflection + end + + klass.new(macro, name, scope, options, ar) + end + + def self.add_reflection(ar, name, reflection) + ar.reflections = ar.reflections.merge(name => reflection) + end + + def self.add_aggregate_reflection(ar, name, reflection) + ar.aggregate_reflections = ar.aggregate_reflections.merge(name => reflection) + end + + # \Reflection enables to interrogate Active Record classes and objects # about their associations and aggregations. This information can, # for example, be used in a form builder that takes an Active Record object # and creates input fields for all of the attributes depending on their type @@ -17,22 +38,9 @@ module ActiveRecord # MacroReflection class has info for AggregateReflection and AssociationReflection # classes. module ClassMethods - def create_reflection(macro, name, scope, options, active_record) - case macro - when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many - klass = options[:through] ? ThroughReflection : AssociationReflection - reflection = klass.new(macro, name, scope, options, active_record) - when :composed_of - reflection = AggregateReflection.new(macro, name, scope, options, active_record) - end - - self.reflections = self.reflections.merge(name => reflection) - reflection - end - # Returns an array of AggregateReflection objects for all the aggregations in the class. def reflect_on_all_aggregations - reflections.values.grep(AggregateReflection) + aggregate_reflections.values end # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). @@ -40,8 +48,7 @@ module ActiveRecord # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection # def reflect_on_aggregation(aggregation) - reflection = reflections[aggregation] - reflection if reflection.is_a?(AggregateReflection) + aggregate_reflections[aggregation] end # Returns an array of AssociationReflection objects for all the @@ -55,7 +62,7 @@ module ActiveRecord # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # def reflect_on_all_associations(macro = nil) - association_reflections = reflections.values.grep(AssociationReflection) + association_reflections = reflections.values macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections end @@ -65,8 +72,7 @@ module ActiveRecord # Invoice.reflect_on_association(:line_items).macro # returns :has_many # def reflect_on_association(association) - reflection = reflections[association] - reflection if reflection.is_a?(AssociationReflection) + reflections[association] end # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. @@ -75,8 +81,13 @@ module ActiveRecord end end - # Abstract base class for AggregateReflection and AssociationReflection. Objects of + # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. + # + # MacroReflection + # AggregateReflection + # AssociationReflection + # ThroughReflection class MacroReflection # Returns the name of the macro. # @@ -95,7 +106,7 @@ module ActiveRecord # Returns the hash of options used for the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt> - # <tt>has_many :clients</tt> returns +{}+ + # <tt>has_many :clients</tt> returns <tt>{}</tt> attr_reader :options attr_reader :active_record @@ -108,10 +119,16 @@ module ActiveRecord @scope = scope @options = options @active_record = active_record + @klass = options[:class] @plural_name = active_record.pluralize_table_names ? name.to_s.pluralize : name.to_s end + def autosave=(autosave) + @automatic_inverse_of = false + @options[:autosave] = autosave + end + # Returns the class for the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class @@ -173,9 +190,15 @@ module ActiveRecord @klass ||= active_record.send(:compute_type, class_name) end - def initialize(*args) + attr_reader :type, :foreign_type + + def initialize(macro, name, scope, options, active_record) super - @collection = [:has_many, :has_and_belongs_to_many].include?(macro) + @collection = :has_many == macro + @automatic_inverse_of = nil + @type = options[:as] && "#{options[:as]}_type" + @foreign_type = options[:foreign_type] || "#{name}_type" + @constructable = calculate_constructable(macro, options) end # Returns a new, unsaved instance of the associated class. +attributes+ will @@ -184,12 +207,16 @@ module ActiveRecord klass.new(attributes, &block) end + def constructable? # :nodoc: + @constructable + end + def table_name - @table_name ||= klass.table_name + klass.table_name end def quoted_table_name - @quoted_table_name ||= klass.quoted_table_name + klass.quoted_table_name end def join_table @@ -200,16 +227,8 @@ module ActiveRecord @foreign_key ||= options[:foreign_key] || derive_foreign_key end - def foreign_type - @foreign_type ||= options[:foreign_type] || "#{name}_type" - end - - def type - @type ||= options[:as] && "#{options[:as]}_type" - end - def primary_key_column - @primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key } + klass.columns_hash[klass.primary_key] end def association_foreign_key @@ -233,20 +252,8 @@ module ActiveRecord end end - def columns(tbl_name) - @columns ||= klass.connection.columns(tbl_name) - end - - def reset_column_information - @columns = nil - end - 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! @@ -262,7 +269,7 @@ module ActiveRecord end def source_reflection - nil + self end # A chain of reflections from this one back to the owner. For more see the explanation in @@ -284,13 +291,13 @@ module ActiveRecord alias :source_macro :macro def has_inverse? - @options[:inverse_of] + inverse_name end def inverse_of - if has_inverse? - @inverse_of ||= klass.reflect_on_association(options[:inverse_of]) - end + return unless inverse_name + + @inverse_of ||= klass.reflect_on_association inverse_name end def polymorphic_inverse_of(associated_class) @@ -328,10 +335,6 @@ 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 @@ -340,8 +343,6 @@ module ActiveRecord else Associations::BelongsToAssociation end - when :has_and_belongs_to_many - Associations::HasAndBelongsToManyAssociation when :has_many if options[:through] Associations::HasManyThroughAssociation @@ -361,7 +362,92 @@ module ActiveRecord options.key? :polymorphic end + VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] + INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] + + protected + + def actual_source_reflection # FIXME: this is a horrible name + self + end + private + + def calculate_constructable(macro, options) + case macro + when :belongs_to + !options[:polymorphic] + when :has_one + !options[:through] + else + true + end + end + + # Attempts to find the inverse association name automatically. + # If it cannot find a suitable inverse association name, it returns + # nil. + def inverse_name + options.fetch(:inverse_of) do + if @automatic_inverse_of == false + nil + else + @automatic_inverse_of ||= automatic_inverse_of + end + end + end + + # returns either nil or the inverse association name that it finds. + def automatic_inverse_of + if can_find_inverse_of_automatically?(self) + inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym + + begin + reflection = klass.reflect_on_association(inverse_name) + rescue NameError + # Give up: we couldn't compute the klass type so we won't be able + # to find any associations either. + reflection = false + end + + if valid_inverse_reflection?(reflection) + return inverse_name + end + end + + false + end + + # Checks if the inverse reflection that is returned from the + # +automatic_inverse_of+ method is a valid reflection. We must + # make sure that the reflection's active_record name matches up + # with the current reflection's klass name. + # + # Note: klass will always be valid because when there's a NameError + # from calling +klass+, +reflection+ will already be set to false. + def valid_inverse_reflection?(reflection) + reflection && + klass.name == reflection.active_record.name && + can_find_inverse_of_automatically?(reflection) + end + + # Checks to see if the reflection doesn't have any options that prevent + # us from being able to guess the inverse automatically. First, the + # <tt>inverse_of</tt> option cannot be set to false. Second, we must + # have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations. + # Third, we must not have options such as <tt>:polymorphic</tt> or + # <tt>:foreign_key</tt> which prevent us from correctly guessing the + # inverse association. + # + # Anything with a scope can additionally ruin our attempt at finding an + # inverse, so we exclude reflections with scopes. + def can_find_inverse_of_automatically?(reflection) + reflection.options[:inverse_of] != false && + VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) && + !INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } && + !reflection.scope + end + def derive_class_name class_name = name.to_s.camelize class_name = class_name.singularize if collection? @@ -393,7 +479,12 @@ module ActiveRecord delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :type, :to => :source_reflection - # Gets the source of the through reflection. It checks both a singularized + def initialize(macro, name, scope, options, active_record) + super + @source_reflection_name = options[:source] + end + + # Returns the source of the through reflection. It checks both a singularized # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. # # class Post < ActiveRecord::Base @@ -401,8 +492,17 @@ module ActiveRecord # has_many :tags, through: :taggings # end # + # class Tagging < ActiveRecord::Base + # belongs_to :post + # belongs_to :tag + # end + # + # tags_reflection = Post.reflect_on_association(:tags) + # tags_reflection.source_reflection + # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags"> + # def source_reflection - @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first + through_reflection.klass.reflect_on_association(source_reflection_name) end # Returns the AssociationReflection object specified in the <tt>:through</tt> option @@ -414,10 +514,11 @@ module ActiveRecord # end # # tags_reflection = Post.reflect_on_association(:tags) - # taggings_reflection = tags_reflection.through_reflection + # tags_reflection.through_reflection + # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings"> # def through_reflection - @through_reflection ||= active_record.reflect_on_association(options[:through]) + active_record.reflect_on_association(options[:through]) end # Returns an array of reflections which are involved in this association. Each item in the @@ -426,9 +527,22 @@ module ActiveRecord # The chain is built by recursively calling #chain on the source reflection and the through # reflection. The base case for the recursion is a normal association, which just returns # [self] as its #chain. + # + # class Post < ActiveRecord::Base + # has_many :taggings + # has_many :tags, through: :taggings + # end + # + # tags_reflection = Post.reflect_on_association(:tags) + # tags_reflection.chain + # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>, + # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>] + # def chain @chain ||= begin - chain = source_reflection.chain + through_reflection.chain + a = source_reflection.chain + b = through_reflection.chain + chain = a + b chain[0] = self # Use self so we don't lose the information from :source_type chain end @@ -460,7 +574,7 @@ module ActiveRecord # Add to it the scope from this reflection (if any) scope_chain.first << scope if scope - through_scope_chain = through_reflection.scope_chain + through_scope_chain = through_reflection.scope_chain.map(&:dup) if options[:source_type] through_scope_chain.first << @@ -479,7 +593,7 @@ module ActiveRecord # A through association is nested if there would be more than one join table def nested? - chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many + chain.length > 2 end # We want to use the klass from this reflection, rather than just delegate straight to @@ -488,20 +602,47 @@ module ActiveRecord def association_primary_key(klass = nil) # Get the "actual" source reflection if the immediate source reflection has a # source reflection itself - source_reflection = self.source_reflection - while source_reflection.source_reflection - source_reflection = source_reflection.source_reflection - end - - source_reflection.options[:primary_key] || primary_key(klass || self.klass) + actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass) end - # Gets an array of possible <tt>:through</tt> source reflection names: + # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form. + # + # class Post < ActiveRecord::Base + # has_many :taggings + # has_many :tags, through: :taggings + # end # - # [:singularized, :pluralized] + # tags_reflection = Post.reflect_on_association(:tags) + # tags_reflection.source_reflection_names + # # => [:tag, :tags] # def source_reflection_names - @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym } + (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }.uniq + end + + def source_reflection_name # :nodoc: + return @source_reflection_name if @source_reflection_name + + names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq + names = names.find_all { |n| + through_reflection.klass.reflect_on_association(n) + } + + if names.length > 1 + example_options = options.dup + example_options[:source] = source_reflection_names.first + ActiveSupport::Deprecation.warn <<-eowarn +Ambiguous source reflection for through association. Please specify a :source +directive on your declaration like: + + class #{active_record.name} < ActiveRecord::Base + #{macro} :#{name}, #{example_options} + end + + eowarn + end + + @source_reflection_name = names.first end def source_options @@ -540,6 +681,12 @@ module ActiveRecord check_validity_of_inverse! end + protected + + def actual_source_reflection # FIXME: this is a horrible name + source_reflection.actual_source_reflection + end + private def derive_class_name # get the class_name of the belongs_to association of the through reflection diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ad54ba55b6..60f2726a6e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -10,25 +10,21 @@ module ActiveRecord :extending] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, - :reverse_order, :uniq, :create_with] + :reverse_order, :distinct, :create_with, :uniq] VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded - attr_accessor :default_scoped alias :model :klass alias :loaded? :loaded - alias :default_scoped? :default_scoped def initialize(klass, table, values = {}) - @klass = klass - @table = table - @values = values - @implicit_readonly = nil - @loaded = false - @default_scoped = false + @klass = klass + @table = table + @values = values + @loaded = false end def initialize_copy(other) @@ -39,7 +35,7 @@ module ActiveRecord reset end - def insert(values) + def insert(values) # :nodoc: primary_key_value = nil if primary_key && Hash === values @@ -56,16 +52,7 @@ module ActiveRecord im = arel.create_insert im.into @table - conn = @klass.connection - - substitutes = values.sort_by { |arel_attr,_| arel_attr.name } - binds = substitutes.map do |arel_attr, value| - [@klass.columns_hash[arel_attr.name], value] - end - - substitutes.each_with_index do |tuple, i| - tuple[1] = conn.substitute_at(binds[i][0], i) - end + substitutes, binds = substitute_values values if values.empty? # empty insert im.values = Arel.sql(connection.empty_insert_statement_value) @@ -73,7 +60,7 @@ module ActiveRecord im.insert substitutes end - conn.insert( + @klass.connection.insert( im, 'SQL', primary_key, @@ -82,6 +69,29 @@ module ActiveRecord binds) end + def update_record(values, id, id_was) # :nodoc: + substitutes, binds = substitute_values values + um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes) + + @klass.connection.update( + um, + 'SQL', + binds) + end + + def substitute_values(values) # :nodoc: + substitutes = values.sort_by { |arel_attr,_| arel_attr.name } + binds = substitutes.map do |arel_attr, value| + [@klass.columns_hash[arel_attr.name], value] + end + + substitutes.each_with_index do |tuple, i| + tuple[1] = @klass.connection.substitute_at(binds[i][0], i) + end + + [substitutes, binds] + end + # Initializes new record from relation while maintaining the current # scope. # @@ -141,34 +151,58 @@ module ActiveRecord first || new(attributes, &block) end - # Finds the first record with the given attributes, or creates a record with the attributes - # if one is not found. + # Finds the first record with the given attributes, or creates a record + # with the attributes if one is not found: # - # ==== Examples - # # Find the first user named Penélope or create a new one. + # # Find the first user named "Penélope" or create a new one. # User.find_or_create_by(first_name: 'Penélope') - # # => <User id: 1, first_name: 'Penélope', last_name: nil> + # # => #<User id: 1, first_name: "Penélope", last_name: nil> # - # # Find the first user named Penélope or create a new one. + # # Find the first user named "Penélope" or create a new one. # # We already have one so the existing record will be returned. # User.find_or_create_by(first_name: 'Penélope') - # # => <User id: 1, first_name: 'Penélope', last_name: nil> + # # => #<User id: 1, first_name: "Penélope", last_name: nil> # - # # Find the first user named Scarlett or create a new one with a particular last name. + # # Find the first user named "Scarlett" or create a new one with + # # a particular last name. # User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett') - # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> + # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson"> # - # # Find the first user named Scarlett or create a new one with a different last name. - # # We already have one so the existing record will be returned. + # This method accepts a block, which is passed down to +create+. The last example + # above can be alternatively written this way: + # + # # Find the first user named "Scarlett" or create a new one with a + # # different last name. # User.find_or_create_by(first_name: 'Scarlett') do |user| - # user.last_name = "O'Hara" + # user.last_name = 'Johansson' # end - # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> + # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson"> + # + # This method always returns a record, but if creation was attempted and + # failed due to validation errors it won't be persisted, you get what + # +create+ returns in such situation. + # + # Please note *this method is not atomic*, it runs first a SELECT, and if + # there are no results an INSERT is attempted. If there are other threads + # or processes there is a race condition between both calls and it could + # be the case that you end up with two similar records. + # + # Whether that is a problem or not depends on the logic of the + # application, but in the particular case in which rows have a UNIQUE + # constraint an exception may be raised, just retry: + # + # begin + # CreditAccount.find_or_create_by(user_id: user.id) + # rescue ActiveRecord::RecordNotUnique + # retry + # end + # def find_or_create_by(attributes, &block) find_by(attributes) || create(attributes, &block) end - # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception is raised if the created record is invalid. + # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception + # is raised if the created record is invalid. def find_or_create_by!(attributes, &block) find_by(attributes) || create!(attributes, &block) end @@ -210,8 +244,7 @@ module ActiveRecord def empty? return @records.empty? if loaded? - c = count - c.respond_to?(:zero?) ? c.zero? : c.empty? + limit_value == 0 ? true : !exists? end # Returns true if there are any records. @@ -276,7 +309,7 @@ module ActiveRecord stmt.table(table) stmt.key = table[primary_key] - if with_default_scope.joins_values.any? + if joins_values.any? @klass.connection.join_to_update(stmt, arel) else stmt.take(arel.limit) @@ -401,7 +434,7 @@ module ActiveRecord stmt = Arel::DeleteManager.new(arel.engine) stmt.from(table) - if with_default_scope.joins_values.any? + if joins_values.any? @klass.connection.join_to_delete(stmt, arel, table[primary_key]) else stmt.wheres = arel.constraints @@ -467,7 +500,21 @@ module ActiveRecord # User.where(name: 'Oscar').to_sql # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql - @to_sql ||= klass.connection.to_sql(arel, bind_values.dup) + @to_sql ||= begin + relation = self + connection = klass.connection + visitor = connection.visitor + + if eager_loading? + find_with_associations { |rel| relation = rel } + end + + ast = relation.arel.ast + binds = relation.bind_values.dup + visitor.accept(ast) do + connection.quote(*binds.shift.reverse) + end + end end # Returns a hash of where conditions. @@ -475,11 +522,12 @@ module ActiveRecord # User.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| + equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node| node.left.relation.name == table_name } binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] + binds.merge!(Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]) Hash[equalities.map { |where| name = where.left.name @@ -506,6 +554,12 @@ module ActiveRecord includes_values & joins_values end + # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+ + # to maintain backwards compatibility. Use +distinct_value+ instead. + def uniq_value + distinct_value + end + # Compares two relations for equality. def ==(other) case other @@ -520,16 +574,6 @@ module ActiveRecord q.pp(self.to_a) end - def with_default_scope #:nodoc: - if default_scoped? && default_scope = klass.send(:build_default_scope) - default_scope = default_scope.merge(self) - default_scope.default_scoped = false - default_scope - else - self - end - end - # Returns true if relation is blank. def blank? to_a.blank? @@ -549,34 +593,24 @@ module ActiveRecord private def exec_queries - default_scoped = with_default_scope - - if default_scoped.equal?(self) - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) + @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 + preload = preload_values + preload += includes_values unless eager_loading? + preloader = ActiveRecord::Associations::Preloader.new + preload.each do |associations| + preloader.preload @records, associations end + @records.each { |record| record.readonly! } if readonly_value + @loaded = true @records end def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| - if join.is_a?(Arel::Nodes::StringJoin) - tables_in_string(join.left) - else + unless join.is_a?(Arel::Nodes::StringJoin) [join.left.table_name, join.left.table_alias] end end @@ -585,37 +619,8 @@ module ActiveRecord # always convert table names to downcase as in Oracle quoted table names are in uppercase joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq - string_tables = tables_in_string(to_sql) - - if (references_values - joined_tables).any? - true - elsif (string_tables - joined_tables).any? - ActiveSupport::Deprecation.warn( - "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \ - "that are referenced in a string SQL snippet. For example: \n" \ - "\n" \ - " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \ - "\n" \ - "Currently, Active Record recognises the table in the string, and knows to JOIN the " \ - "comments table to the query, rather than loading comments in a separate query. " \ - "However, doing this without writing a full-blown SQL parser is inherently flawed. " \ - "Since we don't want to write an SQL parser, we are removing this functionality. " \ - "From now on, you must explicitly tell Active Record when you are referencing a table " \ - "from a string:\n" \ - "\n" \ - " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n\n" - ) - true - else - false - end - end - def tables_in_string(string) - return [] if string.blank? - # always convert table names to downcase as in Oracle quoted table names are in uppercase - # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries - string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] + (references_values - joined_tables).any? end end end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index b921f2eddb..49b01909c6 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -11,7 +11,7 @@ module ActiveRecord # 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.find_each do |person| # person.do_awesome_stuff # end # @@ -19,47 +19,78 @@ module ActiveRecord # person.party_all_night! # end # - # 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 } - end - 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 +:batch_size+ - # option; the default is 1000. + # If you do not provide a block to #find_each, it will return an Enumerator + # for chaining with other methods: + # + # Person.find_each.with_index do |person, index| + # person.award_trophy(index + 1) + # end + # + # ==== Options + # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # 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 +:start+ option on that worker). # - # You can control the starting point for the batch processing by - # 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 +:start+ - # option on that worker). + # # Let's process for a batch of 2000 records, skipping the first 2000 rows + # Person.find_each(start: 2000, batch_size: 2000) do |person| + # person.party_all_night! + # end # - # It's not possible to set the order. That is automatically set to + # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering # work. This also means that this method only works with integer-based - # primary keys. You can't set the limit either, that's used to control + # primary keys. + # + # NOTE: You can't set the limit either, that's used to control # the batch sizes. + def find_each(options = {}) + if block_given? + find_in_batches(options) do |records| + records.each { |record| yield record } + end + else + enum_for :find_each, options + end + end + + # Yields each batch of records that was found by the find +options+ as + # an array. # # 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 # + # ==== Options + # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # 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 +:start+ option on that worker). + # # # Let's process the next 2000 records - # Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group| + # Person.find_in_batches(start: 2000, batch_size: 2000) do |group| # group.each { |person| person.party_all_night! } # end + # + # NOTE: It's not possible to set the order. That is automatically set to + # ascending on the primary key ("id ASC") to make the batch ordering + # work. This also means that this method only works with integer-based + # primary keys. + # + # NOTE: You can't set the limit either, that's used to control + # the batch sizes. 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") + if logger && (arel.orders.present? || arel.taken.present?) + logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end start = options.delete(:start) diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 7f95181c67..2d267183ce 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -11,7 +11,7 @@ module ActiveRecord # Person.count(:all) # # => performs a COUNT(*) (:all is an alias for '*') # - # Person.count(:age, distinct: true) + # Person.distinct.count(:age) # # => counts the number of different age values # # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column, @@ -19,52 +19,43 @@ module ActiveRecord # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } - def count(column_name = nil, options = {}) - column_name, options = nil, column_name if column_name.is_a?(Hash) - calculate(:count, column_name, options) + def count(column_name = nil) + calculate(:count, column_name) end # Calculates the average value on a given column. Returns +nil+ if there's # no row. See +calculate+ for examples with options. # - # Person.average('age') # => 35.8 - def average(column_name, options = {}) - calculate(:average, column_name, options) + # Person.average(:age) # => 35.8 + def average(column_name) + calculate(:average, column_name) end # Calculates the minimum value on a given column. The value is returned # with the same data type of the column, or +nil+ if there's no row. See # +calculate+ for examples with options. # - # Person.minimum('age') # => 7 - def minimum(column_name, options = {}) - calculate(:minimum, column_name, options) + # Person.minimum(:age) # => 7 + def minimum(column_name) + calculate(:minimum, column_name) end # Calculates the maximum value on a given column. The value is returned # with the same data type of the column, or +nil+ if there's no row. See # +calculate+ for examples with options. # - # Person.maximum('age') # => 93 - def maximum(column_name, options = {}) - calculate(:maximum, column_name, options) + # Person.maximum(:age) # => 93 + def maximum(column_name) + calculate(:maximum, column_name) end # Calculates the sum of values on a given column. The value is returned # with the same data type of the column, 0 if there's no row. See # +calculate+ for examples with options. # - # Person.sum('age') # => 4562 + # Person.sum(:age) # => 4562 def sum(*args) - if block_given? - ActiveSupport::Deprecation.warn( - "Calling #sum with a block is deprecated and will be removed in Rails 4.1. " \ - "If you want to perform sum calculation over the array of elements, use `to_a.sum(&block)`." - ) - self.to_a.sum(*args) {|*block_args| yield(*block_args)} - else - calculate(:sum, *args) - end + calculate(:sum, *args) end # This calculates aggregate values in the given column. Methods for count, sum, average, @@ -82,7 +73,7 @@ module ActiveRecord # puts values["Drake"] # # => 43 # - # drake = Family.find_by_last_name('Drake') + # drake = Family.find_by(last_name: 'Drake') # values = Person.group(:family).maximum(:age) # Person belongs_to :family # puts values[drake] # # => 43 @@ -98,20 +89,16 @@ module ActiveRecord # Person.group(:last_name).having("min(age) > 17").minimum(:age) # # Person.sum("2 * age") - def calculate(operation, column_name, options = {}) - relation = with_default_scope + def calculate(operation, column_name) + if column_name.is_a?(Symbol) && attribute_alias?(column_name) + column_name = attribute_alias(column_name) + end - if relation.equal?(self) - if has_include?(column_name) - construct_relation_for_association_calculations.calculate(operation, column_name, options) - else - perform_calculation(operation, column_name, options) - end + if has_include?(column_name) + construct_relation_for_association_calculations.calculate(operation, column_name) else - relation.calculate(operation, column_name, options) + perform_calculation(operation, column_name) end - rescue ThrowResult - 0 end # Use <tt>pluck</tt> as a shortcut to select one or more attributes without @@ -135,7 +122,7 @@ module ActiveRecord # # SELECT people.id, people.name FROM people # # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] # - # Person.uniq.pluck(:role) + # Person.pluck('DISTINCT role') # # SELECT DISTINCT role FROM people # # => ['admin', 'member', 'guest'] # @@ -149,10 +136,10 @@ module ActiveRecord # 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)}" + if column_name.is_a?(Symbol) && attribute_alias?(column_name) + attribute_alias(column_name) else - column_name + column_name.to_s end end @@ -160,22 +147,20 @@ module ActiveRecord construct_relation_for_association_calculations.pluck(*column_names) else relation = spawn - relation.select_values = column_names + relation.select_values = column_names.map { |cn| + columns_hash.key?(cn) ? arel_table[cn] : cn + } result = klass.connection.select_all(relation.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 - } + result.column_types.fetch(key) { result.identity_type } } end result = result.map do |attributes| values = klass.initialize_attributes(attributes).values - columns.zip(values).map do |column, value| - column.type_cast(value) - end + columns.zip(values).map { |column, value| column.type_cast value } end columns.one? ? result.map!(&:first) : result end @@ -195,22 +180,21 @@ module ActiveRecord eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) end - def perform_calculation(operation, column_name, options = {}) + def perform_calculation(operation, column_name) operation = operation.to_s.downcase - # If #count is used in conjuction with #uniq it is considered distinct. (eg. relation.uniq.count) - distinct = options[:distinct] || self.uniq_value + # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) + distinct = self.distinct_value if operation == "count" - column_name ||= (select_for_count || :all) + column_name ||= select_for_count unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true end column_name = primary_key if column_name == :all && distinct - - distinct = nil if column_name =~ /\s*DISTINCT\s+/i + distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i end if group_values.any? @@ -371,10 +355,12 @@ module ActiveRecord column ? column.type_cast(value) : value end + # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). def select_for_count if select_values.present? - select = select_values.join(", ") - select if select !~ /[,*]/ + select_values.join(", ") + else + :all end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 00a506c3a7..1e15bddcdf 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,8 +1,34 @@ -require 'thread' -require 'thread_safe' +require 'active_support/concern' +require 'active_support/deprecation' module ActiveRecord module Delegation # :nodoc: + module DelegateCache + def relation_delegate_class(klass) # :nodoc: + @relation_delegate_cache[klass] + end + + def initialize_relation_delegate_cache # :nodoc: + @relation_delegate_cache = cache = {} + [ + ActiveRecord::Relation, + ActiveRecord::Associations::CollectionProxy, + ActiveRecord::AssociationRelation + ].each do |klass| + delegate = Class.new(klass) { + include ClassSpecificRelation + } + const_set klass.name.gsub('::', '_'), delegate + cache[klass] = delegate + end + end + + def inherited(child_class) + child_class.initialize_relation_delegate_cache + super + end + end + extend ActiveSupport::Concern # This module creates compiled delegation methods dynamically at runtime, which makes @@ -14,14 +40,14 @@ module ActiveRecord delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, :to => :klass - module ClassSpecificRelation + module ClassSpecificRelation # :nodoc: extend ActiveSupport::Concern included do @delegation_mutex = Mutex.new end - module ClassMethods + module ClassMethods # :nodoc: def name superclass.name end @@ -37,11 +63,9 @@ module ActiveRecord end RUBY else - module_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{method}(*args, &block) - scoping { @klass.send(#{method.inspect}, *args, &block) } - end - RUBY + define_method method do |*args, &block| + scoping { @klass.send(method, *args, &block) } + end end end end @@ -60,7 +84,7 @@ module ActiveRecord if @klass.respond_to?(method) self.class.delegate_to_scoped_klass(method) scoping { @klass.send(method, *args, &block) } - elsif Array.method_defined?(method) + elsif array_delegable?(method) self.class.delegate method, :to => :to_a to_a.send(method, *args, &block) elsif arel.respond_to?(method) @@ -72,50 +96,40 @@ module ActiveRecord end end - module ClassMethods - @@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2) - - def new(klass, *args) - relation = relation_class_for(klass).allocate - relation.__send__(:initialize, klass, *args) - relation - end - - # This doesn't have to be thread-safe. relation_class_for guarantees that this will only be - # called exactly once for a given const name. - def const_missing(name) - const_set(name, Class.new(self) { include ClassSpecificRelation }) + module ClassMethods # :nodoc: + def create(klass, *args) + relation_class_for(klass).new(klass, *args) end private - # Cache the constants in @@subclasses because looking them up via const_get - # make instantiation significantly slower. + def relation_class_for(klass) - if klass && (klass_name = klass.name) - my_cache = @@subclasses.compute_if_absent(self) { ThreadSafe::Cache.new } - # This hash is keyed by klass.name to avoid memory leaks in development mode - my_cache.compute_if_absent(klass_name) do - # Cache#compute_if_absent guarantees that the block will only executed once for the given klass_name - const_get("#{name.gsub('::', '_')}_#{klass_name.gsub('::', '_')}", false) - end - else - ActiveRecord::Relation - end + klass.relation_delegate_class(self) end end def respond_to?(method, include_private = false) - super || Array.method_defined?(method) || + super || array_delegable?(method) || @klass.respond_to?(method, include_private) || arel.respond_to?(method, include_private) end protected + def array_delegable?(method) + defined = Array.method_defined?(method) + if defined && method.to_s.ends_with?('!') + ActiveSupport::Deprecation.warn( + "Association will no longer delegate #{method} to #to_a as of Rails 4.2. You instead must first call #to_a on the association to expose the array to be acted on." + ) + end + defined + end + def method_missing(method, *args, &block) if @klass.respond_to?(method) scoping { @klass.send(method, *args, &block) } - elsif Array.method_defined?(method) + elsif array_delegable?(method) to_a.send(method, *args, &block) elsif arel.respond_to?(method) arel.send(method, *args, &block) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 14520381c9..3a02bf90e9 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,5 +1,7 @@ module ActiveRecord module FinderMethods + ONE_AS_ONE = '1 AS one' + # 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+. @@ -11,9 +13,11 @@ module ActiveRecord # 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> - # to ensure the results are sorted. + # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found. + # + # NOTE: The returned records may not be in the same order as the ids you + # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt> + # option if you want the results are sorted. # # ==== Find with lock # @@ -28,6 +32,34 @@ module ActiveRecord # person.visits += 1 # person.save! # end + # + # ==== Variations of +find+ + # + # Person.where(name: 'Spartacus', rating: 4) + # # returns a chainable list (which can be empty). + # + # Person.find_by(name: 'Spartacus', rating: 4) + # # returns the first item or nil. + # + # Person.where(name: 'Spartacus', rating: 4).first_or_initialize + # # returns the first item or returns a new instance (requires you call .save to persist against the database). + # + # Person.where(name: 'Spartacus', rating: 4).first_or_create + # # returns the first item or creates it and returns it, available since Rails 3.2.1. + # + # ==== Alternatives for +find+ + # + # Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none) + # # returns a boolean indicating if any record with the given conditions exist. + # + # Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3") + # # returns a chainable list of instances with only the mentioned fields. + # + # Person.where(name: 'Spartacus', rating: 4).ids + # # returns an Array of ids, available since Rails 3.2.1. + # + # Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2) + # # returns an Array of the required fields, available since Rails 3.1. def find(*args) if block_given? to_a.find { |*block_args| yield(*block_args) } @@ -37,7 +69,7 @@ module ActiveRecord end # Finds the first record matching the specified conditions. There - # is no implied ording so if order matters, you should specify it + # is no implied ordering so if order matters, you should specify it # yourself. # # If no record is found, returns <tt>nil</tt>. @@ -79,13 +111,22 @@ module ActiveRecord # 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 + # + # ==== Rails 3 + # + # Person.first # SELECT "people".* FROM "people" LIMIT 1 + # + # NOTE: Rails 3 may not order this query by the primary key and the order + # will depend on the database implementation. In order to ensure that behavior, + # use <tt>User.order(:id).first</tt> instead. + # + # ==== Rails 4 + # + # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1 + # def first(limit = nil) if limit - if order_values.empty? && primary_key - order(arel_table[primary_key].asc).limit(limit).to_a - else - limit(limit).to_a - end + find_first_with_limit(limit) else find_first end @@ -137,14 +178,14 @@ module ActiveRecord # * String - Finds the record with a primary key corresponding to this # string (such as <tt>'5'</tt>). # * Array - Finds the record that matches these +find+-style conditions - # (such as <tt>['color = ?', 'red']</tt>). + # (such as <tt>['name LIKE ?', "%#{query}%"]</tt>). # * Hash - Finds the record that matches these +find+-style conditions - # (such as <tt>{color: 'red'}</tt>). + # (such as <tt>{name: 'David'}</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. + # For more information about specifying conditions as a hash or array, + # see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>. # # Note: You can't pass in a condition as a string (like <tt>name = # 'Jamie'</tt>), since it would be sanitized and then queried against @@ -160,9 +201,10 @@ module ActiveRecord conditions = conditions.id if Base === 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 AS one").limit(1) + relation = apply_join_dependency(self, construct_join_dependency) + return false if ActiveRecord::NullRelation === relation + + relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1) case conditions when Array, Hash @@ -171,69 +213,95 @@ module ActiveRecord relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none end - connection.select_value(relation, "#{name} Exists", relation.bind_values) - rescue ThrowResult - false + connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false end - protected + # This method is called whenever no records are found with either a single + # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception. + # + # The error message is different depending on whether a single id or + # multiple ids are provided. If multiple ids are provided, then the number + # of results obtained should be provided in the +result_size+ argument and + # the expected number of results should be provided in the +expected_size+ + # argument. + def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc: + conditions = arel.where_sql + conditions = " [#{conditions}]" if conditions + + if Array(ids).size == 1 + error = "Couldn't find #{@klass.name} with #{primary_key}=#{ids}#{conditions}" + else + error = "Couldn't find all #{@klass.name.pluralize} with IDs " + error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})" + end - 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.dup) - join_dependency.instantiate(rows) - rescue ThrowResult - [] + raise RecordNotFound, error end - def construct_join_dependency_for_association_find - including = (eager_load_values + includes_values).uniq - ActiveRecord::Associations::JoinDependency.new(@klass, including, []) + private + + def find_with_associations + join_dependency = construct_join_dependency + + aliases = join_dependency.aliases + relation = select aliases.columns + relation = apply_join_dependency(relation, join_dependency) + + if block_given? + yield relation + else + if ActiveRecord::NullRelation === relation + [] + else + rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup) + join_dependency.instantiate(rows, aliases) + end + end end - def construct_relation_for_association_calculations - 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) + def construct_join_dependency(joins = []) + including = eager_load_values + includes_values + ActiveRecord::Associations::JoinDependency.new(@klass, including, joins) end - def construct_relation_for_association_find(join_dependency) - relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns) - apply_join_dependency(relation, join_dependency) + def construct_relation_for_association_calculations + apply_join_dependency(self, construct_join_dependency(arel.froms.first)) end def apply_join_dependency(relation, join_dependency) - join_dependency.join_associations.each do |association| - relation = association.join_relation(relation) - end + relation = relation.except(:includes, :eager_load, :preload) + relation = relation.joins join_dependency - limitable_reflections = using_limitable_reflections?(join_dependency.reflections) - - if !limitable_reflections && relation.limit_value - limited_id_condition = construct_limited_ids_condition(relation.except(:select)) - relation = relation.where(limited_id_condition) + if using_limitable_reflections?(join_dependency.reflections) + relation + else + if relation.limit_value + limited_ids = limited_ids_for(relation) + limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids)) + end + relation.except(:limit, :offset) end - - relation = relation.except(:limit, :offset) unless limitable_reflections - - relation end - def construct_limited_ids_condition(relation) - orders = relation.order_values.map { |val| val.presence }.compact - values = @klass.connection.distinct("#{quoted_table_name}.#{primary_key}", orders) + def limited_ids_for(relation) + values = @klass.connection.columns_for_distinct( + "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values) - relation = relation.dup.select(values) + relation = relation.except(:select).select(values).distinct! id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values) - ids_array = id_rows.map {|row| row[primary_key]} + id_rows.map {|row| row[primary_key]} + end - ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) + def using_limitable_reflections?(reflections) + reflections.none? { |r| r.collection? } end + protected + def find_with_ids(*ids) + raise UnknownPrimaryKey.new(@klass) if primary_key.nil? + expects_array = ids.first.kind_of?(Array) return ids.first if expects_array && ids.first.empty? @@ -259,11 +327,7 @@ module ActiveRecord relation.bind_values += [[column, id]] record = relation.take - unless record - conditions = arel.where_sql - conditions = " [#{conditions}]" if conditions - raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}" - end + raise_record_not_found_exception!(id, 0, 1) unless record record end @@ -286,12 +350,7 @@ module ActiveRecord if result.size == expected_size result else - conditions = arel.where_sql - conditions = " [#{conditions}]" if conditions - - error = "Couldn't find all #{@klass.name.pluralize} with IDs " - error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})" - raise RecordNotFound, error + raise_record_not_found_exception!(ids, result.size, expected_size) end end @@ -307,12 +366,15 @@ module ActiveRecord if loaded? @records.first else - @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 + @first ||= find_first_with_limit(1).first + end + end + + def find_first_with_limit(limit) + if order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(limit).to_a + else + limit(limit).to_a end end @@ -328,9 +390,5 @@ module ActiveRecord end end end - - def using_limitable_reflections?(reflections) - reflections.none? { |r| r.collection? } - end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index eb23e92fb8..182b9ed89c 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -22,7 +22,7 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.new(relation.klass, relation.table) + other = Relation.create(relation.klass, relation.table) hash.each { |k, v| if k == :joins if Hash === v @@ -39,20 +39,17 @@ module ActiveRecord end class Merger # :nodoc: - attr_reader :relation, :values + attr_reader :relation, :values, :other def initialize(relation, other) - if other.default_scoped? && other.klass != relation.klass - other = other.with_default_scope - end - @relation = relation @values = other.values + @other = other end NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS + Relation::MULTI_VALUE_METHODS - - [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc: + [:joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc: def normal_values NORMAL_VALUES @@ -61,26 +58,78 @@ module ActiveRecord def merge normal_values.each do |name| value = values[name] - relation.send("#{name}!", *value) unless value.blank? + # The unless clause is here mostly for performance reasons (since the `send` call might be moderately + # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that + # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values + # don't fall through the cracks. + relation.send("#{name}!", *value) unless value.nil? || (value.blank? && false != value) end merge_multi_values merge_single_values + merge_joins relation end private + def merge_joins + return if values[:joins].blank? + + if other.klass == relation.klass + relation.joins!(*values[:joins]) + else + joins_dependency, rest = values[:joins].partition do |join| + case join + when Hash, Symbol, Array + true + else + false + end + end + + join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass, + joins_dependency, + []) + relation.joins! rest + + @relation = relation.joins join_dependency + end + end + def merge_multi_values - relation.where_values = merged_wheres - relation.bind_values = merged_binds + lhs_wheres = relation.where_values + rhs_wheres = values[:where] || [] + + lhs_binds = relation.bind_values + rhs_binds = values[:bind] || [] + + removed, kept = partition_overwrites(lhs_wheres, rhs_wheres) + + where_values = kept + rhs_wheres + bind_values = filter_binds(lhs_binds, removed) + rhs_binds + + conn = relation.klass.connection + bv_index = 0 + where_values.map! do |node| + if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right + substitute = conn.substitute_at(bind_values[bv_index].first, bv_index) + bv_index += 1 + Arel::Nodes::Equality.new(node.left, substitute) + else + node + end + end + + relation.where_values = where_values + relation.bind_values = bind_values if values[:reordering] # override any order specified in the original relation relation.reorder! values[:order] elsif values[:order] - # merge in order_values from r + # merge in order_values from relation relation.order! values[:order] end @@ -97,35 +146,28 @@ module ActiveRecord end end - def merged_binds - if values[:bind] - (relation.bind_values + values[:bind]).uniq(&:first) - else - relation.bind_values - end - end + def filter_binds(lhs_binds, removed_wheres) + return lhs_binds if removed_wheres.empty? - def merged_wheres - values[:where] ||= [] + set = Set.new removed_wheres.map { |x| x.left.name } + lhs_binds.dup.delete_if { |col,_| set.include? col.name } + end - if values[:where].empty? || relation.where_values.empty? - relation.where_values + values[:where] - else - # Remove equalities from the existing relation with a LHS which is - # present in the relation being merged in. + # Remove equalities from the existing relation with a LHS which is + # present in the relation being merged in. + # returns [things_to_remove, things_to_keep] + def partition_overwrites(lhs_wheres, rhs_wheres) + if lhs_wheres.empty? || rhs_wheres.empty? + return [[], lhs_wheres] + end - seen = Set.new - values[:where].each { |w| - if w.respond_to?(:operator) && w.operator == :== - seen << w.left - end - } + nodes = rhs_wheres.find_all do |w| + w.respond_to?(:operator) && w.operator == :== + end + seen = Set.new(nodes) { |node| node.left } - relation.where_values.reject { |w| - w.respond_to?(:operator) && - w.operator == :== && - seen.include?(w.left) - } + values[:where] + lhs_wheres.partition do |w| + w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left) end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index bd783a94cf..c60cd27a83 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,5 +1,20 @@ module ActiveRecord class PredicateBuilder # :nodoc: + @handlers = [] + + autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler' + autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler' + + def self.resolve_column_aliases(klass, hash) + hash = hash.dup + hash.keys.grep(Symbol) do |key| + if klass.attribute_alias? key + hash[klass.attribute_alias(key)] = hash.delete key + end + end + hash + end + def self.build_from_hash(klass, attributes, default_table) queries = [] @@ -40,7 +55,7 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && value.class < Base && reflection = klass.reflect_on_association(column.to_sym) + if klass && value.is_a?(Base) && reflection = klass.reflect_on_association(column.to_sym) if reflection.polymorphic? queries << build(table[reflection.foreign_type], value.class.base_class) end @@ -48,7 +63,7 @@ module ActiveRecord column = reflection.foreign_key end - queries << build(table[column.to_sym], value) + queries << build(table[column], value) queries end @@ -63,44 +78,36 @@ module ActiveRecord end.compact end + # Define how a class is converted to Arel nodes when passed to +where+. + # The handler can be any object that responds to +call+, and will be used + # for any value that +===+ the class given. For example: + # + # MyCustomDateRange = Struct.new(:start, :end) + # handler = proc do |column, range| + # Arel::Nodes::Between.new(column, + # Arel::Nodes::And.new([range.start, range.end]) + # ) + # end + # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler) + def self.register_handler(klass, handler) + @handlers.unshift([klass, handler]) + end + + register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) }) + # FIXME: I think we need to deprecate this behavior + register_handler(Class, ->(attribute, value) { attribute.eq(value.name) }) + register_handler(Base, ->(attribute, value) { attribute.eq(value.id) }) + register_handler(Range, ->(attribute, value) { attribute.in(value) }) + register_handler(Relation, RelationHandler.new) + register_handler(Array, ArrayHandler.new) + private def self.build(attribute, value) - case value - when Array - values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x} - ranges, values = values.partition {|v| v.is_a?(Range)} - - values_predicate = if values.include?(nil) - values = values.compact - - case values.length - when 0 - attribute.eq(nil) - when 1 - attribute.eq(values.first).or(attribute.eq(nil)) - else - attribute.in(values).or(attribute.eq(nil)) - end - else - attribute.in(values) - end + handler_for(value).call(attribute, value) + end - 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::Base - attribute.eq(value.id) - when Class - # FIXME: I think we need to deprecate this behavior - attribute.eq(value.name) - else - attribute.eq(value) - end + def self.handler_for(object) + @handlers.detect { |klass, _| klass === object }.last end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb new file mode 100644 index 0000000000..2f6c34ac08 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -0,0 +1,29 @@ +module ActiveRecord + class PredicateBuilder + class ArrayHandler # :nodoc: + def call(attribute, value) + values = value.map { |x| x.is_a?(Base) ? x.id : x } + ranges, values = values.partition { |v| v.is_a?(Range) } + + values_predicate = if values.include?(nil) + values = values.compact + + case values.length + when 0 + attribute.eq(nil) + when 1 + attribute.eq(values.first).or(attribute.eq(nil)) + else + attribute.in(values).or(attribute.eq(nil)) + end + else + attribute.in(values) + end + + array_predicates = ranges.map { |range| attribute.in(range) } + array_predicates << values_predicate + array_predicates.inject { |composite, predicate| composite.or(predicate) } + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb new file mode 100644 index 0000000000..618fa3cdd9 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb @@ -0,0 +1,13 @@ +module ActiveRecord + class PredicateBuilder + class RelationHandler # :nodoc: + def call(attribute, value) + if value.select_values.empty? + value = value.select(value.klass.arel_table[value.klass.primary_key]) + end + + attribute.in(value.arel.ast) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index b7960936cf..bffd8b5d0f 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -34,7 +34,6 @@ module ActiveRecord # # User.where.not(name: "Jon", role: "admin") # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' - # def not(opts, *rest) where_value = @scope.send(:build_where, opts, rest).map do |rel| case rel @@ -101,6 +100,14 @@ module ActiveRecord # firing an additional query. This will often result in a # performance improvement over a simple +join+. # + # You can also specify multiple relationships, like this: + # + # users = User.includes(:address, :friends) + # + # Loading nested relationships is possible using a Hash: + # + # users = User.includes(:address, friends: [:address, :followers]) + # # === conditions # # If you want to add conditions to your included models you'll have @@ -112,14 +119,15 @@ module ActiveRecord # # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) def includes(*args) - check_if_method_has_arguments!("includes", args) + check_if_method_has_arguments!(:includes, args) spawn.includes!(*args) end def includes!(*args) # :nodoc: - args.reject! {|a| a.blank? } + args.reject!(&:blank?) + args.flatten! - self.includes_values = (includes_values + args).flatten.uniq + self.includes_values |= args self end @@ -130,7 +138,7 @@ module ActiveRecord # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = # "users"."id" def eager_load(*args) - check_if_method_has_arguments!("eager_load", args) + check_if_method_has_arguments!(:eager_load, args) spawn.eager_load!(*args) end @@ -144,7 +152,7 @@ module ActiveRecord # User.preload(:posts) # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) def preload(*args) - check_if_method_has_arguments!("preload", args) + check_if_method_has_arguments!(:preload, args) spawn.preload!(*args) end @@ -162,14 +170,15 @@ module ActiveRecord # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN def references(*args) - check_if_method_has_arguments!("references", args) + check_if_method_has_arguments!(:references, args) spawn.references!(*args) end def references!(*args) # :nodoc: args.flatten! + args.map!(&:to_s) - self.references_values = (references_values + args.map!(&:to_s)).uniq + self.references_values |= args self end @@ -222,7 +231,9 @@ module ActiveRecord end def select!(*fields) # :nodoc: - self.select_values += fields.flatten + fields.flatten! + + self.select_values += fields self end @@ -242,7 +253,7 @@ module ActiveRecord # User.group('name AS grouped_name, age') # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] def group(*args) - check_if_method_has_arguments!("group", args) + check_if_method_has_arguments!(:group, args) spawn.group!(*args) end @@ -273,24 +284,14 @@ module ActiveRecord # User.order(:name, email: :desc) # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC def order(*args) - check_if_method_has_arguments!("order", args) + check_if_method_has_arguments!(:order, args) spawn.order!(*args) end def order!(*args) # :nodoc: - args.flatten! - validate_order_args args - - references = args.reject { |arg| Arel::Node === arg } - references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! - references!(references) if references.any? + preprocess_order_args(args) - # if a symbol is given we prepend the quoted table name - args = args.map { |arg| - arg.is_a?(Symbol) ? "#{quoted_table_name}.#{arg} ASC" : arg - } - - self.order_values = args + self.order_values + self.order_values += args self end @@ -302,15 +303,14 @@ module ActiveRecord # # User.order('email DESC').reorder('id ASC').order('name ASC') # - # generates a query with 'ORDER BY name ASC, id ASC'. + # generates a query with 'ORDER BY id ASC, name ASC'. def reorder(*args) - check_if_method_has_arguments!("reorder", args) + check_if_method_has_arguments!(:reorder, args) spawn.reorder!(*args) end def reorder!(*args) # :nodoc: - args.flatten! - validate_order_args args + preprocess_order_args(args) self.reordering_value = true self.order_values = args @@ -341,6 +341,9 @@ module ActiveRecord # User.where(name: "John", active: true).unscope(where: :name) # == User.where(active: true) # + # This method is applied before the default_scope is applied. So the conditions + # specified in default_scope will not be removed. + # # Note that this method is more generalized than ActiveRecord::SpawnMethods#except # because #except will only affect a particular relation's values. It won't wipe # the order, grouping, etc. when that relation is merged. For example: @@ -349,11 +352,11 @@ module ActiveRecord # # will still have an order if it comes from the default_scope on Comment. def unscope(*args) - check_if_method_has_arguments!("unscope", args) + check_if_method_has_arguments!(:unscope, args) spawn.unscope!(*args) end - def unscope!(*args) + def unscope!(*args) # :nodoc: args.flatten! args.each do |scope| @@ -388,8 +391,12 @@ module ActiveRecord # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id") # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id def joins(*args) - check_if_method_has_arguments!("joins", args) - spawn.joins!(*args.compact.flatten) + check_if_method_has_arguments!(:joins, args) + + args.compact! + args.flatten! + + spawn.joins!(*args) end def joins!(*args) # :nodoc: @@ -552,7 +559,6 @@ module ActiveRecord # Order.having('SUM(price) > 30').group('user_id') def having(opts, *rest) opts.blank? ? self : spawn.having!(opts, *rest) - spawn.having!(opts, *rest) end def having!(opts, *rest) # :nodoc: @@ -614,7 +620,7 @@ module ActiveRecord # # 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. + # array of records without querying 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. @@ -710,20 +716,22 @@ module ActiveRecord # User.select(:name) # # => Might return two records with the same name # - # User.select(:name).uniq - # # => Returns 1 record per unique name + # User.select(:name).distinct + # # => Returns 1 record per distinct name # - # User.select(:name).uniq.uniq(false) + # User.select(:name).distinct.distinct(false) # # => You can also remove the uniqueness - def uniq(value = true) - spawn.uniq!(value) + def distinct(value = true) + spawn.distinct!(value) end + alias uniq distinct - # Like #uniq, but modifies relation in place. - def uniq!(value = true) # :nodoc: - self.uniq_value = value + # Like #distinct, but modifies relation in place. + def distinct!(value = true) # :nodoc: + self.distinct_value = value self end + alias uniq! distinct! # Used to extend a scope with additional methods, either through # a module or through a block provided. @@ -770,9 +778,10 @@ module ActiveRecord end def extending!(*modules, &block) # :nodoc: - modules << Module.new(&block) if block_given? + modules << Module.new(&block) if block + modules.flatten! - self.extending_values += modules.flatten + self.extending_values += modules extend(*extending_values) if extending_values.any? self @@ -792,29 +801,29 @@ module ActiveRecord # Returns the Arel object associated with the relation. def arel - @arel ||= with_default_scope.build_arel + @arel ||= build_arel end # Like #arel, but ignores the default scope of the model. def build_arel arel = Arel::SelectManager.new(table.engine, table) - build_joins(arel, joins_values) unless joins_values.empty? + build_joins(arel, joins_values.flatten) unless joins_values.empty? 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(&:blank?)) unless having_values.empty? 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(&:blank?)) unless group_values.empty? build_order(arel) build_select(arel, select_values.uniq) - arel.distinct(uniq_value) + arel.distinct(distinct_value) arel.from(build_from) if from_value arel.lock(lock_value) if lock_value @@ -847,7 +856,7 @@ module ActiveRecord where_values.reject! do |rel| case rel - when Arel::Nodes::In, Arel::Nodes::Equality + when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) subrelation.name.to_sym == target_value_sym else @@ -857,13 +866,11 @@ module ActiveRecord end def custom_join_ast(table, joins) - joins = joins.reject { |join| join.blank? } + joins = joins.reject(&:blank?) return [] if joins.empty? - @implicit_readonly = true - - joins.map do |join| + joins.map! do |join| case join when Array join = Arel.sql(join.join(' ')) if array_of_strings?(join) @@ -875,21 +882,28 @@ module ActiveRecord end def collapse_wheres(arel, wheres) - equalities = wheres.grep(Arel::Nodes::Equality) - - arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty? - - (wheres - equalities).each do |where| + predicates = wheres.map do |where| + next where if ::Arel::Nodes::Equality === where where = Arel.sql(where) if String === where - arel.where(Arel::Nodes::Grouping.new(where)) + Arel::Nodes::Grouping.new(where) end + + arel.where(Arel::Nodes::And.new(predicates)) if predicates.present? end def build_where(opts, other = []) case opts when String, Array + #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113 + values = Hash === other.first ? other.first.values : other + + values.grep(ActiveRecord::Relation) do |rel| + self.bind_values += rel.bind_values + end + [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash + opts = PredicateBuilder.resolve_column_aliases(klass, opts) attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) attributes.values.grep(ActiveRecord::Relation) do |rel| @@ -907,6 +921,7 @@ module ActiveRecord case opts when Relation name ||= 'subquery' + self.bind_values = opts.bind_values + self.bind_values opts.arel.as(name.to_s) else opts @@ -920,7 +935,7 @@ module ActiveRecord :string_join when Hash, Symbol, Array :association_join - when ActiveRecord::Associations::JoinDependency::JoinAssociation + when ActiveRecord::Associations::JoinDependency :stashed_join when Arel::Nodes::Join :join_node @@ -932,9 +947,7 @@ module ActiveRecord association_joins = buckets[:association_join] || [] stashed_association_joins = buckets[:stashed_join] || [] join_nodes = (buckets[:join_node] || []).uniq - string_joins = (buckets[:string_join] || []).map { |x| - x.strip - }.uniq + string_joins = (buckets[:string_join] || []).map(&:strip).uniq join_list = join_nodes + custom_join_ast(manager, string_joins) @@ -944,23 +957,17 @@ module ActiveRecord join_list ) - join_dependency.graft(*stashed_association_joins) - - @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? + joins = join_dependency.join_constraints stashed_association_joins - # FIXME: refactor this to build an AST - join_dependency.join_associations.each do |association| - association.join_to(manager) - end + joins.each { |join| manager.from(join) } - manager.join_sources.concat join_list + manager.join_sources.concat(join_list) manager end def build_select(arel, selects) unless selects.empty? - @implicit_readonly = false arel.project(*selects) else arel.project(@klass.arel_table[Arel.star]) @@ -975,7 +982,7 @@ module ActiveRecord when Arel::Nodes::Ordering o.reverse when String - o.to_s.split(',').collect do |s| + o.to_s.split(',').map! do |s| s.strip! s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') end @@ -992,14 +999,15 @@ module ActiveRecord end def array_of_strings?(o) - o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} + o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) } end def build_order(arel) - orders = order_values + orders = order_values.uniq + orders.reject!(&:blank?) orders = reverse_sql_order(orders) if reverse_order_value - orders = orders.uniq.reject(&:blank?).flat_map do |order| + orders = orders.flat_map do |order| case order when Symbol table[order].asc @@ -1014,13 +1022,27 @@ module ActiveRecord end def validate_order_args(args) - args.select { |a| Hash === a }.each do |h| + args.grep(Hash) do |h| unless (h.values - [:asc, :desc]).empty? raise ArgumentError, 'Direction should be :asc or :desc' end end end + def preprocess_order_args(order_args) + order_args.flatten! + validate_order_args(order_args) + + references = order_args.grep(String) + references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! + references!(references) if references.any? + + # if a symbol is given we prepend the quoted table name + order_args.map! do |arg| + arg.is_a?(Symbol) ? Arel::Nodes::Ascending.new(klass.arel_table[arg]) : arg + end + end + # Checks to make sure that the arguments are not blank. Note that if some # blank-like object were initially passed into the query method, then this # method will not raise an error. diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index de784f9f57..2552cbd234 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -64,8 +64,7 @@ module ActiveRecord private def relation_with(values) # :nodoc: - result = Relation.new(klass, table, values) - result.default_scoped = default_scoped + result = Relation.create(klass, table, values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index bea195e9b8..1dc3bf3f12 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -3,11 +3,36 @@ module ActiveRecord # This class encapsulates a Result returned from calling +exec_query+ on any # database connection adapter. For example: # - # x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo') - # x # => #<ActiveRecord::Result:0xdeadbeef> + # result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts') + # result # => #<ActiveRecord::Result:0xdeadbeef> + # + # # Get the column names of the result: + # result.columns + # # => ["id", "title", "body"] + # + # # Get the record values of the result: + # result.rows + # # => [[1, "title_1", "body_1"], + # [2, "title_2", "body_2"], + # ... + # ] + # + # # Get an array of hashes representing the result (column => value): + # result.to_hash + # # => [{"id" => 1, "title" => "title_1", "body" => "body_1"}, + # {"id" => 2, "title" => "title_2", "body" => "body_2"}, + # ... + # ] + # + # # ActiveRecord::Result also includes Enumerable. + # result.each do |row| + # puts row['title'] + " " + row['body'] + # end class Result include Enumerable + IDENTITY_TYPE = Class.new { def type_cast(v); v; end }.new # :nodoc: + attr_reader :columns, :rows, :column_types def initialize(columns, rows, column_types = {}) @@ -17,8 +42,20 @@ module ActiveRecord @column_types = column_types end + def identity_type # :nodoc: + IDENTITY_TYPE + end + + def column_type(name) + @column_types[name] || identity_type + end + def each - hash_rows.each { |row| yield row } + if block_given? + hash_rows.each { |row| yield row } + else + hash_rows.to_enum + end end def to_hash @@ -52,6 +89,7 @@ module ActiveRecord end private + def hash_rows @hash_rows ||= begin @@ -59,7 +97,21 @@ module ActiveRecord # used as keys in ActiveRecord::Base's @attributes hash columns = @columns.map { |c| c.dup.freeze } @rows.map { |row| - Hash[columns.zip(row)] + # In the past we used Hash[columns.zip(row)] + # though elegant, the verbose way is much more efficient + # both time and memory wise cause it avoids a big array allocation + # this method is called a lot and needs to be micro optimised + hash = {} + + index = 0 + length = columns.length + + while index < length + hash[columns[index]] = row[index] + index += 1 + end + + hash } end end diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb new file mode 100644 index 0000000000..63e6738622 --- /dev/null +++ b/activerecord/lib/active_record/runtime_registry.rb @@ -0,0 +1,17 @@ +require 'active_support/per_thread_registry' + +module ActiveRecord + # This is a thread locals registry for Active Record. For example: + # + # ActiveRecord::RuntimeRegistry.connection_handler + # + # returns the connection handler local to the current thread. + # + # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # for further details. + class RuntimeRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + attr_accessor :connection_handler, :sql_runtime, :connection_id + end +end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 3c5b871e99..cab8fd745a 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -3,8 +3,8 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - def quote_value(value, column = nil) #:nodoc: - connection.quote(value,column) + def quote_value(value, column) #:nodoc: + connection.quote(value, column) end # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. @@ -86,10 +86,11 @@ module ActiveRecord # { address: Address.new("123 abc st.", "chicago") } # # => "address_street='123 abc st.' and address_city='chicago'" def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) + attrs = PredicateBuilder.resolve_column_aliases self, attrs attrs = expand_hash_conditions_for_aggregates(attrs) table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) - PredicateBuilder.build_from_hash(self.class, attrs, table).map { |b| + PredicateBuilder.build_from_hash(self, attrs, table).map { |b| connection.visitor.accept b }.join(' AND ') end @@ -126,7 +127,17 @@ module ActiveRecord raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) bound = values.dup c = connection - statement.gsub('?') { quote_bound_value(bound.shift, c) } + statement.gsub('?') do + replace_bind_variable(bound.shift, c) + end + end + + def replace_bind_variable(value, c = connection) #:nodoc: + if ActiveRecord::Relation === value + value.to_sql + else + quote_bound_value(value, c) + end end def replace_named_bind_variables(statement, bind_vars) #:nodoc: @@ -134,7 +145,7 @@ module ActiveRecord if $1 == ':' # skip postgresql casts $& # return the whole match elsif bind_vars.include?(match = $2.to_sym) - quote_bound_value(bind_vars[match]) + replace_bind_variable(bind_vars[match]) else raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 3259dbbd80..4bfd0167a4 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -43,7 +43,7 @@ module ActiveRecord unless info[:version].blank? initialize_schema_migrations_table - assume_migrated_upto_version(info[:version], migrations_paths) + connection.assume_migrated_upto_version(info[:version], migrations_paths) end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index df090b972d..e055d571ab 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -17,9 +17,19 @@ module ActiveRecord cattr_accessor :ignore_tables @@ignore_tables = [] - def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT) - new(connection).dump(stream) - stream + class << self + def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base) + new(connection, generate_options(config)).dump(stream) + stream + end + + private + def generate_options(config) + { + table_name_prefix: config.table_name_prefix, + table_name_suffix: config.table_name_suffix + } + end end def dump(stream) @@ -32,10 +42,11 @@ module ActiveRecord private - def initialize(connection) + def initialize(connection, options = {}) @connection = connection @types = @connection.native_database_types @version = Migrator::current_version rescue nil + @options = options end def header(stream) @@ -106,9 +117,13 @@ HEADER end tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" - if columns.detect { |c| c.name == pk } + pkcol = columns.detect { |c| c.name == pk } + if pkcol if pk != 'id' tbl.print %Q(, primary_key: "#{pk}") + elsif pkcol.sql_type == 'uuid' + tbl.print ", id: :uuid" + tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function end else tbl.print ", id: false" @@ -118,7 +133,7 @@ HEADER # then dump all non-primary key columns column_specs = columns.map do |column| - raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil? + raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) next if column.name == pk @connection.column_spec(column, @types) end.compact @@ -185,6 +200,10 @@ HEADER statement_parts << ('where: ' + index.where.inspect) if index.where + statement_parts << ('using: ' + index.using.inspect) if index.using + + statement_parts << ('type: ' + index.type.inspect) if index.type + ' ' + statement_parts.join(', ') end @@ -194,7 +213,7 @@ HEADER end def remove_prefix_and_suffix(table) - table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2") + table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[: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 9830abe7d8..a9d164e366 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -4,28 +4,37 @@ require 'active_record/base' module ActiveRecord class SchemaMigration < ActiveRecord::Base + class << self - def self.table_name - "#{Base.table_name_prefix}schema_migrations#{Base.table_name_suffix}" - end + def table_name + "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" + end - def self.index_name - "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" - end + def index_name + "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" + end - def self.create_table - unless connection.table_exists?(table_name) - connection.create_table(table_name, :id => false) do |t| - t.column :version, :string, :null => false + def table_exists? + connection.table_exists?(table_name) + end + + def create_table(limit=nil) + unless table_exists? + version_options = {null: false} + version_options[:limit] = limit if limit + + connection.create_table(table_name, id: false) do |t| + t.column :version, :string, version_options + end + connection.add_index table_name, :version, unique: true, name: index_name end - 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 => index_name - connection.drop_table(table_name) + def drop_table + if table_exists? + connection.remove_index table_name, name: index_name + connection.drop_table(table_name) + end end end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 9746b1c3c2..0cf3d59985 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -1,3 +1,5 @@ +require 'active_support/per_thread_registry' + module ActiveRecord module Scoping extend ActiveSupport::Concern @@ -9,11 +11,11 @@ module ActiveRecord module ClassMethods def current_scope #:nodoc: - Thread.current["#{self}_current_scope"] + ScopeRegistry.value_for(:current_scope, base_class.to_s) end def current_scope=(scope) #:nodoc: - Thread.current["#{self}_current_scope"] = scope + ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope) end end @@ -24,5 +26,57 @@ module ActiveRecord send("#{att}=", value) if respond_to?("#{att}=") end end + + # This class stores the +:current_scope+ and +:ignore_default_scope+ values + # for different classes. The registry is stored as a thread local, which is + # accessed through +ScopeRegistry.current+. + # + # This class allows you to store and get the scope values on different + # classes and different types of scopes. For example, if you are attempting + # to get the current_scope for the +Board+ model, then you would use the + # following code: + # + # registry = ActiveRecord::Scoping::ScopeRegistry + # registry.set_value_for(:current_scope, "Board", some_new_scope) + # + # Now when you run: + # + # registry.value_for(:current_scope, "Board") + # + # You will obtain whatever was defined in +some_new_scope+. The +value_for+ + # and +set_value_for+ methods are delegated to the current +ScopeRegistry+ + # object, so the above example code can also be called as: + # + # ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope, + # "Board", some_new_scope) + class ScopeRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + VALID_SCOPE_TYPES = [:current_scope, :ignore_default_scope] + + def initialize + @registry = Hash.new { |hash, key| hash[key] = {} } + end + + # Obtains the value for a given +scope_name+ and +variable_name+. + def value_for(scope_type, variable_name) + raise_invalid_scope_type!(scope_type) + @registry[scope_type][variable_name] + end + + # Sets the +value+ for a given +scope_type+ and +variable_name+. + def set_value_for(scope_type, variable_name, value) + raise_invalid_scope_type!(scope_type) + @registry[scope_type][variable_name] = value + end + + private + + def raise_invalid_scope_type!(scope_type) + if !VALID_SCOPE_TYPES.include?(scope_type) + raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES" + end + end + end end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 5bd481082e..01fec31544 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -5,7 +5,8 @@ module ActiveRecord included do # Stores the default scope for the class. - class_attribute :default_scopes, instance_writer: false + class_attribute :default_scopes, instance_writer: false, instance_predicate: false + self.default_scopes = [] end @@ -27,14 +28,6 @@ module ActiveRecord # Post.unscoped { # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } - # - # It is recommended that you use the block form of unscoped because - # chaining unscoped with +scope+ does not work. Assuming that - # +published+ is a +scope+, the following two statements - # are equal: the +default_scope+ is applied on both. - # - # Post.unscoped.published - # Post.published def unscoped block_given? ? relation.scoping { yield } : relation end @@ -90,12 +83,11 @@ module ActiveRecord 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 " \ + raise ArgumentError, + "Support for calling #default_scope without a block is removed. 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 += [scope] @@ -108,22 +100,18 @@ module ActiveRecord elsif default_scopes.any? evaluate_default_scope do default_scopes.inject(relation) do |default_scope, scope| - if !scope.is_a?(Relation) && scope.respond_to?(:call) - default_scope.merge(unscoped { scope.call }) - else - default_scope.merge(scope) - end + default_scope.merge(unscoped { scope.call }) end end end end def ignore_default_scope? # :nodoc: - Thread.current["#{self}_ignore_default_scope"] + ScopeRegistry.value_for(:ignore_default_scope, self) end def ignore_default_scope=(ignore) # :nodoc: - Thread.current["#{self}_ignore_default_scope"] = ignore + ScopeRegistry.set_value_for(:ignore_default_scope, self, ignore) end # The ignore_default_scope flag is used to prevent an infinite recursion diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 12317601b6..2a5718f388 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -25,22 +25,18 @@ module ActiveRecord if current_scope current_scope.clone else - scope = relation - scope.default_scoped = true - scope + default_scoped end end + def default_scoped # :nodoc: + relation.merge(build_default_scope) + end + # Collects attributes from scopes that should be applied when creating # an AR instance for the particular class this is called on. def scope_attributes # :nodoc: - if current_scope - current_scope.scope_for_create - else - scope = relation - scope.default_scoped = true - scope.scope_for_create - end + all.scope_for_create end # Are there default attributes associated with this scope? @@ -145,31 +141,9 @@ module ActiveRecord def scope(name, body, &block) extension = Module.new(&block) if block - # 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(:define_method, name) do |*args| - if body.respond_to?(:call) - scope = extension ? body.call(*args).extending(extension) : body.call(*args) - - if scope - default_scoped = scope.default_scoped - scope = relation.merge(scope) - scope.default_scoped = default_scoped - end - else - scope = body - end + scope = all.scoping { body.call(*args) } + scope = scope.extending(extension) if extension scope || all end diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb new file mode 100644 index 0000000000..dd4ee0c4a0 --- /dev/null +++ b/activerecord/lib/active_record/statement_cache.rb @@ -0,0 +1,26 @@ +module ActiveRecord + + # Statement cache is used to cache a single statement in order to avoid creating the AST again. + # Initializing the cache is done by passing the statement in the initialization block: + # + # cache = ActiveRecord::StatementCache.new do + # Book.where(name: "my book").limit(100) + # end + # + # The cached statement is executed by using the +execute+ method: + # + # cache.execute + # + # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped. + # Database is queried when +to_a+ is called on the relation. + class StatementCache + def initialize + @relation = yield + raise ArgumentError.new("Statement cannot be nil") if @relation.nil? + end + + def execute + @relation.dup.to_a + end + end +end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index a610f479f2..b841b977fc 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -86,6 +86,9 @@ module ActiveRecord end end + # assign new store attribute and create new hash to ensure that each class in the hierarchy + # has its own hash of stored attributes. + self.stored_attributes = {} if self.stored_attributes.blank? self.stored_attributes[store_attribute] ||= [] self.stored_attributes[store_attribute] |= keys end @@ -101,26 +104,58 @@ module ActiveRecord protected def read_store_attribute(store_attribute, key) - attribute = initialize_store_attribute(store_attribute) - attribute[key] + accessor = store_accessor_for(store_attribute) + accessor.read(self, store_attribute, key) end def write_store_attribute(store_attribute, key, value) - attribute = initialize_store_attribute(store_attribute) - if value != attribute[key] - send :"#{store_attribute}_will_change!" - attribute[key] = value - end + accessor = store_accessor_for(store_attribute) + accessor.write(self, store_attribute, key, value) end private - def initialize_store_attribute(store_attribute) - attribute = send(store_attribute) - unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) - attribute = IndifferentCoder.as_indifferent_hash(attribute) - send :"#{store_attribute}=", attribute + def store_accessor_for(store_attribute) + @column_types[store_attribute.to_s].accessor + end + + class HashAccessor + def self.read(object, attribute, key) + prepare(object, attribute) + object.public_send(attribute)[key] + end + + def self.write(object, attribute, key, value) + prepare(object, attribute) + if value != read(object, attribute, key) + object.public_send :"#{attribute}_will_change!" + object.public_send(attribute)[key] = value + end + end + + def self.prepare(object, attribute) + object.public_send :"#{attribute}=", {} unless object.send(attribute) + end + end + + class StringKeyedHashAccessor < HashAccessor + def self.read(object, attribute, key) + super object, attribute, key.to_s + end + + def self.write(object, attribute, key, value) + super object, attribute, key.to_s, value + end + end + + class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor + def self.prepare(object, store_attribute) + attribute = object.send(store_attribute) + unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) + attribute = IndifferentCoder.as_indifferent_hash(attribute) + object.send :"#{store_attribute}=", attribute + end + attribute end - attribute end class IndifferentCoder # :nodoc: @@ -138,7 +173,7 @@ module ActiveRecord end def load(yaml) - self.class.as_indifferent_hash @coder.load(yaml) + self.class.as_indifferent_hash(@coder.load(yaml)) end def self.as_indifferent_hash(obj) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 4fa7cf8a7d..be7d496d15 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -3,10 +3,42 @@ module ActiveRecord class DatabaseAlreadyExists < StandardError; end # :nodoc: class DatabaseNotSupported < StandardError; end # :nodoc: - module DatabaseTasks # :nodoc: + # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates + # logic behind common tasks used to manage database and migrations. + # + # The tasks defined here are used in rake tasks provided by Active Record. + # + # In order to use DatabaseTasks, a few config values need to be set. All the needed + # config values are set by Rails already, so it's necessary to do it only if you + # want to change the defaults or when you want to use Active Record outside of Rails + # (in such case after configuring the database tasks, you can also use the rake tasks + # defined in Active Record). + # + # + # The possible config values are: + # + # * +env+: current environment (like Rails.env). + # * +database_configuration+: configuration of your databases (as in +config/database.yml+). + # * +db_dir+: your +db+ directory. + # * +fixtures_path+: a path to fixtures directory. + # * +migrations_paths+: a list of paths to directories with migrations. + # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method. + # * +root+: a path to the root of the application. + # + # Example usage of +DatabaseTasks+ outside Rails could look as such: + # + # include ActiveRecord::Tasks + # DatabaseTasks.database_configuration = YAML.load(File.read('my_database_config.yml')) + # DatabaseTasks.db_dir = 'db' + # # other settings... + # + # DatabaseTasks.create_current('production') + module DatabaseTasks extend self attr_writer :current_config + attr_accessor :database_configuration, :migrations_paths, :seed_loader, :db_dir, + :fixtures_path, :env, :root LOCAL_HOSTS = ['127.0.0.1', 'localhost'] @@ -15,12 +47,12 @@ module ActiveRecord @tasks[pattern] = task end - register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks) - register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) - register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) + register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks) + register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) + register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) def current_config(options = {}) - options.reverse_merge! :env => Rails.env + options.reverse_merge! :env => env if options.has_key?(:config) @current_config = options[:config] else @@ -46,7 +78,7 @@ module ActiveRecord each_local_configuration { |configuration| create configuration } end - def create_current(environment = Rails.env) + def create_current(environment = env) each_current_configuration(environment) { |configuration| create configuration } @@ -69,7 +101,7 @@ module ActiveRecord each_local_configuration { |configuration| drop configuration } end - def drop_current(environment = Rails.env) + def drop_current(environment = env) each_current_configuration(environment) { |configuration| drop configuration } @@ -79,7 +111,7 @@ module ActiveRecord drop database_url_config end - def charset_current(environment = Rails.env) + def charset_current(environment = env) charset ActiveRecord::Base.configurations[environment] end @@ -88,7 +120,7 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(*arguments).charset end - def collation_current(environment = Rails.env) + def collation_current(environment = env) collation ActiveRecord::Base.configurations[environment] end @@ -113,6 +145,24 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) end + def check_schema_file(filename) + unless File.exist?(filename) + message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} + message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails) + Kernel.abort message + end + end + + def load_seed + if seed_loader + seed_loader.load_seed + else + raise "You tried to load seed data, but no seed loader is specified. Please specify seed " + + "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" + + "Seed loader should respond to load_seed method" + end + end + private def database_url_config @@ -130,7 +180,7 @@ module ActiveRecord def each_current_configuration(environment) environments = [environment] - environments << 'test' if environment.development? + environments << 'test' if environment == 'development' configurations = ActiveRecord::Base.configurations.values_at(*environments) configurations.compact.each do |configuration| diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 10696258c9..c755831e6d 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -26,7 +26,9 @@ module ActiveRecord $stdout.print error.error establish_connection root_configuration_without_database connection.create_database configuration['database'], creation_options - connection.execute grant_statement.gsub(/\s+/, ' ').strip + if configuration['username'] != 'root' + connection.execute grant_statement.gsub(/\s+/, ' ').strip + end establish_connection configuration else $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" @@ -132,8 +134,9 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; args << "--password=#{configuration['password']}" if configuration['password'] args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding'] configuration.slice('host', 'port', 'socket').each do |k, v| - args.concat([ "--#{k}", v ]) if v + args.concat([ "--#{k}", v.to_s ]) if v end + args end end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 0b1b030516..3d02ee07d0 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -59,7 +59,7 @@ module ActiveRecord def structure_load(filename) set_psql_env - Kernel.system("psql -f #{filename} #{configuration['database']}") + Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}") end private diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index de8b16627e..5688931db2 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -3,7 +3,7 @@ module ActiveRecord class SQLiteDatabaseTasks # :nodoc: delegate :connection, :establish_connection, to: ActiveRecord::Base - def initialize(configuration, root = Rails.root) + def initialize(configuration, root = ActiveRecord::Tasks::DatabaseTasks.root) @configuration, @root = configuration, root end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb deleted file mode 100644 index e9142481a3..0000000000 --- a/activerecord/lib/active_record/test_case.rb +++ /dev/null @@ -1,96 +0,0 @@ -require 'active_support/test_case' - -ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase') -module ActiveRecord - # = Active Record Test Case - # - # Defines some test assertions to test against SQL queries. - class TestCase < ActiveSupport::TestCase #:nodoc: - def teardown - SQLCounter.clear_log - end - - def assert_date_from_db(expected, actual, message = nil) - # SybaseAdapter doesn't have a separate column type just for dates, - # so the time is in the string and incorrectly formatted - if current_adapter?(:SybaseAdapter) - assert_equal expected.to_s, actual.to_date.to_s, message - else - assert_equal expected.to_s, actual.to_s, message - end - end - - def assert_sql(*patterns_to_match) - SQLCounter.clear_log - yield - SQLCounter.log_all - ensure - failed_patterns = [] - patterns_to_match.each do |pattern| - 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, options = {}) - ignore_none = options.fetch(:ignore_none) { num == :any } - SQLCounter.clear_log - yield - ensure - 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) - assert_queries(0, :ignore_none => true, &block) - end - - end - - class SQLCounter - class << self - attr_accessor :ignored_sql, :log, :log_all - def clear_log; self.log = []; self.log_all = []; end - end - - self.clear_log - - self.ignored_sql = [/^PRAGMA/, /^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, 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, /^SHOW search_path/i] - sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] - - [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| - ignored_sql.concat db_ignored_sql - end - - attr_reader :ignore - - def initialize(ignore = Regexp.union(self.class.ignored_sql)) - @ignore = ignore - end - - def call(name, start, finish, message_id, values) - sql = values[:sql] - - # FIXME: this seems bad. we should probably have a better way to indicate - # the query was cached - return if 'CACHE' == values[:name] - - self.class.log_all << sql - self.class.log << sql unless ignore =~ sql - end - end - - ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) -end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index ae99cff35e..9253150c4f 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -10,9 +10,9 @@ module ActiveRecord # # config.active_record.record_timestamps = false # - # Timestamps are in the local timezone by default but you can use UTC by setting: + # Timestamps are in UTC by default but you can use the local timezone by setting: # - # config.active_record.default_timezone = :utc + # config.active_record.default_timezone = :local # # == Time Zone aware attributes # diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 33718ef0e9..45313b5e75 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -10,7 +10,9 @@ module ActiveRecord end included do - define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name] + define_callbacks :commit, :rollback, + terminator: ->(_, result) { result == false }, + scope: [:kind, :name] end # = Active Record Transactions @@ -160,7 +162,7 @@ module ActiveRecord # end # end # - # only "Kotori" is created. (This works on MySQL and PostgreSQL, but not on SQLite3.) + # only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it. # # Most databases don't support true nested transactions. At the time of # writing, the only database that we're aware of that supports true nested @@ -218,8 +220,8 @@ module ActiveRecord # after_commit :do_bar, on: :update # after_commit :do_baz, on: :destroy # - # after_commit :do_foo_bar, :on [:create, :update] - # after_commit :do_bar_baz, :on [:update, :destroy] + # after_commit :do_foo_bar, on: [:create, :update] + # after_commit :do_bar_baz, on: [:update, :destroy] # # 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. @@ -243,7 +245,7 @@ module ActiveRecord if options.is_a?(Hash) && options[:on] assert_valid_transaction_action(options[:on]) options[:if] = Array(options[:if]) - fire_on = Array(options[:on]).map(&:to_sym) + fire_on = Array(options[:on]) options[:if] << "transaction_include_any_action?(#{fire_on})" end end @@ -286,25 +288,26 @@ module ActiveRecord clear_transaction_record_state end - # Call the after_commit callbacks + # Call the +after_commit+ callbacks. # # Ensure that it is not called if the object was never persisted (failed create), - # but call it after the commit of a destroyed object + # but call it after the commit of a destroyed object. def committed! #:nodoc: run_callbacks :commit if destroyed? || persisted? ensure clear_transaction_record_state end - # Call the after rollback callbacks. The restore_state argument indicates if the record + # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state = false) #:nodoc: run_callbacks :rollback ensure restore_transaction_record_state(force_restore_state) + clear_transaction_record_state end - # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks + # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks # can be called. def add_to_transaction if self.class.connection.add_transaction_record(self) @@ -339,8 +342,12 @@ 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[:id] = id if has_attribute?(self.class.primary_key) - @_start_transaction_state[:new_record] = @new_record - @_start_transaction_state[:destroyed] = @destroyed + 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[:level] = (@_start_transaction_state[:level] || 0) + 1 @_start_transaction_state[:frozen?] = @attributes.frozen? end @@ -354,8 +361,8 @@ module ActiveRecord # 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: unless @_start_transaction_state.empty? - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - if @_start_transaction_state[:level] < 1 || force + transaction_level = (@_start_transaction_state[:level] || 0) - 1 + if transaction_level < 1 || force restore_state = @_start_transaction_state was_frozen = restore_state[:frozen?] @attributes = @attributes.dup if @attributes.frozen? @@ -368,7 +375,6 @@ module ActiveRecord @attributes_cache.delete(self.class.primary_key) end @attributes.freeze if was_frozen - @_start_transaction_state.clear end end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 3706885881..26dca415ff 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -74,8 +74,7 @@ module ActiveRecord protected def perform_validations(options={}) # :nodoc: - perform_validation = options[:validate] != false - perform_validation ? valid?(options[:context]) : true + options[:validate] == false || valid?(options[:context]) end end end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 7f1972ccf9..b4785d3ba4 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -2,15 +2,15 @@ module ActiveRecord module Validations 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? + if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any? record.errors.add(attribute, :invalid, options.merge(:value => value)) end end 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. + # Works with any kind of association. # # class Book < ActiveRecord::Base # has_many :pages diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index a705d8c2c4..b55af692d6 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -7,11 +7,7 @@ module ActiveRecord "Pass a callable instead: `conditions: -> { where(approved: true) }`" end super({ case_sensitive: true }.merge!(options)) - end - - # Unfortunately, we have to tie Uniqueness validators to a class. - def setup(klass) - @klass = klass + @klass = options[:class] end def validate_each(record, attribute, value) @@ -34,7 +30,6 @@ module ActiveRecord end protected - # The check for an existing value should be run from a class that # isn't abstract. This means working down from the current class # (self), to the first non-abstract class. Since classes don't know @@ -202,8 +197,8 @@ module ActiveRecord # will result in the default Rails exception page being shown), or you # can catch it and restart the transaction (e.g. by telling the user # that the title already exists, and asking him to re-enter the title). - # This technique is also known as optimistic concurrency control: - # http://en.wikipedia.org/wiki/Optimistic_concurrency_control. + # This technique is also known as + # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control]. # # The bundled ActiveRecord::ConnectionAdapters distinguish unique index # constraint errors from other types of database errors by throwing an diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index c0471bb506..de5fd05468 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,10 +1,11 @@ module ActiveRecord - module VERSION #:nodoc: - MAJOR = 4 - MINOR = 0 - TINY = 0 - PRE = "beta1" + # Returns the version of the currently loaded ActiveRecord as a Gem::Version + def self.version + Gem::Version.new "4.1.0.beta" + end - STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') + module VERSION #:nodoc: + MAJOR, MINOR, TINY, PRE = ActiveRecord.version.segments + STRING = ActiveRecord.version.to_s end end diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb index c8aa37f275..dc29213235 100644 --- a/activerecord/lib/rails/generators/active_record.rb +++ b/activerecord/lib/rails/generators/active_record.rb @@ -1,23 +1,17 @@ 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 # :nodoc: class Base < Rails::Generators::NamedBase # :nodoc: - include Rails::Generators::Migration + include ActiveRecord::Generators::Migration # Set the current directory as base for the inherited generators. def self.base_root File.dirname(__FILE__) end - - # Implement the required interface for Rails::Generators::Migration. - def self.next_migration_number(dirname) - next_migration_number = current_migration_number(dirname) + 1 - ActiveRecord::Migration.next_migration_number(next_migration_number) - end end end end diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb new file mode 100644 index 0000000000..b7418cf42f --- /dev/null +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -0,0 +1,18 @@ +require 'rails/generators/migration' + +module ActiveRecord + module Generators # :nodoc: + module Migration + extend ActiveSupport::Concern + include Rails::Generators::Migration + + module ClassMethods + # Implement the required interface for Rails::Generators::Migration. + def next_migration_number(dirname) + next_migration_number = current_migration_number(dirname) + 1 + ActiveRecord::Migration.next_migration_number(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 b967bb6e0f..3968acba64 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -14,6 +14,10 @@ module ActiveRecord protected attr_reader :migration_action, :join_tables + # sets the default migration template that is being used for the generation of the migration + # depending on the arguments which would be sent out in the command line, the migration template + # and the table name instance variables are setup. + def set_local_assigns! @migration_template = "migration.rb" case file_name diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index 3a3cf86d73..fd94a2d038 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -2,8 +2,12 @@ class <%= migration_class_name %> < ActiveRecord::Migration def change create_table :<%= table_name %> do |t| <% attributes.each do |attribute| -%> +<% if attribute.password_digest? -%> + t.string :password_digest<%= attribute.inject_options %> +<% else -%> t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> <% end -%> +<% end -%> <% if options[:timestamps] %> t.timestamps <% 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 40e134e626..7e8d68ce69 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -12,6 +12,9 @@ module ActiveRecord class_option :parent, :type => :string, :desc => "The parent class for the generated model" class_option :indexes, :type => :boolean, :default => true, :desc => "Add indexes for references and belongs_to columns" + + # creates the migration file for the model. + 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 @@ -39,6 +42,7 @@ module ActiveRecord protected + # Used by the migration template to determine the parent name of the model def parent_class_name options[:parent] || "ActiveRecord::Base" end diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 056f55470c..808598699b 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,10 @@ <% module_namespacing do -%> class <%= class_name %> < <%= parent_class_name.classify %> -<% attributes.select {|attr| attr.reference? }.each do |attribute| -%> +<% attributes.select(&:reference?).each do |attribute| -%> belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %> <% end -%> +<% if attributes.any?(&:password_digest?) -%> + has_secure_password +<% end -%> end <% end -%> diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 0af7cbf74f..dd355e8d0c 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -114,7 +114,7 @@ module ActiveRecord end end - # test resetting sequences in odd tables in postgreSQL + # test resetting sequences in odd tables in PostgreSQL if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) require 'models/movie' require 'models/subscriber' @@ -167,12 +167,17 @@ module ActiveRecord else @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" end - # should deleted created record as otherwise disable_referential_integrity will try to enable contraints after executed block + # should delete created record as otherwise disable_referential_integrity will try to enable constraints after executed block # and will fail (at least on Oracle) @connection.execute "DELETE FROM fk_test_has_fk" end end end + + def test_select_all_always_return_activerecord_result + result = @connection.select_all "SELECT * FROM posts" + assert result.is_a?(ActiveRecord::Result) + end end class AdapterTestWithoutTransaction < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 8812cf1b7d..0878925a6c 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -2,40 +2,61 @@ require "cases/helper" class ActiveSchemaTest < ActiveRecord::TestCase def setup - ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do + @connection = ActiveRecord::Base.remove_connection + ActiveRecord::Base.establish_connection(@connection) + + ActiveRecord::Base.connection.singleton_class.class_eval do alias_method :execute_without_stub, :execute - remove_method :execute def execute(sql, name = nil) return sql end end end def teardown - ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do - remove_method :execute - alias_method :execute, :execute_without_stub - end + ActiveRecord::Base.remove_connection + ActiveRecord::Base.establish_connection(@connection) end def test_add_index # add_index calls index_name_exists? which can't work since execute is stubbed - ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:define_method, :index_name_exists?) do |*| - false - end - expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)" + def (ActiveRecord::Base.connection).index_name_exists?(*); false; end + + expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " assert_equal expected, add_index(:people, :last_name, :length => nil) - expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10))" + expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10)) " assert_equal expected, add_index(:people, :last_name, :length => 10) - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15))" + expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) " assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15) - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`)" + expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) " assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15}) - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10))" + expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) " assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10}) - ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:remove_method, :index_name_exists?) + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) " + assert_equal expected, add_index(:people, :last_name, :type => type) + end + + %w(btree hash).each do |using| + expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) " + assert_equal expected, add_index(:people, :last_name, :using => using) + end + + expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) " + assert_equal expected, add_index(:people, :last_name, :length => 10, :using => :btree) + + expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) ALGORITHM = COPY" + assert_equal expected, add_index(:people, :last_name, :length => 10, using: :btree, algorithm: :copy) + + assert_raise ArgumentError do + add_index(:people, :last_name, algorithm: :coyp) + end + + expected = "CREATE INDEX `index_people_on_last_name_and_first_name` USING btree ON `people` (`last_name`(15), `first_name`(15)) " + assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree) end def test_drop_table @@ -70,8 +91,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase def test_add_timestamps with_real_execute do begin - ActiveRecord::Base.connection.create_table :delete_me do |t| - end + ActiveRecord::Base.connection.create_table :delete_me ActiveRecord::Base.connection.add_timestamps :delete_me assert column_present?('delete_me', 'updated_at', 'datetime') assert column_present?('delete_me', 'created_at', 'datetime') @@ -98,22 +118,20 @@ class ActiveSchemaTest < ActiveRecord::TestCase private def with_real_execute - #we need to actually modify some data, so we make execute point to the original method - ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do + ActiveRecord::Base.connection.singleton_class.class_eval do alias_method :execute_with_stub, :execute remove_method :execute alias_method :execute, :execute_without_stub end + yield ensure - #before finishing, we restore the alias to the mock-up method - ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do + ActiveRecord::Base.connection.singleton_class.class_eval do remove_method :execute alias_method :execute, :execute_with_stub end end - def method_missing(method_symbol, *arguments) ActiveRecord::Base.connection.send(method_symbol, *arguments) end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index b965983fec..1844a2e0dc 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -17,7 +17,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase end def test_connect_with_url - run_without_connection do |orig| + run_without_connection do ar_config = ARTest.connection_config['arunit'] skip "This test doesn't work with custom socket location" if ar_config['socket'] diff --git a/activerecord/test/cases/adapters/mysql/enum_test.rb b/activerecord/test/cases/adapters/mysql/enum_test.rb index 40af317ad1..f4e7a3ef0a 100644 --- a/activerecord/test/cases/adapters/mysql/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql/enum_test.rb @@ -5,6 +5,6 @@ class MysqlEnumTest < ActiveRecord::TestCase end def test_enum_limit - assert_equal 5, EnumTest.columns.first.limit + assert_equal 6, 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 0eb1cc511e..9ad0744aee 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -16,6 +16,15 @@ module ActiveRecord eosql end + def test_valid_column + column = @conn.columns('ex').find { |col| col.name == 'id' } + assert @conn.valid_type?(column.type) + end + + def test_invalid_column + assert_not @conn.valid_type?(:foobar) + end + def test_client_encoding assert_equal Encoding::UTF_8, @conn.client_encoding end @@ -86,14 +95,39 @@ module ActiveRecord assert_equal @conn.default_sequence_name('ex_with_custom_index_type_pk', 'id'), seq end + def test_tinyint_integer_typecasting + @conn.exec_query('drop table if exists ex_with_non_boolean_tinyint_column') + @conn.exec_query(<<-eosql) + CREATE TABLE `ex_with_non_boolean_tinyint_column` ( + `status` TINYINT(4)) + eosql + insert(@conn, { 'status' => 2 }, 'ex_with_non_boolean_tinyint_column') + + result = @conn.exec_query('SELECT status FROM ex_with_non_boolean_tinyint_column') + + assert_equal 2, result.column_types['status'].type_cast(result.last['status']) + end + + def test_supports_extensions + assert_not @conn.supports_extensions?, 'does not support extensions' + end + + def test_respond_to_enable_extension + assert @conn.respond_to?(:enable_extension) + end + + def test_respond_to_disable_extension + assert @conn.respond_to?(:disable_extension) + end + private - def insert(ctx, data) + def insert(ctx, data, table='ex') binds = data.map { |name, value| - [ctx.columns('ex').find { |x| x.name == name }, value] + [ctx.columns(table).find { |x| x.name == name }, value] } columns = binds.map(&:first).map(&:name) - sql = "INSERT INTO ex (#{columns.join(", ")}) + sql = "INSERT INTO #{table} (#{columns.join(", ")}) VALUES (#{(['?'] * columns.length).join(', ')})" ctx.exec_insert(sql, 'SQL', binds) diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 4cf4bc4c61..8eb9565963 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -2,7 +2,7 @@ require "cases/helper" class Group < ActiveRecord::Base Group.table_name = 'group' - belongs_to :select, :class_name => 'Select' + belongs_to :select has_one :values end diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index d94bb629a7..807a7a155e 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -35,6 +35,28 @@ module ActiveRecord def test_table_exists_wrong_schema assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") end + + def test_dump_indexes + index_a_name = 'index_key_tests_on_snack' + index_b_name = 'index_key_tests_on_pizza' + index_c_name = 'index_key_tests_on_awesome' + + table = 'key_tests' + + indexes = @connection.indexes(table).sort_by {|i| i.name} + assert_equal 3,indexes.size + + index_a = indexes.select{|i| i.name == index_a_name}[0] + index_b = indexes.select{|i| i.name == index_b_name}[0] + index_c = indexes.select{|i| i.name == index_c_name}[0] + assert_equal :btree, index_a.using + assert_nil index_a.type + assert_equal :btree, index_b.using + assert_nil index_b.type + + assert_nil index_c.using + assert_equal :fulltext, index_c.type + end end end end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index a83399d0cd..4ccf568406 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -2,40 +2,61 @@ require "cases/helper" class ActiveSchemaTest < ActiveRecord::TestCase def setup - ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do + @connection = ActiveRecord::Base.remove_connection + ActiveRecord::Base.establish_connection(@connection) + + ActiveRecord::Base.connection.singleton_class.class_eval do alias_method :execute_without_stub, :execute - remove_method :execute def execute(sql, name = nil) return sql end end end def teardown - ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do - remove_method :execute - alias_method :execute, :execute_without_stub - end + ActiveRecord::Base.remove_connection + ActiveRecord::Base.establish_connection(@connection) end def test_add_index # add_index calls index_name_exists? which can't work since execute is stubbed - ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:define_method, :index_name_exists?) do |*| - false - end - expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)" + def (ActiveRecord::Base.connection).index_name_exists?(*); false; end + + expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " assert_equal expected, add_index(:people, :last_name, :length => nil) - expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10))" + expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10)) " assert_equal expected, add_index(:people, :last_name, :length => 10) - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15))" + expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) " assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15) - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`)" + expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) " assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15}) - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10))" + expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) " assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10}) - ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:remove_method, :index_name_exists?) + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) " + assert_equal expected, add_index(:people, :last_name, :type => type) + end + + %w(btree hash).each do |using| + expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) " + assert_equal expected, add_index(:people, :last_name, :using => using) + end + + expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) " + assert_equal expected, add_index(:people, :last_name, :length => 10, :using => :btree) + + expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) ALGORITHM = COPY" + assert_equal expected, add_index(:people, :last_name, :length => 10, using: :btree, algorithm: :copy) + + assert_raise ArgumentError do + add_index(:people, :last_name, algorithm: :coyp) + end + + expected = "CREATE INDEX `index_people_on_last_name_and_first_name` USING btree ON `people` (`last_name`(15), `first_name`(15)) " + assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree) end def test_drop_table @@ -70,8 +91,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase def test_add_timestamps with_real_execute do begin - ActiveRecord::Base.connection.create_table :delete_me do |t| - end + ActiveRecord::Base.connection.create_table :delete_me ActiveRecord::Base.connection.add_timestamps :delete_me assert column_present?('delete_me', 'updated_at', 'datetime') assert column_present?('delete_me', 'created_at', 'datetime') @@ -98,22 +118,20 @@ class ActiveSchemaTest < ActiveRecord::TestCase private def with_real_execute - #we need to actually modify some data, so we make execute point to the original method - ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do + ActiveRecord::Base.connection.singleton_class.class_eval do alias_method :execute_with_stub, :execute remove_method :execute alias_method :execute, :execute_without_stub end + yield ensure - #before finishing, we restore the alias to the mock-up method - ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do + ActiveRecord::Base.connection.singleton_class.class_eval do remove_method :execute alias_method :execute, :execute_with_stub end end - def method_missing(method_symbol, *arguments) ActiveRecord::Base.connection.send(method_symbol, *arguments) end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index fedd9f603c..679c515e8c 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -3,14 +3,14 @@ require "cases/helper" class MysqlConnectionTest < ActiveRecord::TestCase def setup super + @subscriber = SQLSubscriber.new + ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) @connection = ActiveRecord::Base.connection - @connection.extend(LogIntercepter) - @connection.intercepted = true end def teardown - @connection.intercepted = false - @connection.logged = [] + ActiveSupport::Notifications.unsubscribe(@subscriber) + super end def test_no_automatic_reconnection_after_timeout @@ -72,14 +72,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase def test_logs_name_show_variable @connection.show_variable 'foo' - assert_equal "SCHEMA", @connection.logged[0][1] + assert_equal "SCHEMA", @subscriber.logged[0][1] end def test_logs_name_rename_column_sql @connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))" - @connection.logged = [] + @subscriber.logged.clear @connection.send(:rename_column_sql, 'bar_baz', 'foo', 'foo2') - assert_equal "SCHEMA", @connection.logged[0][1] + assert_equal "SCHEMA", @subscriber.logged[0][1] ensure @connection.execute "DROP TABLE `bar_baz`" end diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index f3a05e48ad..6dd9a5ec87 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -5,6 +5,6 @@ class Mysql2EnumTest < ActiveRecord::TestCase end def test_enum_limit - assert_equal 5, EnumTest.columns.first.limit + assert_equal 6, 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 98596a7f62..1a82308176 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -2,7 +2,7 @@ require "cases/helper" class Group < ActiveRecord::Base Group.table_name = 'group' - belongs_to :select, :class_name => 'Select' + belongs_to :select has_one :values end @@ -84,7 +84,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase assert_nothing_raised { x.save } assert_nothing_raised { Group.find_by_order('y') } assert_nothing_raised { Group.find(1) } - x = Group.find(1) end # has_one association with reserved-word table name diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb new file mode 100644 index 0000000000..9ecd901eac --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class Mysql2Adapter + class SchemaMigrationsTest < ActiveRecord::TestCase + def test_initializes_schema_migrations_for_encoding_utf8mb4 + conn = ActiveRecord::Base.connection + + smtn = ActiveRecord::Migrator.schema_migrations_table_name + conn.drop_table(smtn) if conn.table_exists?(smtn) + + config = conn.instance_variable_get(:@config) + original_encoding = config[:encoding] + + config[:encoding] = 'utf8mb4' + conn.initialize_schema_migrations_table + + assert conn.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4) + ensure + config[:encoding] = original_encoding + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 94429e772f..5db60ff8a0 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -44,6 +44,27 @@ module ActiveRecord assert_match(/database 'foo-bar'/, e.inspect) end + def test_dump_indexes + index_a_name = 'index_key_tests_on_snack' + index_b_name = 'index_key_tests_on_pizza' + index_c_name = 'index_key_tests_on_awesome' + + table = 'key_tests' + + indexes = @connection.indexes(table).sort_by {|i| i.name} + assert_equal 3,indexes.size + + index_a = indexes.select{|i| i.name == index_a_name}[0] + index_b = indexes.select{|i| i.name == index_b_name}[0] + index_c = indexes.select{|i| i.name == index_c_name}[0] + assert_equal :btree, index_a.using + assert_nil index_a.type + assert_equal :btree, index_b.using + assert_nil index_b.type + + assert_nil index_c.using + assert_equal :fulltext, index_c.type + 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 01c3e6b49b..22dd48e113 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -25,14 +25,30 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase 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 + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.stubs(:index_name_exists?).returns(false) - expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active') + 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?) + expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name")) + assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently) + + %w(gin gist hash btree).each do |type| + expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name")) + assert_equal expected, add_index(:people, :last_name, using: type) + + expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" USING #{type} ("last_name")) + assert_equal expected, add_index(:people, :last_name, using: type, algorithm: :concurrently) + end + + assert_raise ArgumentError do + add_index(:people, :last_name, algorithm: :copy) + end + expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name")) + assert_equal expected, add_index(:people, :last_name, :unique => true, :using => :gist) + + expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name") WHERE state = 'active') + assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'", :using => :gist) end private diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 8774bf626f..9536cceb1d 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -12,7 +12,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection @connection.transaction do @connection.create_table('pg_arrays') do |t| - t.string 'tags', :array => true + t.string 'tags', array: true + t.integer 'ratings', array: true end end @column = PgArray.columns.find { |c| c.name == 'tags' } @@ -27,6 +28,27 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert @column.array end + def test_change_column_with_array + @connection.add_column :pg_arrays, :snippets, :string, array: true, default: [] + @connection.change_column :pg_arrays, :snippets, :text, array: true, default: "{}" + + PgArray.reset_column_information + column = PgArray.columns.find { |c| c.name == 'snippets' } + + assert_equal :text, column.type + assert_equal [], column.default + assert column.array + end + + def test_change_column_cant_make_non_array_column_to_array + @connection.add_column :pg_arrays, :a_string, :string + assert_raises ActiveRecord::StatementInvalid do + @connection.transaction do + @connection.change_column :pg_arrays, :a_string, :string, array: true + end + end + end + def test_type_cast_array assert @column @@ -57,42 +79,57 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal(['1','2','3'], x.tags) end - def test_multi_dimensional - assert_cycle([['1','2'],['2','3']]) + def test_multi_dimensional_with_strings + assert_cycle(:tags, [[['1'], ['2']], [['2'], ['3']]]) + end + + def test_multi_dimensional_with_integers + assert_cycle(:ratings, [[[1], [7]], [[8], [10]]]) end def test_strings_with_quotes - assert_cycle(['this has','some "s that need to be escaped"']) + assert_cycle(:tags, ['this has','some "s that need to be escaped"']) end def test_strings_with_commas - assert_cycle(['this,has','many,values']) + assert_cycle(:tags, ['this,has','many,values']) end def test_strings_with_array_delimiters - assert_cycle(['{','}']) + assert_cycle(:tags, ['{','}']) end def test_strings_with_null_strings - assert_cycle(['NULL','NULL']) + assert_cycle(:tags, ['NULL','NULL']) end def test_contains_nils - assert_cycle(['1',nil,nil]) + assert_cycle(:tags, ['1',nil,nil]) + end + + def test_insert_fixture + tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] + @connection.insert_fixture({"tags" => tag_values}, "pg_arrays" ) + assert_equal(PgArray.last.tags, tag_values) + end + + def test_attribute_for_inspect_for_array_field + record = PgArray.new { |a| a.ratings = (1..11).to_a } + assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) end private - def assert_cycle array + def assert_cycle field, array # test creation - x = PgArray.create!(:tags => array) + x = PgArray.create!(field => array) x.reload - assert_equal(array, x.tags) + assert_equal(array, x.public_send(field)) # test updating - x = PgArray.create!(:tags => []) - x.tags = array + x = PgArray.create!(field => []) + x.public_send("#{field}=", array) x.save! x.reload - assert_equal(array, x.tags) + assert_equal(array, x.public_send(field)) end end diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb new file mode 100644 index 0000000000..b8dd35c4c5 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -0,0 +1,104 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlByteaTest < ActiveRecord::TestCase + class ByteaDataType < ActiveRecord::Base + self.table_name = 'bytea_data_type' + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.transaction do + @connection.create_table('bytea_data_type') do |t| + t.binary 'payload' + t.binary 'serialized' + end + end + end + @column = ByteaDataType.columns.find { |c| c.name == 'payload' } + assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn)) + end + + def teardown + @connection.execute 'drop table if exists bytea_data_type' + end + + def test_column + assert_equal :binary, @column.type + end + + def test_type_cast_binary_converts_the_encoding + assert @column + + data = "\u001F\x8B" + assert_equal('UTF-8', data.encoding.name) + assert_equal('ASCII-8BIT', @column.type_cast(data).encoding.name) + end + + def test_type_cast_binary_value + data = "\u001F\x8B".force_encoding("BINARY") + assert_equal(data, @column.type_cast(data)) + end + + def test_type_case_nil + assert_equal(nil, @column.type_cast(nil)) + end + + def test_read_value + data = "\u001F" + @connection.execute "insert into bytea_data_type (payload) VALUES ('#{data}')" + record = ByteaDataType.first + assert_equal(data, record.payload) + record.delete + end + + def test_read_nil_value + @connection.execute "insert into bytea_data_type (payload) VALUES (null)" + record = ByteaDataType.first + assert_equal(nil, record.payload) + record.delete + end + + def test_write_value + data = "\u001F" + record = ByteaDataType.create(payload: data) + assert_not record.new_record? + assert_equal(data, record.payload) + end + + def test_write_binary + data = File.read(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'example.log')) + assert(data.size > 1) + record = ByteaDataType.create(payload: data) + assert_not record.new_record? + assert_equal(data, record.payload) + assert_equal(data, ByteaDataType.where(id: record.id).first.payload) + end + + def test_write_nil + record = ByteaDataType.create(payload: nil) + assert_not record.new_record? + assert_equal(nil, record.payload) + assert_equal(nil, ByteaDataType.where(id: record.id).first.payload) + end + + class Serializer + def load(str); str; end + def dump(str); str; end + end + + def test_serialize + klass = Class.new(ByteaDataType) { + serialize :serialized, Serializer.new + } + obj = klass.new + obj.serialized = "hello world" + obj.save! + obj.reload + assert_equal "hello world", obj.serialized + end +end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index c03660957e..81aa977c59 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -7,14 +7,14 @@ module ActiveRecord def setup super + @subscriber = SQLSubscriber.new + ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) @connection = ActiveRecord::Base.connection - @connection.extend(LogIntercepter) - @connection.intercepted = true end def teardown - @connection.intercepted = false - @connection.logged = [] + ActiveSupport::Notifications.unsubscribe(@subscriber) + super end def test_encoding @@ -47,74 +47,48 @@ module ActiveRecord def test_tables_logs_name @connection.tables('hello') - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_indexes_logs_name @connection.indexes('items', 'hello') - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_table_exists_logs_name @connection.table_exists?('items') - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.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] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_current_database_logs_name @connection.current_database - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_encoding_logs_name @connection.encoding - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_schema_names_logs_name @connection.schema_names - assert_equal 'SCHEMA', @connection.logged[0][1] + assert_equal 'SCHEMA', @subscriber.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, __FILE__, __LINE__ + 1 - 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" + def test_statement_key_is_logged + bindval = 1 + @connection.exec_query('SELECT $1::integer', 'SQL', [[nil, bindval]]) + name = @subscriber.payloads.last[:statement_name] + assert name + res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(#{bindval})") + plan = res.column_types['QUERY PLAN'].type_cast res.rows.first.first + assert_operator plan.length, :>, 0 end # Must have with_manual_interventions set to true for this diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 1e6ae85a25..c5ff8cb609 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -246,7 +246,7 @@ _SQL assert_equal 2...10, @second_range.int4_range assert_equal 2...Float::INFINITY, @third_range.int4_range assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) - assert_equal nil, @empty_range.int4_range + assert_nil @empty_range.int4_range end def test_int8range_values @@ -255,7 +255,7 @@ _SQL assert_equal 11...100, @second_range.int8_range assert_equal 11...Float::INFINITY, @third_range.int8_range assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) - assert_equal nil, @empty_range.int8_range + assert_nil @empty_range.int8_range end def test_daterange_values @@ -264,7 +264,7 @@ _SQL assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) - assert_equal nil, @empty_range.date_range + assert_nil @empty_range.date_range end def test_numrange_values @@ -273,7 +273,7 @@ _SQL assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range - assert_equal nil, @empty_range.num_range + assert_nil @empty_range.num_range end def test_tsrange_values @@ -281,18 +281,16 @@ _SQL tz = ::ActiveRecord::Base.default_timezone assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Float::INFINITY, @third_range.ts_range assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) - assert_equal nil, @empty_range.ts_range + assert_nil @empty_range.ts_range end def test_tstzrange_values skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range - assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Float::INFINITY, @third_range.tstz_range assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) - assert_equal nil, @empty_range.tstz_range + assert_nil @empty_range.tstz_range end def test_money_values @@ -300,6 +298,14 @@ _SQL assert_equal(-567.89, @second_money.wealth) end + def test_money_type_cast + column = PostgresqlMoney.columns.find { |c| c.name == 'wealth' } + assert_equal(12345678.12, column.type_cast("$12,345,678.12")) + assert_equal(12345678.12, column.type_cast("$12.345.678,12")) + assert_equal(-1.15, column.type_cast("-$1.15")) + assert_equal(-2.25, column.type_cast("($2.25)")) + end + def test_create_tstzrange skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') @@ -313,14 +319,14 @@ _SQL def test_update_tstzrange skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? new_tstzrange = Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET') - assert @first_range.tstz_range = new_tstzrange + @first_range.tstz_range = new_tstzrange assert @first_range.save assert @first_range.reload - assert_equal @first_range.tstz_range, new_tstzrange - assert @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000') + assert_equal new_tstzrange, @first_range.tstz_range + @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000') assert @first_range.save assert @first_range.reload - assert_equal @first_range.tstz_range, nil + assert_nil @first_range.tstz_range end def test_create_tsrange @@ -337,14 +343,14 @@ _SQL skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? tz = ::ActiveRecord::Base.default_timezone new_tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0) - assert @first_range.ts_range = new_tsrange + @first_range.ts_range = new_tsrange assert @first_range.save assert @first_range.reload - assert_equal @first_range.ts_range, new_tsrange - assert @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0) + assert_equal new_tsrange, @first_range.ts_range + @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0) assert @first_range.save assert @first_range.reload - assert_equal @first_range.ts_range, nil + assert_nil @first_range.ts_range end def test_create_numrange @@ -359,14 +365,14 @@ _SQL def test_update_numrange skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? new_numrange = BigDecimal.new('0.5')...BigDecimal.new('1') - assert @first_range.num_range = new_numrange + @first_range.num_range = new_numrange assert @first_range.save assert @first_range.reload - assert_equal @first_range.num_range, new_numrange - assert @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5') + assert_equal new_numrange, @first_range.num_range + @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5') assert @first_range.save assert @first_range.reload - assert_equal @first_range.num_range, nil + assert_nil @first_range.num_range end def test_create_daterange @@ -381,14 +387,14 @@ _SQL def test_update_daterange skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? new_daterange = Date.new(2012, 2, 3)...Date.new(2012, 2, 10) - assert @first_range.date_range = new_daterange + @first_range.date_range = new_daterange assert @first_range.save assert @first_range.reload - assert_equal @first_range.date_range, new_daterange - assert @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3) + assert_equal new_daterange, @first_range.date_range + @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3) assert @first_range.save assert @first_range.reload - assert_equal @first_range.date_range, nil + assert_nil @first_range.date_range end def test_create_int4range @@ -403,14 +409,14 @@ _SQL def test_update_int4range skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? new_int4range = 6...10 - assert @first_range.int4_range = new_int4range + @first_range.int4_range = new_int4range assert @first_range.save assert @first_range.reload - assert_equal @first_range.int4_range, new_int4range - assert @first_range.int4_range = 3...3 + assert_equal new_int4range, @first_range.int4_range + @first_range.int4_range = 3...3 assert @first_range.save assert @first_range.reload - assert_equal @first_range.int4_range, nil + assert_nil @first_range.int4_range end def test_create_int8range @@ -425,25 +431,25 @@ _SQL def test_update_int8range skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? new_int8range = 60000...10000000 - assert @first_range.int8_range = new_int8range + @first_range.int8_range = new_int8range assert @first_range.save assert @first_range.reload - assert_equal @first_range.int8_range, new_int8range - assert @first_range.int8_range = 39999...39999 + assert_equal new_int8range, @first_range.int8_range + @first_range.int8_range = 39999...39999 assert @first_range.save assert @first_range.reload - assert_equal @first_range.int8_range, nil + assert_nil @first_range.int8_range end def test_update_tsvector new_text_vector = "'new' 'text' 'vector'" - assert @first_tsvector.text_vector = new_text_vector + @first_tsvector.text_vector = new_text_vector assert @first_tsvector.save assert @first_tsvector.reload - assert @first_tsvector.text_vector = new_text_vector + @first_tsvector.text_vector = new_text_vector assert @first_tsvector.save assert @first_tsvector.reload - assert_equal @first_tsvector.text_vector, new_text_vector + assert_equal new_text_vector, @first_tsvector.text_vector end def test_number_values @@ -481,31 +487,31 @@ _SQL def test_update_integer_array new_value = [32800,95000,29350,17000] - assert @first_array.commission_by_quarter = new_value + @first_array.commission_by_quarter = new_value assert @first_array.save assert @first_array.reload - assert_equal @first_array.commission_by_quarter, new_value - assert @first_array.commission_by_quarter = new_value + assert_equal new_value, @first_array.commission_by_quarter + @first_array.commission_by_quarter = new_value assert @first_array.save assert @first_array.reload - assert_equal @first_array.commission_by_quarter, new_value + assert_equal new_value, @first_array.commission_by_quarter end def test_update_text_array new_value = ['robby','robert','rob','robbie'] - assert @first_array.nicknames = new_value + @first_array.nicknames = new_value assert @first_array.save assert @first_array.reload - assert_equal @first_array.nicknames, new_value - assert @first_array.nicknames = new_value + assert_equal new_value, @first_array.nicknames + @first_array.nicknames = new_value assert @first_array.save assert @first_array.reload - assert_equal @first_array.nicknames, new_value + assert_equal new_value, @first_array.nicknames end def test_update_money new_value = BigDecimal.new('123.45') - assert @first_money.wealth = new_value + @first_money.wealth = new_value assert @first_money.save assert @first_money.reload assert_equal new_value, @first_money.wealth @@ -514,28 +520,28 @@ _SQL def test_update_number new_single = 789.012 new_double = 789012.345 - assert @first_number.single = new_single - assert @first_number.double = new_double + @first_number.single = new_single + @first_number.double = new_double assert @first_number.save assert @first_number.reload - assert_equal @first_number.single, new_single - assert_equal @first_number.double, new_double + assert_equal new_single, @first_number.single + assert_equal new_double, @first_number.double end def test_update_time - assert @first_time.time_interval = '2 years 3 minutes' + @first_time.time_interval = '2 years 3 minutes' assert @first_time.save assert @first_time.reload - assert_equal @first_time.time_interval, '2 years 00:03:00' + assert_equal '2 years 00:03:00', @first_time.time_interval end def test_update_network_address 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 - assert @first_network_address.mac_address = new_mac_address + @first_network_address.cidr_address = new_cidr_address + @first_network_address.inet_address = new_inet_address + @first_network_address.mac_address = new_mac_address assert @first_network_address.save assert @first_network_address.reload assert_equal @first_network_address.cidr_address, new_cidr_address @@ -545,56 +551,66 @@ _SQL def test_update_bit_string new_bit_string = '11111111' - new_bit_string_varying = '11111110' - assert @first_bit_string.bit_string = new_bit_string - assert @first_bit_string.bit_string_varying = new_bit_string_varying + new_bit_string_varying = '0xFF' + @first_bit_string.bit_string = new_bit_string + @first_bit_string.bit_string_varying = new_bit_string_varying assert @first_bit_string.save assert @first_bit_string.reload assert_equal new_bit_string, @first_bit_string.bit_string - assert_equal new_bit_string_varying, @first_bit_string.bit_string_varying + assert_equal @first_bit_string.bit_string, @first_bit_string.bit_string_varying + end + + def test_invalid_hex_string + new_bit_string = 'FF' + @first_bit_string.bit_string = new_bit_string + assert_raise(ActiveRecord::StatementInvalid) { assert @first_bit_string.save } + end + + def test_invalid_network_address + @first_network_address.cidr_address = 'invalid addr' + assert_nil @first_network_address.cidr_address + assert_equal 'invalid addr', @first_network_address.cidr_address_before_type_cast + assert @first_network_address.save + + @first_network_address.reload + + @first_network_address.inet_address = 'invalid addr' + assert_nil @first_network_address.inet_address + assert_equal 'invalid addr', @first_network_address.inet_address_before_type_cast + assert @first_network_address.save end def test_update_oid new_value = 567890 - assert @first_oid.obj_id = new_value + @first_oid.obj_id = new_value assert @first_oid.save assert @first_oid.reload - assert_equal @first_oid.obj_id, new_value + assert_equal new_value, @first_oid.obj_id end def test_timestamp_with_zone_values_with_rails_time_zone_support - old_tz = ActiveRecord::Base.time_zone_aware_attributes - old_default_tz = ActiveRecord::Base.default_timezone + with_timezone_config default: :utc, aware_attributes: true do + @connection.reconnect! - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - - @connection.reconnect! - - @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time - assert_instance_of Time, @first_timestamp_with_zone.time + @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time + assert_instance_of Time, @first_timestamp_with_zone.time + end ensure - ActiveRecord::Base.default_timezone = old_default_tz - ActiveRecord::Base.time_zone_aware_attributes = old_tz @connection.reconnect! end def test_timestamp_with_zone_values_without_rails_time_zone_support - old_tz = ActiveRecord::Base.time_zone_aware_attributes - old_default_tz = ActiveRecord::Base.default_timezone - - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - - @connection.reconnect! - - @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time - assert_instance_of Time, @first_timestamp_with_zone.time + with_timezone_config default: :local, aware_attributes: false do + @connection.reconnect! + # make sure to use a non-UTC time zone + @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA') + + @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1) + assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time + assert_instance_of Time, @first_timestamp_with_zone.time + end ensure - ActiveRecord::Base.default_timezone = old_default_tz - ActiveRecord::Base.time_zone_aware_attributes = old_tz @connection.reconnect! end end diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 619d581d5f..0b61f61572 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -22,13 +22,6 @@ 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 ad98d7c8ce..de724486c2 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -7,6 +7,8 @@ require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlHstoreTest < ActiveRecord::TestCase class Hstore < ActiveRecord::Base self.table_name = 'hstores' + + store_accessor :settings, :language, :timezone end def setup @@ -26,6 +28,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase @connection.transaction do @connection.create_table('hstores') do |t| t.hstore 'tags', :default => '' + t.hstore 'settings' end end @column = Hstore.columns.find { |c| c.name == 'tags' } @@ -40,25 +43,15 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert @connection.extensions.include?('hstore'), "extension list should include hstore" end - def test_hstore_enabled + def test_disable_enable_hstore assert @connection.extension_enabled?('hstore') - end - - def test_disable_hstore - if @connection.extension_enabled?('hstore') - @connection.disable_extension 'hstore' - assert_not @connection.extension_enabled?('hstore') - end - end - - def test_enable_hstore - if @connection.extension_enabled?('hstore') - @connection.disable_extension 'hstore' - end - + @connection.disable_extension 'hstore' assert_not @connection.extension_enabled?('hstore') @connection.enable_extension 'hstore' assert @connection.extension_enabled?('hstore') + ensure + # Restore column(s) dropped by `drop extension hstore cascade;` + load_schema end def test_column @@ -80,6 +73,13 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase Hstore.reset_column_information end + def test_cast_value_on_write + x = Hstore.new tags: {"bool" => true, "number" => 5} + assert_equal({"bool" => "true", "number" => "5"}, x.tags) + x.save + assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags) + end + def test_type_cast_hstore assert @column @@ -93,6 +93,24 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b"))) end + def test_with_store_accessors + x = Hstore.new(language: "fr", timezone: "GMT") + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + x.save! + x = Hstore.first + assert_equal "fr", x.language + assert_equal "GMT", x.timezone + + x.language = "de" + x.save! + + x = Hstore.first + assert_equal "de", x.language + assert_equal "GMT", x.timezone + end + def test_gen1 assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) end @@ -189,6 +207,10 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_cycle('ca' => 'cà', 'ac' => 'àc') end + def test_multiline + assert_cycle("a\nb" => "c\nd") + end + private def assert_cycle hash # test creation diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 6fc08ae4f0..c33c7ef968 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -7,6 +7,8 @@ require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlJSONTest < ActiveRecord::TestCase class JsonDataType < ActiveRecord::Base self.table_name = 'json_data_type' + + store_accessor :settings, :resolution end def setup @@ -15,6 +17,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase @connection.transaction do @connection.create_table('json_data_type') do |t| t.json 'payload', :default => {} + t.json 'settings' end end rescue ActiveRecord::StatementInvalid @@ -46,6 +49,13 @@ class PostgresqlJSONTest < ActiveRecord::TestCase JsonDataType.reset_column_information end + def test_cast_value_on_write + x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} + assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) + x.save + assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) + end + def test_type_cast_json assert @column @@ -83,4 +93,32 @@ class PostgresqlJSONTest < ActiveRecord::TestCase x = JsonDataType.first assert_equal(nil, x.payload) end + + def test_select_array_json_value + @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| + x = JsonDataType.first + assert_equal(['v0', {'k1' => 'v1'}], x.payload) + end + + def test_rewrite_array_json_value + @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| + x = JsonDataType.first + x.payload = ['v1', {'k2' => 'v2'}, 'v3'] + assert x.save! + end + + def test_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + x.save! + x = JsonDataType.first + assert_equal "320×480", x.resolution + + x.resolution = "640×1136" + x.save! + + x = JsonDataType.first + assert_equal "640×1136", x.resolution + 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 05e0f0e192..8b017760b1 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -10,6 +10,15 @@ module ActiveRecord @connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))') end + def test_valid_column + column = @connection.columns('ex').find { |col| col.name == 'id' } + assert @connection.valid_type?(column.type) + end + + def test_invalid_column + assert_not @connection.valid_type?(:foobar) + end + def test_primary_key assert_equal 'id', @connection.primary_key('ex') end @@ -216,38 +225,38 @@ module ActiveRecord assert_equal "(number > 100)", index.where end - def test_distinct_zero_orders - assert_equal "DISTINCT posts.id", - @connection.distinct("posts.id", []) + def test_columns_for_distinct_zero_orders + assert_equal "posts.id", + @connection.columns_for_distinct("posts.id", []) end - def test_distinct_one_order - assert_equal "DISTINCT posts.id, posts.created_at AS alias_0", - @connection.distinct("posts.id", ["posts.created_at desc"]) + def test_columns_for_distinct_one_order + assert_equal "posts.id, posts.created_at AS alias_0", + @connection.columns_for_distinct("posts.id", ["posts.created_at desc"]) end - def test_distinct_few_orders - assert_equal "DISTINCT posts.id, posts.created_at AS alias_0, posts.position AS alias_1", - @connection.distinct("posts.id", ["posts.created_at desc", "posts.position asc"]) + def test_columns_for_distinct_few_orders + assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1", + @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"]) end - def test_distinct_blank_not_nil_orders - assert_equal "DISTINCT posts.id, posts.created_at AS alias_0", - @connection.distinct("posts.id", ["posts.created_at desc", "", " "]) + def test_columns_for_distinct_blank_not_nil_orders + assert_equal "posts.id, posts.created_at AS alias_0", + @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "]) end - def test_distinct_with_arel_order + def test_columns_for_distinct_with_arel_order order = Object.new def order.to_sql "posts.created_at desc" end - assert_equal "DISTINCT posts.id, posts.created_at AS alias_0", - @connection.distinct("posts.id", [order]) + assert_equal "posts.id, posts.created_at AS alias_0", + @connection.columns_for_distinct("posts.id", [order]) 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"]) + def test_columns_for_distinct_with_nulls + assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"]) + assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"]) end def test_raise_error_when_cannot_translate_exception diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 685f0ea74f..1122f8b9a1 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -44,6 +44,19 @@ module ActiveRecord c = Column.new(nil, 1, 'float') assert_equal "'Infinity'", @conn.quote(infinity, c) end + + def test_quote_cast_numeric + fixnum = 666 + c = Column.new(nil, nil, 'varchar') + assert_equal "'666'", @conn.quote(fixnum, c) + c = Column.new(nil, nil, 'text') + assert_equal "'666'", @conn.quote(fixnum, c) + end + + def test_quote_time_usec + assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0)) + assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime) + 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 cd31900d4e..e8dd188ec8 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -11,16 +11,19 @@ class SchemaTest < ActiveRecord::TestCase INDEX_B_NAME = 'b_index_things_on_different_columns_in_each_schema' INDEX_C_NAME = 'c_index_full_text_search' INDEX_D_NAME = 'd_index_things_on_description_desc' + INDEX_E_NAME = 'e_index_things_on_name_vector' INDEX_A_COLUMN = 'name' INDEX_B_COLUMN_S1 = 'email' INDEX_B_COLUMN_S2 = 'moment' INDEX_C_COLUMN = %q{(to_tsvector('english', coalesce(things.name, '')))} INDEX_D_COLUMN = 'description' + INDEX_E_COLUMN = 'name_vector' COLUMNS = [ 'id integer', 'name character varying(50)', 'email character varying(50)', 'description character varying(100)', + 'name_vector tsvector', 'moment timestamp without time zone default now()' ] PK_TABLE_NAME = 'table_with_pk' @@ -61,6 +64,8 @@ class SchemaTest < ActiveRecord::TestCase @connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});" @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 INDEX #{INDEX_E_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});" + @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});" @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))" @@ -236,15 +241,15 @@ class SchemaTest < ActiveRecord::TestCase end def test_dump_indexes_for_schema_one - do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN) + do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN) end def test_dump_indexes_for_schema_two - do_dump_index_tests_for_schema(SCHEMA2_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S2, INDEX_D_COLUMN) + do_dump_index_tests_for_schema(SCHEMA2_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S2, INDEX_D_COLUMN, INDEX_E_COLUMN) end def test_dump_indexes_for_schema_multiple_schemas_in_search_path - do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN) + do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN) end def test_with_uppercase_index_name @@ -344,15 +349,20 @@ class SchemaTest < ActiveRecord::TestCase @connection.schema_search_path = "'$user', public" end - def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name) + def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name) with_schema_search_path(this_schema_name) do indexes = @connection.indexes(TABLE_NAME).sort_by {|i| i.name} - assert_equal 3,indexes.size + assert_equal 4,indexes.size do_dump_index_assertions_for_one_index(indexes[0], INDEX_A_NAME, first_index_column_name) do_dump_index_assertions_for_one_index(indexes[1], INDEX_B_NAME, second_index_column_name) do_dump_index_assertions_for_one_index(indexes[2], INDEX_D_NAME, third_index_column_name) + do_dump_index_assertions_for_one_index(indexes[3], INDEX_E_NAME, fourth_index_column_name) + indexes.select{|i| i.name != INDEX_E_NAME}.each do |index| + assert_equal :btree, index.using + end + assert_equal :gin, indexes.select{|i| i.name == INDEX_E_NAME}[0].using assert_equal :desc, indexes.select{|i| i.name == INDEX_D_NAME}[0].orders[INDEX_D_COLUMN] end end diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb index f1c4b85126..c5fd40accc 100644 --- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -1,38 +1,40 @@ require 'cases/helper' -module ActiveRecord::ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter - class InactivePGconn - def query(*args) - raise PGError - end +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + class InactivePGconn + def query(*args) + raise PGError + end - def status - PGconn::CONNECTION_BAD + def status + PGconn::CONNECTION_BAD + end end - end - class StatementPoolTest < ActiveRecord::TestCase - def test_cache_is_per_pid - return skip('must support fork') unless Process.respond_to?(:fork) + class StatementPoolTest < ActiveRecord::TestCase + def test_cache_is_per_pid + return skip('must support fork') unless Process.respond_to?(:fork) - cache = StatementPool.new nil, 10 - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } - Process.waitpid pid - assert $?.success?, 'process should exit successfully' - end + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end - def test_dealloc_does_not_raise_on_inactive_connection - cache = StatementPool.new InactivePGconn.new, 10 - cache['foo'] = 'bar' - assert_nothing_raised { cache.clear } + def test_dealloc_does_not_raise_on_inactive_connection + cache = StatementPool.new InactivePGconn.new, 10 + cache['foo'] = 'bar' + assert_nothing_raised { cache.clear } + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb new file mode 100644 index 0000000000..a753a23c09 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -0,0 +1,136 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlUUIDTest < ActiveRecord::TestCase + class UUID < ActiveRecord::Base + self.table_name = 'pg_uuids' + end + + def setup + @connection = ActiveRecord::Base.connection + + unless @connection.supports_extensions? + return skip "do not test on PG without uuid-ossp" + end + + unless @connection.extension_enabled?('uuid-ossp') + @connection.enable_extension 'uuid-ossp' + @connection.commit_db_transaction + end + + @connection.reconnect! + + @connection.transaction do + @connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t| + t.string 'name' + t.uuid 'other_uuid', default: 'uuid_generate_v4()' + end + end + end + + def teardown + @connection.execute 'drop table if exists pg_uuids' + end + + def test_id_is_uuid + assert_equal :uuid, UUID.columns_hash['id'].type + assert UUID.primary_key + end + + def test_id_has_a_default + u = UUID.create + assert_not_nil u.id + end + + def test_auto_create_uuid + u = UUID.create + u.reload + assert_not_nil u.other_uuid + end + + def test_pk_and_sequence_for_uuid_primary_key + pk, seq = @connection.pk_and_sequence_for('pg_uuids') + assert_equal 'id', pk + assert_equal nil, seq + end + + def test_schema_dumper_for_uuid_primary_key + schema = StringIO.new + ActiveRecord::SchemaDumper.dump(@connection, schema) + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string) + assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string) + end +end + +class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase + class UUID < ActiveRecord::Base + self.table_name = 'pg_uuids' + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.reconnect! + + @connection.transaction do + @connection.create_table('pg_uuids', id: false) do |t| + t.primary_key :id, :uuid, default: nil + t.string 'name' + end + end + end + + def teardown + @connection.execute 'drop table if exists pg_uuids' + end + + def test_id_allows_default_override_via_nil + col_desc = @connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default + FROM pg_attribute a + LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum + WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first + assert_nil col_desc["default"] + end +end + +class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase + class UuidPost < ActiveRecord::Base + self.table_name = 'pg_uuid_posts' + has_many :uuid_comments, inverse_of: :uuid_post + end + + class UuidComment < ActiveRecord::Base + self.table_name = 'pg_uuid_comments' + belongs_to :uuid_post + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.reconnect! + + @connection.transaction do + @connection.create_table('pg_uuid_posts', id: :uuid) do |t| + t.string 'title' + end + @connection.create_table('pg_uuid_comments', id: :uuid) do |t| + t.uuid :uuid_post_id, default: 'uuid_generate_v4()' + t.string 'content' + end + end + end + + def teardown + @connection.transaction do + @connection.execute 'drop table if exists pg_uuid_comments' + @connection.execute 'drop table if exists pg_uuid_posts' + end + end + + def test_collection_association_with_uuid + post = UuidPost.create! + comment = post.uuid_comments.create! + assert post.uuid_comments.find(comment.id) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb new file mode 100644 index 0000000000..bf14b378d8 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +require 'cases/helper' +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlXMLTest < ActiveRecord::TestCase + class XmlDataType < ActiveRecord::Base + self.table_name = 'xml_data_type' + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.transaction do + @connection.create_table('xml_data_type') do |t| + t.xml 'payload', default: {} + end + end + rescue ActiveRecord::StatementInvalid + return skip "do not test on PG without xml" + end + @column = XmlDataType.columns.find { |c| c.name == 'payload' } + end + + def teardown + @connection.execute 'drop table if exists xml_data_type' + end + + def test_column + assert_equal :xml, @column.type + end + + def test_null_xml + @connection.execute %q|insert into xml_data_type (payload) VALUES(null)| + assert_nil XmlDataType.first.payload + 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 d03d1dd94c..e78cb88562 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class CopyTableTest < ActiveRecord::TestCase - fixtures :customers, :companies, :comments + fixtures :customers, :companies, :comments, :binaries def setup @connection = ActiveRecord::Base.connection @@ -54,7 +54,7 @@ class CopyTableTest < ActiveRecord::TestCase end def test_copy_table_with_id_col_that_is_not_primary_key - test_copy_table('goofy_string_id', 'goofy_string_id2') do |from, to, options| + test_copy_table('goofy_string_id', 'goofy_string_id2') do original_id = @connection.columns('goofy_string_id').detect{|col| col.name == 'id' } copied_id = @connection.columns('goofy_string_id2').detect{|col| col.name == 'id' } assert_equal original_id.type, copied_id.type @@ -65,13 +65,17 @@ class CopyTableTest < ActiveRecord::TestCase end def test_copy_table_with_unconventional_primary_key - test_copy_table('owners', 'owners_unconventional') do |from, to, options| + test_copy_table('owners', 'owners_unconventional') do original_pk = @connection.primary_key('owners') copied_pk = @connection.primary_key('owners_unconventional') assert_equal original_pk, copied_pk end end + def test_copy_table_with_binary_column + test_copy_table 'binaries', 'binaries2' + end + protected def copy_table(from, to, options = {}) @connection.copy_table(from, to, {:temporary => true}.merge(options)) @@ -86,7 +90,7 @@ protected end def table_indexes_without_name(table) - @connection.indexes('comments_with_index').delete(:name) + @connection.indexes(table).delete(:name) end def row_count(table) diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 003052bac4..ce7c869eec 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -21,13 +21,26 @@ module ActiveRecord ) eosql - @conn.extend(LogIntercepter) - @conn.intercepted = true + @subscriber = SQLSubscriber.new + ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) + end + + def test_valid_column + column = @conn.columns('items').find { |col| col.name == 'id' } + assert @conn.valid_type?(column.type) + end + + # sqlite databases should be able to support any type and not + # just the ones mentioned in the native_database_types. + # Therefore test_invalid column should always return true + # even if the type is not valid. + def test_invalid_column + assert @conn.valid_type?(:foobar) end def teardown - @conn.intercepted = false - @conn.logged = [] + ActiveSupport::Notifications.unsubscribe(@subscriber) + super end def test_column_types @@ -243,7 +256,7 @@ module ActiveRecord def test_tables_logs_name assert_logged [['SCHEMA', []]] do @conn.tables('hello') - assert_not_nil @conn.logged.first.shift + assert_not_nil @subscriber.logged.first.shift end end @@ -255,7 +268,7 @@ module ActiveRecord def test_table_exists_logs_name assert @conn.table_exists?('items') - assert_equal 'SCHEMA', @conn.logged[0][1] + assert_equal 'SCHEMA', @subscriber.logged[0][1] end def test_columns @@ -293,10 +306,10 @@ module ActiveRecord end def test_indexes_logs - assert_difference('@conn.logged.length') do + assert_difference('@subscriber.logged.length') do @conn.indexes('items') end - assert_match(/items/, @conn.logged.last.first) + assert_match(/items/, @subscriber.logged.last.first) end def test_no_indexes @@ -341,11 +354,23 @@ module ActiveRecord assert_nil @conn.primary_key('failboat') end + def test_supports_extensions + assert_not @conn.supports_extensions?, 'does not support extensions' + end + + def test_respond_to_enable_extension + assert @conn.respond_to?(:enable_extension) + end + + def test_respond_to_disable_extension + assert @conn.respond_to?(:disable_extension) + end + private def assert_logged logs yield - assert_equal logs, @conn.logged + assert_equal logs, @subscriber.logged end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb new file mode 100644 index 0000000000..5a4fe63580 --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb @@ -0,0 +1,21 @@ +# encoding: utf-8 +require "cases/helper" +require 'models/owner' + +module ActiveRecord + module ConnectionAdapters + class SQLite3CreateFolder < ActiveRecord::TestCase + def test_sqlite_creates_directory + Dir.mktmpdir do |dir| + dir = Pathname.new(dir) + @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"), + :adapter => 'sqlite3', + :timeout => 100 + + assert Dir.exists? dir.join('db') + assert File.exist? dir.join('db/foo.sqlite3') + end + end + end + end +end diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index 10195e3ae4..5536702f58 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -141,7 +141,6 @@ class AggregationsTest < ActiveRecord::TestCase end class OverridingAggregationsTest < ActiveRecord::TestCase - class Name; end class DifferentName; end class Person < ActiveRecord::Base diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 244e0b7179..500df52cd8 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -12,6 +12,8 @@ if ActiveRecord::Base.connection.supports_migrations? def teardown @connection.drop_table :fruits rescue nil + @connection.drop_table :nep_fruits rescue nil + @connection.drop_table :nep_schema_migrations rescue nil ActiveRecord::SchemaMigration.delete_all rescue nil end @@ -30,6 +32,24 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal 7, ActiveRecord::Migrator::current_version end + def test_schema_define_w_table_name_prefix + table_name = ActiveRecord::SchemaMigration.table_name + ActiveRecord::Base.table_name_prefix = "nep_" + ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" + ActiveRecord::Schema.define(:version => 7) do + create_table :fruits do |t| + t.column :color, :string + t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle + t.column :texture, :string + t.column :flavor, :string + end + end + assert_equal 7, ActiveRecord::Migrator::current_version + ensure + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::SchemaMigration.table_name = table_name + end + def test_schema_raises_an_error_for_invalid_column_type assert_raise NoMethodError do ActiveRecord::Schema.define(:version => 8) do diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index f5316952b8..a79f145e31 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -1,4 +1,4 @@ -require "cases/helper" +require 'cases/helper' require 'models/developer' require 'models/project' require 'models/company' @@ -14,6 +14,8 @@ require 'models/sponsor' require 'models/member' require 'models/essay' require 'models/toy' +require 'models/invoice' +require 'models/line_item' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, @@ -324,6 +326,45 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Topic.find(topic.id)[:replies_count] end + def test_belongs_to_with_touch_option_on_touch + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + assert_queries(1) { line_item.touch } + end + + def test_belongs_to_with_touch_option_on_touch_and_removed_parent + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + line_item.invoice = nil + + assert_queries(2) { line_item.touch } + end + + def test_belongs_to_with_touch_option_on_update + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + assert_queries(2) { line_item.update amount: 10 } + end + + def test_belongs_to_with_touch_option_on_destroy + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + assert_queries(2) { line_item.destroy } + end + + def test_belongs_to_with_touch_option_on_touch_and_reassigned_parent + line_item = LineItem.create! + Invoice.create!(line_items: [line_item]) + + line_item.invoice = Invoice.create! + + assert_queries(3) { line_item.touch } + end + def test_belongs_to_counter_after_update topic = Topic.create!(title: "37s") topic.replies.create!(title: "re: 37s", content: "rails") @@ -338,7 +379,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase topic.replies.create!(:title => "re: 37s", :content => "rails") assert_equal 1, Topic.find(topic.id)[:replies_count] - topic.update_columns(content: "rails is wonderfull") + topic.update_columns(content: "rails is wonderful") assert_equal 1, Topic.find(topic.id)[:replies_count] end @@ -391,8 +432,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_dont_find_target_when_foreign_key_is_null tagging = taggings(:thinking_general) - queries = assert_sql { tagging.super_tag } - assert_equal 0, queries.length + assert_queries(0) { tagging.super_tag } end def test_field_name_same_as_foreign_key @@ -414,6 +454,26 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 15, topic.replies.size end + def test_counter_cache_double_destroy + topic = Topic.create :title => "Zoom-zoom-zoom" + + 5.times do + topic.replies.create(:title => "re: zoom", :content => "speedy quick!") + end + + assert_equal 5, topic.reload[:replies_count] + assert_equal 5, topic.replies.size + + reply = topic.replies.first + + reply.destroy + assert_equal 4, topic.reload[:replies_count] + + reply.destroy + assert_equal 4, topic.reload[:replies_count] + assert_equal 4, topic.replies.size + end + def test_custom_counter_cache reply = Reply.create(:title => "re: zoom", :content => "speedy quick!") assert_equal 0, reply[:replies_count] @@ -545,6 +605,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_dependent_delete_and_destroy_with_belongs_to + AuthorAddress.destroyed_author_address_ids.clear + author_address = author_addresses(:david_address) author_address_extra = author_addresses(:david_address_extra) assert_equal [], AuthorAddress.destroyed_author_address_ids diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 80bca7f63e..811d91f849 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -52,12 +52,10 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_cascaded_eager_association_loading_with_join_for_count categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors]) - assert_nothing_raised do - assert_equal 4, categories.count - assert_equal 4, categories.to_a.count - assert_equal 3, categories.count(:distinct => true) - assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes - end + assert_equal 4, categories.count + assert_equal 4, categories.to_a.count + assert_equal 3, categories.distinct.count + assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes end def test_cascaded_eager_association_loading_with_duplicated_includes diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 1de7ee0846..498a4e8144 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -4,6 +4,7 @@ require 'models/tagging' require 'models/tag' require 'models/comment' require 'models/author' +require 'models/essay' require 'models/category' require 'models/company' require 'models/person' @@ -24,7 +25,7 @@ require 'models/categorization' require 'models/sponsor' class EagerAssociationTest < ActiveRecord::TestCase - fixtures :posts, :comments, :authors, :author_addresses, :categories, :categories_posts, + fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts, :companies, :accounts, :tags, :taggings, :people, :readers, :categorizations, :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books, :developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors @@ -193,7 +194,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end end - def test_finding_with_includes_on_has_one_assocation_with_same_include_includes_only_once + def test_finding_with_includes_on_has_one_association_with_same_include_includes_only_once author = authors(:david) post = author.post_about_thinking_with_last_comment last_comment = post.last_comment @@ -250,7 +251,8 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_nil Post.all.merge!(:includes => :author).find(posts(:authorless).id).author end - def test_nested_loading_with_no_associations + # Regression test for 21c75e5 + def test_nested_loading_does_not_raise_exception_when_association_does_not_exist assert_nothing_raised do Post.all.merge!(:includes => {:author => :author_addresss}).find(posts(:authorless).id) end @@ -302,7 +304,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_foreign_keys pets = Pet.all.merge!(:includes => :owner).to_a - assert_equal 3, pets.length + assert_equal 4, pets.length end def test_eager_association_loading_with_belongs_to @@ -345,9 +347,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.all.merge!(:includes => :post, :where => ['posts.id = ?',4]).to_a - end + Comment.includes(:post).references(:posts).where('posts.id = ?', 4) end end @@ -366,9 +366,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_string_with_quoted_table_name quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id') assert_nothing_raised do - ActiveSupport::Deprecation.silence do - Comment.all.merge!(:includes => :post, :where => ["#{quoted_posts_id} = ?",4]).to_a - end + Comment.includes(:post).references(:posts).where("#{quoted_posts_id} = ?", 4) end end @@ -381,9 +379,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id') assert_nothing_raised do - ActiveSupport::Deprecation.silence do - Comment.all.merge!(:includes => :post, :order => quoted_posts_id).to_a - end + Comment.includes(:post).references(:posts).order(quoted_posts_id) end end @@ -467,7 +463,7 @@ class EagerAssociationTest < ActiveRecord::TestCase 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 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 } end @@ -523,7 +519,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit 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 } + assert_equal 3, posts.inject(0) { |sum, post| sum + post.comments.size } end def test_eager_with_has_many_and_limit_and_conditions @@ -547,15 +543,11 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers - posts = ActiveSupport::Deprecation.silence do - Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "authors.name = ?", 'David' ]).to_a - end + posts = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", 'David') assert_equal 2, posts.size - count = ActiveSupport::Deprecation.silence do - Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) - end - assert_equal count, posts.size + count = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", 'David').count + assert_equal posts.size, count end def test_eager_with_has_many_and_limit_and_high_offset @@ -756,6 +748,8 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_default_scope_as_block + # warm up the habtm cache + EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first.projects developer = EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a assert_no_queries do @@ -1145,6 +1139,10 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_deep_including_through_habtm + # warm up habtm cache + posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a + posts[0].categories[0].categorizations.length + 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 } @@ -1174,8 +1172,26 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries { assert_equal 5, author.posts.size, "should not cache a subset of the association" } end - test "works in combination with order(:symbol)" do - author = Author.includes(:posts).references(:posts).order(:name).where('posts.title IS NOT NULL').first + test "preloading a through association twice does not reset it" do + members = Member.includes(current_membership: :club).includes(:club).to_a + assert_no_queries { + assert_equal 3, members.map(&:current_membership).map(&:club).size + } + end + + test "works in combination with order(:symbol) and reorder(:symbol)" do + author = Author.includes(:posts).references(:posts).order(:name).find_by('posts.title IS NOT NULL') assert_equal authors(:bob), author + + author = Author.includes(:posts).references(:posts).reorder(:name).find_by('posts.title IS NOT NULL') + assert_equal authors(:bob), author + end + + test "preloading with a polymorphic association and using the existential predicate" do + assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer + + assert_nothing_raised do + authors(:david).essays.includes(:writer).any? + end end end diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index da767a2a7e..f8f2832ab1 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -59,9 +59,11 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase end def test_extension_name - assert_equal 'DeveloperAssociationNameAssociationExtension', extension_name(Developer) - assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer) - assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer) + extend!(Developer) + extend!(MyApplication::Business::Developer) + + assert Object.const_get 'DeveloperAssociationNameAssociationExtension' + assert MyApplication::Business.const_get 'DeveloperAssociationNameAssociationExtension' end def test_proxy_association_after_scoped @@ -72,9 +74,7 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase private - def extension_name(model) - builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { } - builder.send(:wrap_block_extension) - builder.extension_module.name + def extend!(model) + ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_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 1b1b479f1a..be928ec8ee 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 @@ -65,19 +65,6 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base :foreign_key => "developer_id" end -class DeveloperWithCounterSQL < ActiveRecord::Base - self.table_name = 'developers' - - 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 fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings @@ -316,7 +303,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase dev.projects << projects(:active_record) assert_equal 3, dev.projects.size - assert_equal 1, dev.projects.uniq.size + assert_equal 1, dev.projects.distinct.size end def test_uniq_before_the_fact @@ -364,31 +351,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, david.projects(true).size end - def test_deleting_with_sql - david = Developer.find(1) - active_record = Project.find(1) - active_record.developers.reload - assert_equal 3, active_record.developers_by_sql.size - - active_record.developers_by_sql.delete(david) - assert_equal 2, active_record.developers_by_sql(true).size - end - - def test_deleting_array_with_sql - active_record = Project.find(1) - active_record.developers.reload - assert_equal 3, active_record.developers_by_sql.size - - 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 @@ -475,13 +437,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert george.treasures(true).empty? end - def test_deprecated_push_with_attributes_was_removed - jamis = developers(:jamis) - assert_raise(NoMethodError) do - jamis.projects.push_with_attributes(projects(:action_controller), :joined_on => Date.today) - end - end - def test_associations_with_conditions assert_equal 3, projects(:active_record).developers.size assert_equal 1, projects(:active_record).developers_named_david.size @@ -537,25 +492,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert ! project.developers.include?(developer) end - def test_find_in_association_with_custom_finder_sql - assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id), "SQL find" - - active_record = projects(:active_record) - active_record.developers_with_finder_sql.reload - assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find" - end - - def test_find_in_association_with_custom_finder_sql_and_multiple_interpolations - # interpolate once: - assert_equal [developers(:david), developers(:jamis), developers(:poor_jamis)], projects(:active_record).developers_with_finder_sql, "first interpolation" - # interpolate again, for a different project id - assert_equal [developers(:david)], projects(:action_controller).developers_with_finder_sql, "second interpolation" - end - - def test_find_in_association_with_custom_finder_sql_and_string_id - assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find" - end - def test_find_with_merged_options assert_equal 1, projects(:active_record).limited_developers.size assert_equal 1, projects(:active_record).limited_developers.to_a.size @@ -570,9 +506,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis') end - def test_find_should_prepend_to_association_order + def test_find_should_append_to_association_order ordered_developers = projects(:active_record).developers.order('projects.id') - assert_equal ['projects.id', 'developers.name desc, developers.id desc'], ordered_developers.order_values + assert_equal ['developers.name desc, developers.id desc', 'projects.id'], ordered_developers.order_values end def test_dynamic_find_all_should_respect_readonly_access @@ -669,16 +605,24 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_join_table_alias + # FIXME: `references` has no impact on the aliases generated for the join + # query. The fact that we pass `:developers_projects_join` to `references` + # and that the SQL string contains `developers_projects_join` is merely a + # coincidence. assert_equal( 3, Developer.references(:developers_projects_join).merge( :includes => {:projects => :developers}, - :where => 'developers_projects_join.joined_on IS NOT NULL' + :where => 'projects_developers_projects_join.joined_on IS NOT NULL' ).to_a.size ) end def test_join_with_group + # FIXME: `references` has no impact on the aliases generated for the join + # query. The fact that we pass `:developers_projects_join` to `references` + # and that the SQL string contains `developers_projects_join` is merely a + # coincidence. group = Developer.columns.inject([]) do |g, c| g << "developers.#{c.name}" g << "developers_projects_2.#{c.name}" @@ -688,7 +632,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal( 3, Developer.references(:developers_projects_join).merge( - :includes => {:projects => :developers}, :where => 'developers_projects_join.joined_on IS NOT NULL', + :includes => {:projects => :developers}, :where => 'projects_developers_projects_join.joined_on IS NOT NULL', :group => group.join(",") ).to_a.size ) @@ -702,12 +646,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_find_scoped_grouped - assert_equal 5, categories(:general).posts_grouped_by_title.size - assert_equal 1, categories(:technology).posts_grouped_by_title.size + assert_equal 5, categories(:general).posts_grouped_by_title.to_a.size + assert_equal 1, categories(:technology).posts_grouped_by_title.to_a.size end def test_find_scoped_grouped_having - assert_equal 2, projects(:active_record).well_payed_salary_groups.size + assert_equal 2, projects(:active_record).well_payed_salary_groups.to_a.size assert projects(:active_record).well_payed_salary_groups.all? { |g| g.salary > 10000 } end @@ -774,12 +718,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal project, developer.projects.first end - def test_self_referential_habtm_without_foreign_key_set_should_raise_exception - assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) { - SelfMember.new.friends - } - end - def test_dynamic_find_should_respect_association_include # SQL error in sort clause if :include is not included # due to Unknown column 'authors.id' @@ -791,21 +729,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, david.projects.count end - def test_count_with_counter_sql - developer = DeveloperWithCounterSQL.create(:name => 'tekin') - developer.project_ids = [projects(:active_record).id] - developer.save - developer.reload - assert_equal 1, developer.projects.count - end - - unless current_adapter?(:PostgreSQLAdapter) - def test_count_with_finder_sql - assert_equal 3, projects(:active_record).developers_with_finder_sql.count - assert_equal 3, projects(:active_record).developers_with_multiline_finder_sql.count - end - end - def test_association_proxy_transaction_method_starts_transaction_in_association_class Post.expects(:transaction) Category.first.posts.transaction do @@ -845,18 +768,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase 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 - test "has and belongs to many associations on new records use null relations" do projects = Developer.new.projects assert_no_queries do diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 1ddd380f23..dfc8a68e8c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -23,79 +23,6 @@ require 'models/categorization' require 'models/minivan' require 'models/speedometer' -class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase - class Invoice < ActiveRecord::Base - 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 - Invoice.create.custom_line_items.count(:conditions => {:amount => 0}) - end - end -end - -class HasManyAssociationsTestForCountWithCountSql < ActiveRecord::TestCase - class Invoice < ActiveRecord::Base - 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 - Invoice.create.custom_line_items.count(:conditions => {:amount => 0}) - end - end -end - -class HasManyAssociationsTestForCountWithVariousFinderSqls < ActiveRecord::TestCase - class Invoice < ActiveRecord::Base - ActiveSupport::Deprecation.silence do - has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT DISTINCT line_items.amount from line_items" - has_many :custom_full_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.invoice_id, line_items.amount from line_items" - has_many :custom_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT * from line_items" - has_many :custom_qualified_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items" - end - end - - def test_should_count_distinct_results - invoice = Invoice.new - invoice.custom_line_items << LineItem.new(:amount => 0) - invoice.custom_line_items << LineItem.new(:amount => 0) - invoice.save! - - assert_equal 1, invoice.custom_line_items.count - end - - def test_should_count_results_with_multiple_fields - invoice = Invoice.new - invoice.custom_full_line_items << LineItem.new(:amount => 0) - invoice.custom_full_line_items << LineItem.new(:amount => 0) - invoice.save! - - assert_equal 2, invoice.custom_full_line_items.count - end - - def test_should_count_results_with_star - invoice = Invoice.new - invoice.custom_star_line_items << LineItem.new(:amount => 0) - invoice.custom_star_line_items << LineItem.new(:amount => 0) - invoice.save! - - assert_equal 2, invoice.custom_star_line_items.count - end - - def test_should_count_results_with_qualified_star - invoice = Invoice.new - invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0) - invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0) - invoice.save! - - assert_equal 2, invoice.custom_qualified_star_line_items.count - end -end - class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase fixtures :authors, :posts, :comments @@ -118,6 +45,24 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Client.destroyed_client_ids.clear end + def test_anonymous_has_many + developer = Class.new(ActiveRecord::Base) { + self.table_name = 'developers' + dev = self + + developer_project = Class.new(ActiveRecord::Base) { + self.table_name = 'developers_projects' + belongs_to :developer, :class => dev + } + has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id' + } + dev = developer.first + named = Developer.find(dev.id) + assert_operator dev.developer_projects.count, :>, 0 + assert_equal named.projects.map(&:id).sort, + dev.developer_projects.map(&:project_id).sort + end + def test_create_from_association_should_respect_default_scope car = Car.create(:name => 'honda') assert_equal 'honda', car.name @@ -135,6 +80,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 'exotic', bulb.name end + def test_build_from_association_should_respect_scope + author = Author.new + + post = author.thinking_posts.build + assert_equal 'So I was thinking', post.title + end + def test_create_from_association_with_nil_values_should_work car = Car.create(:name => 'honda') @@ -148,6 +100,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 'defaulty', bulb.name end + def test_do_not_call_callbacks_for_delete_all + car = Car.create(:name => 'honda') + car.funky_bulbs.create! + assert_nothing_raised { car.reload.funky_bulbs.delete_all } + assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategey" + end + def test_building_the_associated_object_with_implicit_sti_base_class firm = DependentFirm.new company = firm.companies.build @@ -176,6 +135,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::SubclassNotFound) { firm.companies.build(:type => "Account") } end + test "building the association with an array" do + speedometer = Speedometer.new(speedometer_id: "a") + data = [{name: "first"}, {name: "second"}] + speedometer.minivans.build(data) + + assert_equal 2, speedometer.minivans.size + assert speedometer.save + assert_equal ["first", "second"], speedometer.reload.minivans.map(&:name) + end + def test_association_keys_bypass_attribute_protection car = Car.create(:name => 'honda') @@ -308,9 +277,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, companies(:first_firm).limited_clients.limit(nil).to_a.size end - def test_find_should_prepend_to_association_order + def test_find_should_append_to_association_order ordered_clients = companies(:first_firm).clients_sorted_desc.order('companies.id') - assert_equal ['companies.id', 'id DESC'], ordered_clients.order_values + assert_equal ['id DESC', 'companies.id'], ordered_clients.order_values end def test_dynamic_find_should_respect_association_order @@ -347,37 +316,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients_using_primary_key.first.name end - def test_finding_using_sql - 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.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.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.order("id").first.no_clients_using_counter_sql.size - end - - def test_counting_using_finder_sql - assert_equal 2, Firm.find(4).clients_using_sql.count - end - def test_belongs_to_sanity c = Client.new assert_nil c.firm @@ -405,20 +343,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) } end - def test_find_string_ids_when_using_finder_sql - firm = Firm.order("id").first + def test_find_ids_and_inverse_of + force_signal37_to_load_all_clients_of_firm - client = firm.clients_using_finder_sql.find("2") + firm = companies(:first_firm) + client = firm.clients_of_firm.find(3) assert_kind_of Client, client - client_ary = firm.clients_using_finder_sql.find(["2"]) + client_ary = firm.clients_of_firm.find([3]) assert_kind_of Array, client_ary assert_equal client, client_ary.first - - client_ary = firm.clients_using_finder_sql.find("2", "3") - assert_kind_of Array, client_ary - assert_equal 2, client_ary.size - assert client_ary.include?(client) end def test_find_all @@ -602,6 +536,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + def test_inverse_on_before_validate + firm = companies(:first_firm) + assert_queries(1) do + firm.clients_of_firm << Client.new("name" => "Natural Company") + end + end + def test_new_aliased_to_build company = companies(:first_firm) new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") } @@ -755,6 +696,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal topic.replies.to_a.size, topic.replies_count end + def test_pushing_association_updates_counter_cache + topic = Topic.order("id ASC").first + reply = Reply.create! + + assert_difference "topic.reload.replies_count", 1 do + topic.replies << reply + end + end + def test_deleting_updates_counter_cache_without_dependent_option post = posts(:welcome) @@ -789,6 +739,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + def test_calling_update_attributes_on_id_changes_the_counter_cache + topic = Topic.order("id ASC").first + original_count = topic.replies.to_a.size + assert_equal original_count, topic.replies_count + + first_reply = topic.replies.first + first_reply.update_attributes(:parent_id => nil) + assert_equal original_count - 1, topic.reload.replies_count + + first_reply.update_attributes(:parent_id => topic.id) + assert_equal original_count, topic.reload.replies_count + end + + def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache + topic1 = Topic.find(1) + topic2 = Topic.find(3) + original_count1 = topic1.replies.to_a.size + original_count2 = topic2.replies.to_a.size + + reply1 = topic1.replies.first + reply2 = topic2.replies.first + + reply1.update_attributes(:parent_id => topic2.id) + assert_equal original_count1 - 1, topic1.reload.replies_count + assert_equal original_count2 + 1, topic2.reload.replies_count + + reply2.update_attributes(:parent_id => topic1.id) + assert_equal original_count1, topic1.reload.replies_count + assert_equal original_count2, topic2.reload.replies_count + end + def test_deleting_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") @@ -800,13 +781,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all force_signal37_to_load_all_clients_of_firm - companies(:first_firm).clients_of_firm.create("name" => "Another Client") - clients = companies(:first_firm).clients_of_firm.to_a + companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client") + clients = companies(:first_firm).dependent_clients_of_firm.to_a assert_equal 2, clients.count - deleted = companies(:first_firm).clients_of_firm.delete_all - assert_equal clients.sort_by(&:id), deleted.sort_by(&:id) - assert_equal 0, companies(:first_firm).clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + + assert_difference "Client.count", -(clients.count) do + companies(:first_firm).dependent_clients_of_firm.delete_all + end end def test_delete_all_with_not_yet_loaded_association_collection @@ -880,18 +861,33 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) client_id = firm.dependent_clients_of_firm.first.id assert_equal 1, firm.dependent_clients_of_firm.size + assert_equal 1, Client.find_by_id(client_id).client_of - # :dependent means destroy is called on each client + # :delete_all is called on each client since the dependent options is :destroy firm.dependent_clients_of_firm.clear assert_equal 0, firm.dependent_clients_of_firm.size assert_equal 0, firm.dependent_clients_of_firm(true).size - assert_equal [client_id], Client.destroyed_client_ids[firm.id] + assert_equal [], Client.destroyed_client_ids[firm.id] # Should be destroyed since the association is dependent. assert_nil Client.find_by_id(client_id) end + def test_delete_all_with_option_delete_all + firm = companies(:first_firm) + client_id = firm.dependent_clients_of_firm.first.id + firm.dependent_clients_of_firm.delete_all(:delete_all) + assert_nil Client.find_by_id(client_id) + end + + def test_delete_all_accepts_limited_parameters + firm = companies(:first_firm) + assert_raise(ArgumentError) do + firm.dependent_clients_of_firm.delete_all(:destroy) + end + end + def test_clearing_an_exclusively_dependent_association_collection firm = companies(:first_firm) client_id = firm.exclusively_dependent_clients_of_firm.first.id @@ -1139,21 +1135,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal num_accounts, Account.count end - def test_restrict - firm = RestrictedFirm.create!(:name => 'restrict') - firm.companies.create(:name => 'child') - - assert !firm.companies.empty? - assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } - assert RestrictedFirm.exists?(:name => 'restrict') - assert firm.companies.exists?(:name => 'child') - end - - 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') @@ -1180,14 +1161,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_included_in_collection - assert companies(:first_firm).clients.include?(Client.find(2)) + assert_equal true, companies(:first_firm).clients.include?(Client.find(2)) end def test_included_in_collection_for_new_records client = Client.create(:name => 'Persisted') assert_nil client.client_of - assert !Firm.new.clients_of_firm.include?(client), - 'includes a client that does not belong to any firm' + assert_equal false, Firm.new.clients_of_firm.include?(client), + 'includes a client that does not belong to any firm' end def test_adding_array_and_collection @@ -1214,7 +1195,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.save firm.reload assert_equal 2, firm.clients.length - assert !firm.clients.include?(:first_client) + assert_equal false, firm.clients.include?(:first_client) end def test_replace_failure @@ -1275,24 +1256,44 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [readers(:michael_welcome).id], posts(:welcome).readers_with_person_ids end - def test_get_ids_for_unloaded_finder_sql_associations_loads_them - company = companies(:first_firm) - assert !company.clients_using_sql.loaded? - assert_equal [companies(:second_client).id], company.clients_using_sql_ids - assert company.clients_using_sql.loaded? - end - def test_get_ids_for_ordered_association assert_equal [companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids end + def test_get_ids_for_association_on_new_record_does_not_try_to_find_records + Company.columns # Load schema information so we don't query below + Contract.columns # if running just this test. + + company = Company.new + assert_queries(0) do + company.contract_ids + end + + assert_equal [], company.contract_ids + end + + def test_set_ids_for_association_on_new_record_applies_association_correctly + contract_a = Contract.create! + contract_b = Contract.create! + Contract.create! # another contract + company = Company.new(:name => "Some Company") + + company.contract_ids = [contract_a.id, contract_b.id] + assert_equal [contract_a.id, contract_b.id], company.contract_ids + assert_equal [contract_a, contract_b], company.contracts + + company.save! + assert_equal company, contract_a.reload.company + assert_equal company, contract_b.reload.company + end + def test_assign_ids_ignoring_blanks firm = Firm.create!(:name => 'Apple') firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, ''] firm.save! assert_equal 2, firm.clients(true).size - assert firm.clients.include?(companies(:second_client)) + assert_equal true, firm.clients.include?(companies(:second_client)) end def test_get_ids_for_through @@ -1326,7 +1327,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_no_queries do assert firm.clients.loaded? - assert firm.clients.include?(client) + assert_equal true, firm.clients.include?(client) end end @@ -1337,28 +1338,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.reload assert ! firm.clients.loaded? assert_queries(1) do - assert firm.clients.include?(client) + assert_equal true, firm.clients.include?(client) end assert ! firm.clients.loaded? end - def test_include_loads_collection_if_target_uses_finder_sql - firm = companies(:first_firm) - client = firm.clients_using_sql.first - - firm.reload - assert ! firm.clients_using_sql.loaded? - assert firm.clients_using_sql.include?(client) - assert firm.clients_using_sql.loaded? - end - - def test_include_returns_false_for_non_matching_record_to_verify_scoping firm = companies(:first_firm) client = Client.create!(:name => 'Not Associated') assert ! firm.clients.loaded? - assert ! firm.clients.include?(client) + assert_equal false, firm.clients.include?(client) end def test_calling_first_or_last_on_association_should_not_load_association @@ -1432,6 +1422,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal david.essays, Essay.where(writer_id: "David") end + def test_has_many_assignment_with_custom_primary_key + david = people(:david) + + assert_equal ["A Modest Proposal"], david.essays.map(&:name) + david.essays = [Essay.create!(name: "Remote Work" )] + assert_equal ["Remote Work"], david.essays.map(&:name) + end + def test_blank_custom_primary_key_on_new_record_should_not_run_queries author = Author.new assert !author.essays.loaded? @@ -1441,15 +1439,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - def test_calling_first_or_last_with_integer_on_association_should_load_association + def test_calling_first_or_last_with_integer_on_association_should_not_load_association firm = companies(:first_firm) + firm.clients.create(:name => 'Foo') + assert !firm.clients.loaded? - assert_queries 1 do + assert_queries 2 do firm.clients.first(2) firm.clients.last(2) end - assert firm.clients.loaded? + assert !firm.clients.loaded? end def test_calling_many_should_count_instead_of_loading_association @@ -1565,7 +1565,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_include_method_in_has_many_association_should_return_true_for_instance_added_with_build post = Post.new comment = post.comments.build - assert post.comments.include?(comment) + assert_equal true, post.comments.include?(comment) end def test_load_target_respects_protected_attributes @@ -1635,6 +1635,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal car.id, bulb.attributes_after_initialize['car_id'] end + def test_attributes_are_set_when_initialized_from_has_many_null_relationship + car = Car.new name: 'honda' + bulb = car.bulbs.where(name: 'headlight').first_or_initialize + assert_equal 'headlight', bulb.name + end + + def test_attributes_are_set_when_initialized_from_polymorphic_has_many_null_relationship + post = Post.new title: 'title', body: 'bar' + tag = Tag.create!(name: 'foo') + + tagging = post.taggings.where(tag: tag).first_or_initialize + + assert_equal tag.id, tagging.tag_id + assert_equal 'Post', tagging.taggable_type + end + def test_replace car = Car.create(:name => 'honda') bulb1 = car.bulbs.create @@ -1689,22 +1705,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - test ":finder_sql is deprecated" do - klass = Class.new(ActiveRecord::Base) - assert_deprecated { klass.has_many :foo, :finder_sql => 'lol' } - end - - test ":counter_sql is deprecated" do - klass = Class.new(ActiveRecord::Base) - assert_deprecated { klass.has_many :foo, :counter_sql => 'lol' } - end - - test "sum calculation with block for array compatibility is deprecated" do - assert_deprecated do - posts(:welcome).comments.sum { |c| c.id } - end - end - test "has many associations on new records use null relations" do post = Post.new 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 67d18f313a..c450b1beb5 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -5,6 +5,7 @@ require 'models/reference' require 'models/job' require 'models/reader' require 'models/comment' +require 'models/rating' require 'models/tag' require 'models/tagging' require 'models/author' @@ -27,7 +28,8 @@ require 'models/club' class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags, :owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses, - :subscribers, :books, :subscriptions, :developers, :categorizations, :essays + :subscribers, :books, :subscriptions, :developers, :categorizations, :essays, + :categories_posts, :clubs, :memberships # Dummies to force column loads so query counts are clean. def setup @@ -35,6 +37,136 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Reader.create :person_id => 0, :post_id => 0 end + def test_preload_sti_rhs_class + developers = Developer.includes(:firms).all.to_a + assert_no_queries do + developers.each { |d| d.firms } + end + end + + def test_preload_sti_middle_relation + club = Club.create!(name: 'Aaron cool banana club') + member1 = Member.create!(name: 'Aaron') + member2 = Member.create!(name: 'Cat') + + SuperMembership.create! club: club, member: member1 + CurrentMembership.create! club: club, member: member2 + + club1 = Club.includes(:members).find_by_id club.id + assert_equal [member1, member2].sort_by(&:id), + club1.members.sort_by(&:id) + end + + def make_model(name) + Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } + end + + def test_ordered_habtm + person_prime = Class.new(ActiveRecord::Base) do + def self.name; 'Person'; end + + has_many :readers + has_many :posts, -> { order('posts.id DESC') }, :through => :readers + end + posts = person_prime.includes(:posts).first.posts + + assert_operator posts.length, :>, 1 + posts.each_cons(2) do |left,right| + assert_operator left.id, :>, right.id + end + end + + def test_singleton_has_many_through + book = make_model "Book" + subscription = make_model "Subscription" + subscriber = make_model "Subscriber" + + subscriber.primary_key = 'nick' + subscription.belongs_to :book, class: book + subscription.belongs_to :subscriber, class: subscriber + + book.has_many :subscriptions, class: subscription + book.has_many :subscribers, through: :subscriptions, class: subscriber + + anonbook = book.first + namebook = Book.find anonbook.id + + assert_operator anonbook.subscribers.count, :>, 0 + anonbook.subscribers.each do |s| + assert_instance_of subscriber, s + end + assert_equal namebook.subscribers.map(&:id).sort, + anonbook.subscribers.map(&:id).sort + end + + def test_no_pk_join_table_append + lesson, _, student = make_no_pk_hm_t + + sicp = lesson.new(:name => "SICP") + ben = student.new(:name => "Ben Bitdiddle") + sicp.students << ben + assert sicp.save! + end + + def test_no_pk_join_table_delete + lesson, lesson_student, student = make_no_pk_hm_t + + sicp = lesson.new(:name => "SICP") + ben = student.new(:name => "Ben Bitdiddle") + louis = student.new(:name => "Louis Reasoner") + sicp.students << ben + sicp.students << louis + assert sicp.save! + + sicp.students.reload + assert_operator lesson_student.count, :>=, 2 + assert_no_difference('student.count') do + assert_difference('lesson_student.count', -2) do + sicp.students.destroy(*student.all.to_a) + end + end + end + + def test_no_pk_join_model_callbacks + lesson, lesson_student, student = make_no_pk_hm_t + + after_destroy_called = false + lesson_student.after_destroy do + after_destroy_called = true + end + + sicp = lesson.new(:name => "SICP") + ben = student.new(:name => "Ben Bitdiddle") + sicp.students << ben + assert sicp.save! + + sicp.students.reload + sicp.students.destroy(*student.all.to_a) + assert after_destroy_called, "after destroy should be called" + end + + def make_no_pk_hm_t + lesson = make_model 'Lesson' + student = make_model 'Student' + + lesson_student = make_model 'LessonStudent' + lesson_student.table_name = 'lessons_students' + + lesson_student.belongs_to :lesson, :class => lesson + lesson_student.belongs_to :student, :class => student + lesson.has_many :lesson_students, :class => lesson_student + lesson.has_many :students, :through => :lesson_students, :class => student + [lesson, lesson_student, student] + end + + def test_pk_is_not_required_for_join + post = Post.includes(:scategories).first + post2 = Post.includes(:categories).first + + assert_operator post.categories.length, :>, 0 + assert_equal post2.categories, post.categories + end + def test_include? person = Person.new post = Post.new @@ -57,6 +189,47 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert post.reload.people(true).include?(person) end + def test_delete_all_for_with_dependent_option_destroy + person = people(:david) + assert_equal 1, person.jobs_with_dependent_destroy.count + + assert_no_difference 'Job.count' do + assert_difference 'Reference.count', -1 do + person.reload.jobs_with_dependent_destroy.delete_all + end + end + end + + def test_delete_all_for_with_dependent_option_nullify + person = people(:david) + assert_equal 1, person.jobs_with_dependent_nullify.count + + assert_no_difference 'Job.count' do + assert_no_difference 'Reference.count' do + person.reload.jobs_with_dependent_nullify.delete_all + end + end + end + + def test_delete_all_for_with_dependent_option_delete_all + person = people(:david) + assert_equal 1, person.jobs_with_dependent_delete_all.count + + assert_no_difference 'Job.count' do + assert_difference 'Reference.count', -1 do + person.reload.jobs_with_dependent_delete_all.delete_all + end + end + end + + def test_concat + person = people(:david) + post = posts(:thinking) + post.people.concat [person] + assert_equal 1, post.people.size + assert_equal 1, post.people(true).size + end + def test_associate_existing_record_twice_should_add_to_target_twice post = posts(:thinking) person = people(:david) @@ -582,8 +755,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal post.author.author_favorites, post.author_favorites end + def test_merge_join_association_with_has_many_through_association_proxy + author = authors(:mary) + assert_nothing_raised { author.comments.ratings.to_sql } + end + def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys - assert_equal 1, owners(:blackbeard).toys.count + assert_equal 2, owners(:blackbeard).toys.count end def test_find_on_has_many_association_collection_with_include_and_conditions @@ -607,7 +785,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase sarah = Person.create!(:first_name => 'Sarah', :primary_contact_id => people(:susan).id, :gender => 'F', :number1_fan_id => 1) john = Person.create!(:first_name => 'John', :primary_contact_id => sarah.id, :gender => 'M', :number1_fan_id => 1) assert_equal sarah.agents, [john] - assert_equal people(:susan).agents.map(&:agents).flatten, people(:susan).agents_of_agents + assert_equal people(:susan).agents.flat_map(&:agents), people(:susan).agents_of_agents end def test_associate_existing_with_nonstandard_primary_key_on_belongs_to @@ -882,6 +1060,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [tags(:general)], post.reload.tags end + def test_has_many_through_obeys_order_on_through_association + owner = owners(:blackbeard) + assert owner.toys.to_sql.include?("pets.name desc") + assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name } + end + test "has many through associations on new records use null relations" do person = Person.new @@ -901,4 +1085,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase readers(:michael_authorless).update(first_post_id: 1) assert_equal [posts(:thinking)], person.reload.first_posts end + + def test_has_many_through_with_includes_in_through_association_scope + assert_not_empty posts(:welcome).author_address_extra_with_address + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 4ed09a3bf7..cdd386187b 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -158,22 +158,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nothing_raised { firm.destroy } end - def test_restrict - firm = RestrictedFirm.create!(:name => 'restrict') - firm.create_account(:credit_limit => 10) - - assert_not_nil firm.account - - assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } - assert RestrictedFirm.exists?(:name => 'restrict') - assert firm.account.present? - end - - def test_restrict_is_deprecated - klass = Class.new(ActiveRecord::Base) - assert_deprecated { klass.has_one :post, dependent: :restrict } - end - def test_restrict_with_exception firm = RestrictedWithExceptionFirm.create!(:name => 'restrict') firm.create_account(:credit_limit => 10) @@ -521,5 +505,34 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_no_queries { company.account = nil } account = Account.find(2) assert_queries { company.account = account } + + assert_no_queries { Firm.new.account = account } + end + + def test_has_one_assignment_triggers_save_on_change + pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") + ship = pirate.build_ship(name: 'old name') + ship.save! + + ship.name = 'new name' + assert ship.changed? + assert_queries(2) do + # One query for updating name and second query for updating pirate_id + pirate.ship = ship + end + + assert_equal 'new name', pirate.ship.reload.name + end + + def test_has_one_autosave_with_primary_key_manually_set + post = Post.create(id: 1234, title: "Some title", body: 'Some content') + author = Author.new(id: 33, name: 'Hank Moody') + + author.post = post + author.save + author.reload + + assert_not_nil author.post + assert_equal author.post, post 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 90c557e886..f2723f2e18 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -191,6 +191,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_preloading_has_one_through_on_belongs_to + MemberDetail.delete_all assert_not_nil @member.member_type @organization = organizations(:nsa) @member_detail = MemberDetail.new @@ -201,7 +202,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end @new_detail = @member_details[0] assert @new_detail.send(:association, :member_type).loaded? - assert_not_nil assert_no_queries { @new_detail.member_type } + assert_no_queries { @new_detail.member_type } end def test_save_of_record_with_loaded_has_one_through diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 4f246f575e..9fe5ff50d9 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -41,15 +41,20 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase assert_no_match(/WHERE/i, sql) end + def test_join_association_conditions_support_string_and_arel_expressions + assert_equal 0, Author.joins(:welcome_posts_with_comment).count + assert_equal 1, Author.joins(:welcome_posts_with_comments).count + end + def test_join_conditions_allow_nil_associations authors = Author.includes(:essays).where(:essays => {:id => nil}) assert_equal 2, authors.count end - def test_find_with_implicit_inner_joins_honors_readonly_without_select - authors = Author.joins(:posts).to_a - assert !authors.empty?, "expected authors to be non-empty" - assert authors.all? {|a| a.readonly? }, "expected all authors to be readonly" + def test_find_with_implicit_inner_joins_without_select_does_not_imply_readonly + authors = Author.joins(:posts) + assert_not authors.empty?, "expected authors to be non-empty" + assert authors.none? {|a| a.readonly? }, "expected no authors to be readonly" end def test_find_with_implicit_inner_joins_honors_readonly_with_select @@ -82,7 +87,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions 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) + authors_with_welcoming_post_titles = Author.all.merge!(joins: :posts, where: "posts.title like 'Welcome%'").distinct.calculate(:count, 'authors.id') assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" end @@ -104,4 +109,12 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase assert !posts(:welcome).tags.empty? assert Post.joins(:misc_tags).where(:id => posts(:welcome).id).empty? end + + test "the default scope of the target is applied when joining associations" do + author = Author.create! name: "Jon" + author.categorizations.create! + author.categorizations.create! special: true + + assert_equal [author], Author.where(id: author).joins(:special_categorizations) + end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 8c9b4fb921..893030345f 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -5,6 +5,102 @@ require 'models/interest' require 'models/zine' require 'models/club' require 'models/sponsor' +require 'models/rating' +require 'models/comment' +require 'models/car' +require 'models/bulb' +require 'models/mixed_case_monkey' + +class AutomaticInverseFindingTests < ActiveRecord::TestCase + fixtures :ratings, :comments, :cars + + def test_has_one_and_belongs_to_should_find_inverse_automatically_on_multiple_word_name + monkey_reflection = MixedCaseMonkey.reflect_on_association(:man) + man_reflection = Man.reflect_on_association(:mixed_case_monkey) + + assert_respond_to monkey_reflection, :has_inverse? + assert monkey_reflection.has_inverse?, "The monkey reflection should have an inverse" + assert_equal man_reflection, monkey_reflection.inverse_of, "The monkey reflection's inverse should be the man reflection" + + assert_respond_to man_reflection, :has_inverse? + assert man_reflection.has_inverse?, "The man reflection should have an inverse" + assert_equal monkey_reflection, man_reflection.inverse_of, "The man reflection's inverse should be the monkey reflection" + end + + def test_has_one_and_belongs_to_should_find_inverse_automatically + car_reflection = Car.reflect_on_association(:bulb) + bulb_reflection = Bulb.reflect_on_association(:car) + + assert_respond_to car_reflection, :has_inverse? + assert car_reflection.has_inverse?, "The Car reflection should have an inverse" + assert_equal bulb_reflection, car_reflection.inverse_of, "The Car reflection's inverse should be the Bulb reflection" + + assert_respond_to bulb_reflection, :has_inverse? + assert bulb_reflection.has_inverse?, "The Bulb reflection should have an inverse" + assert_equal car_reflection, bulb_reflection.inverse_of, "The Bulb reflection's inverse should be the Car reflection" + end + + def test_has_many_and_belongs_to_should_find_inverse_automatically + comment_reflection = Comment.reflect_on_association(:ratings) + rating_reflection = Rating.reflect_on_association(:comment) + + assert_respond_to comment_reflection, :has_inverse? + assert comment_reflection.has_inverse?, "The Comment reflection should have an inverse" + assert_equal rating_reflection, comment_reflection.inverse_of, "The Comment reflection's inverse should be the Rating reflection" + end + + def test_has_one_and_belongs_to_automatic_inverse_shares_objects + car = Car.first + bulb = Bulb.create!(car: car) + + assert_equal car.bulb, bulb, "The Car's bulb should be the original bulb" + + car.bulb.color = "Blue" + assert_equal car.bulb.color, bulb.color, "Changing the bulb's color on the car association should change the bulb's color" + + bulb.color = "Red" + assert_equal bulb.color, car.bulb.color, "Changing the bulb's color should change the bulb's color on the car association" + end + + def test_has_many_and_belongs_to_automatic_inverse_shares_objects_on_rating + comment = Comment.first + rating = Rating.create!(comment: comment) + + assert_equal rating.comment, comment, "The Rating's comment should be the original Comment" + + rating.comment.body = "Brogramming is the act of programming, like a bro." + assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body" + + comment.body = "Broseiden is the king of the sea of bros." + assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association" + end + + def test_has_many_and_belongs_to_automatic_inverse_shares_objects_on_comment + rating = Rating.create! + comment = Comment.first + rating.comment = comment + + assert_equal rating.comment, comment, "The Rating's comment should be the original Comment" + + rating.comment.body = "Brogramming is the act of programming, like a bro." + assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body" + + comment.body = "Broseiden is the king of the sea of bros." + assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association" + end + + def test_polymorphic_and_has_many_through_relationships_should_not_have_inverses + sponsor_reflection = Sponsor.reflect_on_association(:sponsorable) + + assert_respond_to sponsor_reflection, :has_inverse? + assert !sponsor_reflection.has_inverse?, "A polymorphic association should not find an inverse automatically" + + club_reflection = Club.reflect_on_association(:members) + + assert_respond_to club_reflection, :has_inverse? + assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically" + end +end class InverseAssociationTests < ActiveRecord::TestCase def test_should_allow_for_inverse_of_options_in_associations @@ -235,6 +331,22 @@ class InverseHasManyTests < ActiveRecord::TestCase assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" end + def test_parent_instance_should_be_shared_within_create_block_of_new_child + man = Man.first + interest = man.interests.build do |i| + assert i.man.equal?(man), "Man of child should be the same instance as a parent" + end + assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent" + end + + def test_parent_instance_should_be_shared_within_build_block_of_new_child + man = Man.first + interest = man.interests.build do |i| + assert i.man.equal?(man), "Man of child should be the same instance as a parent" + end + assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent" + end + def test_parent_instance_should_be_shared_with_poked_in_child m = men(:gordon) i = Interest.create(:topic => 'Industrial Revolution Re-enactment') @@ -278,9 +390,75 @@ class InverseHasManyTests < ActiveRecord::TestCase assert interests[1].man.equal? man end + def test_parent_instance_should_find_child_instance_using_child_instance_id + man = Man.create! + interest = Interest.create! + man.interests = [interest] + + assert interest.equal?(man.interests.first), "The inverse association should use the interest already created and held in memory" + assert interest.equal?(man.interests.find(interest.id)), "The inverse association should use the interest already created and held in memory" + assert man.equal?(man.interests.first.man), "Two inversion should lead back to the same object that was originally held" + assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held" + end + + def test_parent_instance_should_find_child_instance_using_child_instance_id_when_created + man = Man.create! + interest = Interest.create!(man: man) + + assert man.equal?(man.interests.first.man), "Two inverses should lead back to the same object that was originally held" + assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held" + + assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed" + man.name = "Ben Bitdiddle" + assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the parent name is changed" + man.interests.find(interest.id).man.name = "Alyssa P. Hacker" + assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the child name is changed" + end + + def test_find_on_child_instance_with_id_should_not_load_all_child_records + man = Man.create! + interest = Interest.create!(man: man) + + man.interests.find(interest.id) + assert_not man.interests.loaded? + end + + def test_raise_record_not_found_error_when_invalid_ids_are_passed + # delete all interest records to ensure that hard coded invalid_id(s) + # are indeed invalid. + Interest.delete_all + + man = Man.create! + + invalid_id = 245324523 + assert_raise(ActiveRecord::RecordNotFound) { man.interests.find(invalid_id) } + + invalid_ids = [8432342, 2390102913, 2453245234523452] + assert_raise(ActiveRecord::RecordNotFound) { man.interests.find(invalid_ids) } + end + + def test_raise_record_not_found_error_when_no_ids_are_passed + man = Man.create! + + assert_raise(ActiveRecord::RecordNotFound) { man.interests.find() } + end + def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests } end + + def test_child_instance_should_point_to_parent_without_saving + man = Man.new + i = Interest.create(:topic => 'Industrial Revolution Re-enactment') + + man.interests << i + assert_not_nil i.man + + i.man.name = "Charles" + assert_equal i.man.name, man.name + + assert !man.persisted? + end end class InverseBelongsToTests < ActiveRecord::TestCase @@ -425,6 +603,18 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" end + def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed + new_man = Man.new + face = Face.new + new_man.face = face + + old_inversed_man = face.man + new_man.save! + new_inversed_man = face.man + + assert_equal old_inversed_man.object_id, new_inversed_man.object_id + end + def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many i = interests(:llama_wrangling) m = i.polymorphic_man diff --git a/activerecord/test/cases/associations/join_dependency_test.rb b/activerecord/test/cases/associations/join_dependency_test.rb deleted file mode 100644 index 08c166dc33..0000000000 --- a/activerecord/test/cases/associations/join_dependency_test.rb +++ /dev/null @@ -1,8 +0,0 @@ -require "cases/helper" -require 'models/edge' - -class JoinDependencyTest < ActiveRecord::TestCase - def test_column_names_with_alias_handles_nil_primary_key - assert_equal Edge.column_names, ActiveRecord::Associations::JoinDependency::JoinBase.new(Edge).column_names_with_alias.map(&:first) - end -end
\ No newline at end of file diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 10ec33be75..aabeea025f 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -397,14 +397,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_many - assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.uniq.sort_by { |t| t.id } + assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by { |t| t.id } end def test_include_has_many_through_polymorphic_has_many 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 } + assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } end end @@ -464,7 +464,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert saved_post.reload.tags(true).include?(new_tag) - new_post = Post.new(:title => "Association replacmenet works!", :body => "You best believe it.") + new_post = Post.new(:title => "Association replacement works!", :body => "You best believe it.") saved_tag = tags(:general) new_post.tags << saved_tag diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index e355ed3495..8ef351cda8 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -186,7 +186,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a.sort_by(&:id) } groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) - assert_no_queries do + # postgresql test if randomly executed then executes "SHOW max_identifier_length". Hence + # the need to ignore certain predefined sqls that deal with system calls. + assert_no_queries(ignore_none: false) do assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id) end end @@ -212,7 +214,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload - authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) } + authors = assert_queries(4) { Author.includes(:post_categories).to_a.sort_by(&:id) } general, cooking = categories(:general), categories(:cooking) assert_no_queries do @@ -240,7 +242,8 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload - categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) } + Category.includes(:post_comments).to_a # preheat cache + categories = assert_queries(4) { Category.includes(:post_comments).to_a.sort_by(&:id) } greetings, more = comments(:greetings), comments(:more_greetings) assert_no_queries do @@ -268,7 +271,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload - authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) } + authors = assert_queries(6) { Author.includes(:category_post_comments).to_a.sort_by(&:id) } greetings, more = comments(:greetings), comments(:more_greetings) assert_no_queries do @@ -369,7 +372,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase prev_default_scope = Club.default_scopes [:includes, :preload, :joins, :eager_load].each do |q| - Club.default_scopes = [Club.send(q, :category)] + Club.default_scopes = [proc { Club.send(q, :category) }] assert_equal categories(:general), members(:groucho).reload.club_category end ensure @@ -410,7 +413,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # Mary and Bob both have posts in misc, but they are the only ones. authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id) - assert_equal [authors(:mary), authors(:bob)], authors.uniq.sort_by(&:id) + assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id) # Check the polymorphism of taggings is being observed correctly (in both joins) authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel') diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 201fa5d5a9..48e6fc5cd4 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -18,6 +18,8 @@ require 'models/ship' require 'models/liquid' require 'models/molecule' require 'models/electron' +require 'models/man' +require 'models/interest' class AssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :developers_projects, @@ -95,7 +97,7 @@ class AssociationsTest < ActiveRecord::TestCase def test_force_reload firm = Firm.new("name" => "A New Firm, Inc") firm.save - firm.clients.each {|c|} # forcing to load all clients + firm.clients.each {} # forcing to load all clients assert firm.clients.empty?, "New firm shouldn't have client objects" assert_equal 0, firm.clients.size, "New firm should have 0 clients" @@ -215,7 +217,7 @@ class AssociationProxyTest < ActiveRecord::TestCase assert_equal post.body, "More cool stuff!" end - def test_reload_returns_assocition + def test_reload_returns_association david = developers(:david) assert_nothing_raised do assert_equal david.projects, david.projects.reload.reload @@ -237,10 +239,25 @@ class AssociationProxyTest < ActiveRecord::TestCase assert david.projects.scope.is_a?(ActiveRecord::Relation) assert_equal david.projects, david.projects.scope end + + test "proxy object is cached" do + david = developers(:david) + assert david.projects.equal?(david.projects) + end + + test "inverses get set of subsets of the association" do + man = Man.create + man.interests.create + + man = Man.find(man.id) + + assert_queries(1) do + assert_equal man, man.interests.where("1=1").first.man + end + end end class OverridingAssociationsTest < ActiveRecord::TestCase - class Person < ActiveRecord::Base; end class DifferentPerson < ActiveRecord::Base; end class PeopleList < ActiveRecord::Base @@ -261,7 +278,7 @@ class OverridingAssociationsTest < ActiveRecord::TestCase def test_habtm_association_redefinition_callbacks_should_differ_and_not_inherited # redeclared association on AR descendant should not inherit callbacks from superclass callbacks = PeopleList.before_add_for_has_and_belongs_to_many - assert_equal([:enlist], callbacks) + assert_equal(1, callbacks.length) callbacks = DifferentPeopleList.before_add_for_has_and_belongs_to_many assert_equal([], callbacks) end @@ -269,7 +286,7 @@ class OverridingAssociationsTest < ActiveRecord::TestCase def test_has_many_association_redefinition_callbacks_should_differ_and_not_inherited # redeclared association on AR descendant should not inherit callbacks from superclass callbacks = PeopleList.before_add_for_has_many - assert_equal([:enlist], callbacks) + assert_equal(1, callbacks.length) callbacks = DifferentPeopleList.before_add_for_has_many assert_equal([], callbacks) end diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 8d8ff2f952..c0659fddef 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -15,13 +15,6 @@ module ActiveRecord include ActiveRecord::AttributeMethods - def self.define_attribute_methods - # Created in the inherited/included hook for "proper" ARs - @attribute_methods_mutex ||= Mutex.new - - super - end - def self.column_names %w{ one two three } end @@ -56,9 +49,9 @@ module ActiveRecord end def test_attribute_methods_generated? - assert(!@klass.attribute_methods_generated?, 'attribute_methods_generated?') + assert_not @klass.method_defined?(:one) @klass.define_attribute_methods - assert(@klass.attribute_methods_generated?, 'attribute_methods_generated?') + assert @klass.method_defined?(:one) end end end diff --git a/activerecord/test/cases/attribute_methods/serialization_test.rb b/activerecord/test/cases/attribute_methods/serialization_test.rb new file mode 100644 index 0000000000..75de773961 --- /dev/null +++ b/activerecord/test/cases/attribute_methods/serialization_test.rb @@ -0,0 +1,29 @@ +require "cases/helper" + +module ActiveRecord + module AttributeMethods + class SerializationTest < ActiveSupport::TestCase + class FakeColumn < Struct.new(:name) + def type; :integer; end + def type_cast(s); "#{s}!"; end + end + + class NullCoder + def load(v); v; end + end + + def test_type_cast_serialized_value + value = Serialization::Attribute.new(NullCoder.new, "Hello world", :serialized) + type = Serialization::Type.new(FakeColumn.new) + assert_equal "Hello world!", type.type_cast(value) + end + + def test_type_cast_unserialized_value + value = Serialization::Attribute.new(nil, "Hello world", :unserialized) + type = Serialization::Type.new(FakeColumn.new) + type.type_cast(value) + assert_equal "Hello world", type.type_cast(value) + end + end + end +end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index c503c21e27..9c66ed354e 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -27,6 +27,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers) end + def test_attribute_for_inspect + t = topics(:first) + t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters" + + assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on) + assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:title) + end + def test_attribute_present t = Topic.new t.title = "hello there!" @@ -69,7 +77,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_boolean_attributes - assert ! Topic.find(1).approved? + assert !Topic.find(1).approved? assert Topic.find(2).approved? end @@ -84,7 +92,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_set_attributes_without_hash topic = Topic.new - assert_nothing_raised { topic.attributes = '' } + assert_raise(ArgumentError) { topic.attributes = '' } end def test_integers_as_nil @@ -130,6 +138,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase 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') + assert_equal '10', keyboard.read_attribute_before_type_cast(:key_number) end # Syck calls respond_to? before actually calling initialize @@ -141,13 +150,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_respond_to topic, :title end - # IRB inspects the return value of "MyModel.allocate" - # by inspecting it. + # IRB inspects the return value of "MyModel.allocate". def test_allocated_object_can_be_inspected topic = Topic.allocate - topic.instance_eval { @attributes = nil } - assert_nothing_raised { topic.inspect } - assert topic.inspect, "#<Topic not initialized>" + assert_equal "#<Topic not initialized>", topic.inspect end def test_array_content @@ -159,8 +165,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_read_attributes_before_type_cast - category = Category.new({:name=>"Test categoty", :type => nil}) - category_attrs = {"name"=>"Test categoty", "id" => nil, "type" => nil, "categorizations_count" => nil} + category = Category.new({:name=>"Test category", :type => nil}) + category_attrs = {"name"=>"Test category", "id" => nil, "type" => nil, "categorizations_count" => nil} assert_equal category_attrs , category.attributes_before_type_cast end @@ -713,6 +719,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } end + def test_bulk_update_raise_unknown_attribute_errro + error = assert_raises(ActiveRecord::UnknownAttributeError) { + @target.new(:hello => "world") + } + assert @target, error.record + assert "hello", error.attribute + assert "unknown attribute: hello", error.message + end + def test_read_attribute_overwrites_private_method_not_considered_implemented # simulate a model with a db column that shares its name an inherited # private method (e.g. Object#system) @@ -744,21 +759,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert subklass.method_defined?(:id), "subklass is missing id method" end - def test_dispatching_column_attributes_through_method_missing_deprecated - Topic.define_attribute_methods - - topic = Topic.new(:id => 5) - topic.id = 5 - - topic.method(:id).owner.send(:undef_method, :id) - - assert_deprecated do - assert_equal 5, topic.id - end - ensure - Topic.undefine_attribute_methods - end - def test_read_attribute_with_nil_should_not_asplode assert_equal nil, Topic.new.read_attribute(nil) end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index e5cb4f8f7a..517d2674a7 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -439,7 +439,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa end def test_assign_ids_for_through_a_belongs_to - post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!") + post = Post.new(:title => "Assigning IDs works!", :body => "You heard it here first, folks!") post.person_ids = [people(:david).id, people(:michael).id] post.save post.reload @@ -566,7 +566,7 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase end class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_fixtures = false def setup super @@ -705,6 +705,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase ids.each { |id| assert_nil klass.find_by_id(id) } end + def test_should_not_resave_destroyed_association + @pirate.birds.create!(name: :parrot) + @pirate.birds.first.destroy + @pirate.save! + assert @pirate.reload.birds.empty? + end + def test_should_skip_validation_on_has_many_if_marked_for_destruction 2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") } @@ -764,6 +771,20 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase assert_equal 2, @pirate.birds.reload.length end + def test_should_save_new_record_that_has_same_value_as_existing_record_marked_for_destruction_on_field_that_has_unique_index + Bird.connection.add_index :birds, :name, unique: true + + 3.times { |i| @pirate.birds.create(name: "unique_birds_#{i}") } + + @pirate.birds[0].mark_for_destruction + @pirate.birds.build(name: @pirate.birds[0].name) + @pirate.save! + + assert_equal 3, @pirate.birds.reload.length + ensure + Bird.connection.remove_index :birds, column: :name + 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 @@ -846,8 +867,10 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.parrots.each { |parrot| parrot.mark_for_destruction } assert @pirate.save - assert_queries(0) do - assert @pirate.save + Pirate.transaction do + assert_queries(0) do + assert @pirate.save + end end end @@ -1335,7 +1358,7 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes assert !@pirate.valid? end - test "should not automatically asd validate associations without :validate => true" do + test "should not automatically add validate associations without :validate => true" do assert @pirate.valid? @pirate.non_validated_ship.name = '' assert @pirate.valid? @@ -1417,10 +1440,6 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas test "should generate validation methods for HABTM associations with :validate => true" do assert_respond_to @pirate, :validate_associated_records_for_parrots end - - test "should not generate validation methods for HABTM associations without :validate => true" do - assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrots) - end end class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index e5880a6b7b..82b20e8cee 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1,4 +1,7 @@ +# encoding: utf-8 + require "cases/helper" +require 'active_support/concurrency/latch' require 'models/post' require 'models/author' require 'models/topic' @@ -21,7 +24,6 @@ require 'models/parrot' require 'models/person' require 'models/edge' require 'models/joke' -require 'models/bulb' require 'models/bird' require 'models/car' require 'models/bulb' @@ -76,12 +78,6 @@ 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) @@ -141,13 +137,13 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_limit_should_sanitize_sql_injection_for_limit_without_comas + def test_limit_should_sanitize_sql_injection_for_limit_without_commas assert_raises(ArgumentError) do Topic.limit("1 select * from schema").to_a end end - def test_limit_should_sanitize_sql_injection_for_limit_with_comas + def test_limit_should_sanitize_sql_injection_for_limit_with_commas assert_raises(ArgumentError) do Topic.limit("1, 7 procedure help()").to_a end @@ -232,7 +228,7 @@ class BasicsTest < ActiveRecord::TestCase def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do time = Time.local(2000) topic = Topic.create('written_on' => time) saved_time = Topic.find(topic.id).reload.written_on @@ -245,7 +241,7 @@ class BasicsTest < ActiveRecord::TestCase def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) topic = Topic.create('written_on' => time) @@ -260,18 +256,20 @@ class BasicsTest < ActiveRecord::TestCase def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local with_env_tz 'America/New_York' do - time = Time.utc(2000) - topic = Topic.create('written_on' => time) - saved_time = Topic.find(topic.id).reload.written_on - assert_equal time, saved_time - assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a - assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a + with_timezone_config default: :local do + time = Time.utc(2000) + topic = Topic.create('written_on' => time) + saved_time = Topic.find(topic.id).reload.written_on + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a + assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a + end end end def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local with_env_tz 'America/New_York' do - with_active_record_default_timezone :local do + with_timezone_config default: :local do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) topic = Topic.create('written_on' => time) @@ -307,20 +305,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal("last_read", ex.errors[0].attribute) end - def test_initialize_abstract_class - e = assert_raises(NotImplementedError) do - FirstAbstractClass.new - end - assert_equal("FirstAbstractClass is an abstract class and can not be instantiated.", e.message) - end - - def test_initialize_base - e = assert_raises(NotImplementedError) do - ActiveRecord::Base.new - end - assert_equal("ActiveRecord::Base is an abstract class and can not be instantiated.", e.message) - end - def test_create_after_initialize_without_block cb = CustomBulb.create(:name => 'Dude') assert_equal('Dude', cb.name) @@ -505,25 +489,25 @@ class BasicsTest < ActiveRecord::TestCase # Oracle, and Sybase do not have a TIME datatype. unless current_adapter?(:OracleAdapter, :SybaseAdapter) def test_utc_as_time_zone - Topic.default_timezone = :utc - attributes = { "bonus_time" => "5:42:00AM" } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time - Topic.default_timezone = :local + with_timezone_config default: :utc do + attributes = { "bonus_time" => "5:42:00AM" } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time + end end def test_utc_as_time_zone_and_new - Topic.default_timezone = :utc - attributes = { "bonus_time(1i)"=>"2000", - "bonus_time(2i)"=>"1", - "bonus_time(3i)"=>"1", - "bonus_time(4i)"=>"10", - "bonus_time(5i)"=>"35", - "bonus_time(6i)"=>"50" } - topic = Topic.new(attributes) - assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time - Topic.default_timezone = :local + with_timezone_config default: :utc do + attributes = { "bonus_time(1i)"=>"2000", + "bonus_time(2i)"=>"1", + "bonus_time(3i)"=>"1", + "bonus_time(4i)"=>"10", + "bonus_time(5i)"=>"35", + "bonus_time(6i)"=>"50" } + topic = Topic.new(attributes) + assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time + end end end @@ -570,11 +554,27 @@ class BasicsTest < ActiveRecord::TestCase assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ] end - def test_comparison - topic_1 = Topic.create! - topic_2 = Topic.create! + def test_create_without_prepared_statement + topic = Topic.connection.unprepared_statement do + Topic.create(:title => 'foo') + end + + assert_equal topic, Topic.find(topic.id) + end + + def test_destroy_without_prepared_statement + topic = Topic.create(title: 'foo') + Topic.connection.unprepared_statement do + Topic.find(topic.id).destroy + end + + assert_equal nil, Topic.find_by_id(topic.id) + end - assert_equal [topic_2, topic_1].sort, [topic_1, topic_2] + def test_blank_ids + one = Subscriber.new(:id => '') + two = Subscriber.new(:id => '') + assert_equal one, two end def test_comparison_with_different_objects @@ -583,6 +583,13 @@ class BasicsTest < ActiveRecord::TestCase assert_nil topic <=> category end + def test_comparison_with_different_objects_in_array + topic = Topic.create + assert_raises(ArgumentError) do + [1, topic].sort + end + end + def test_readonly_attributes assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes @@ -596,10 +603,24 @@ 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 } + def test_unicode_column_name + Weird.reset_column_information + weird = Weird.create(:なまえ => 'たこ焼き仮面') + assert_equal 'たこ焼き仮面', weird.なまえ + end + + def test_respect_internal_encoding + if current_adapter?(:PostgreSQLAdapter) + skip 'pg does not respect internal encoding and always returns utf8' + end + old_default_internal = Encoding.default_internal + silence_warnings { Encoding.default_internal = "EUC-JP" } + + Weird.reset_column_information + + assert_equal ["EUC-JP"], Weird.columns.map {|c| c.name.encoding.name }.uniq + ensure + silence_warnings { Encoding.default_internal = old_default_internal } end def test_non_valid_identifier_column_name @@ -624,12 +645,14 @@ class BasicsTest < ActiveRecord::TestCase # Oracle, and Sybase do not have a TIME datatype. return true if current_adapter?(:OracleAdapter, :SybaseAdapter) - attributes = { - "bonus_time" => "5:42:00AM" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time + with_timezone_config default: :local do + attributes = { + "bonus_time" => "5:42:00AM" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time + end end def test_attributes_on_dummy_time_with_invalid_time @@ -817,19 +840,18 @@ class BasicsTest < ActiveRecord::TestCase # TODO: extend defaults tests to other databases! if current_adapter?(:PostgreSQLAdapter) def test_default - tz = Default.default_timezone - Default.default_timezone = :local - default = Default.new - Default.default_timezone = tz - - # fixed dates / times - assert_equal Date.new(2004, 1, 1), default.fixed_date - assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time - - # char types - assert_equal 'Y', default.char1 - assert_equal 'a varchar field', default.char2 - assert_equal 'a text field', default.char3 + with_timezone_config default: :local do + default = Default.new + + # fixed dates / times + assert_equal Date.new(2004, 1, 1), default.fixed_date + assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time + + # char types + assert_equal 'Y', default.char1 + assert_equal 'a varchar field', default.char2 + assert_equal 'a text field', default.char3 + end end class Geometric < ActiveRecord::Base; end @@ -854,7 +876,7 @@ class BasicsTest < ActiveRecord::TestCase # Reload and check that we have all the geometric attributes. h = Geometric.find(g.id) - assert_equal '(5,6.1)', h.a_point + assert_equal [5.0, 6.1], h.a_point assert_equal '[(2,3),(5.5,7)]', h.a_line_segment assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path @@ -883,7 +905,7 @@ class BasicsTest < ActiveRecord::TestCase # Reload and check that we have all the geometric attributes. h = Geometric.find(g.id) - assert_equal '(5,6.1)', h.a_point + assert_equal [5.0, 6.1], h.a_point assert_equal '[(2,3),(5.5,7)]', h.a_line_segment assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path @@ -894,6 +916,29 @@ class BasicsTest < ActiveRecord::TestCase objs = Geometric.find_by_sql ["select isclosed(a_path) from geometrics where id = ?", g.id] assert_equal true, objs[0].isclosed + + # test native ruby formats when defining the geometric types + g = Geometric.new( + :a_point => [5.0, 6.1], + #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql + :a_line_segment => '((2.0, 3), (5.5, 7.0))', + :a_box => '(2.0, 3), (5.5, 7.0)', + :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', # ( ) is a closed path + :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0', + :a_circle => '((5.3, 10.4), 2)' + ) + + assert g.save + + # Reload and check that we have all the geometric attributes. + h = Geometric.find(g.id) + + assert_equal [5.0, 6.1], h.a_point + assert_equal '[(2,3),(5.5,7)]', h.a_line_segment + assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner + assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path + assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon + assert_equal '<(5.3,10.4),2>', h.a_circle end end @@ -1038,7 +1083,7 @@ class BasicsTest < ActiveRecord::TestCase Joke.reset_sequence_name end - def test_dont_clear_inheritnce_column_when_setting_explicitly + def test_dont_clear_inheritance_column_when_setting_explicitly Joke.inheritance_column = "my_type" before_inherit = Joke.inheritance_column @@ -1105,7 +1150,7 @@ 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.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count(distinct: true) + res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").distinct.count end assert_equal res6, res7 end @@ -1156,8 +1201,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_keeps_multiple_group_values - 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 + combined = Developer.all.merge!(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at, developers.created_on, developers.updated_on').to_a + assert_equal combined, Developer.all.merge!(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at', 'developers.created_on', 'developers.updated_on']).to_a end def test_find_symbol_ordered_last @@ -1220,93 +1265,6 @@ class BasicsTest < ActiveRecord::TestCase assert_no_queries { assert true } end - def test_to_param_should_return_string - assert_kind_of String, Client.first.to_param - end - - def test_to_param_returns_id_even_if_not_persisted - client = Client.new - client.id = 1 - assert_equal "1", client.to_param - end - - def test_inspect_class - assert_equal 'ActiveRecord::Base', ActiveRecord::Base.inspect - assert_equal 'LoosePerson(abstract)', LoosePerson.inspect - assert_match(/^Topic\(id: integer, title: string/, Topic.inspect) - end - - def test_inspect_instance - topic = topics(:first) - assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", important: nil, approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_s(:db)}", updated_at: "#{topic.updated_at.to_s(:db)}">), topic.inspect - end - - def test_inspect_new_instance - assert_match(/Topic id: nil/, Topic.new.inspect) - end - - def test_inspect_limited_select_instance - 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 - assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect - end - - def test_attribute_for_inspect - t = topics(:first) - t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters" - - assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on) - assert_equal '"The First Topic Now Has A Title With\nNewlines And M..."', t.attribute_for_inspect(:title) - end - - def test_becomes - assert_kind_of Reply, topics(:first).becomes(Reply) - assert_equal "The First Topic", topics(:first).becomes(Reply).title - end - - def test_becomes_includes_errors - company = Company.new(:name => nil) - assert !company.valid? - original_errors = company.errors - client = company.becomes(Client) - assert_equal original_errors, client.errors - end - - def test_silence_sets_log_level_to_error_in_block - original_logger = ActiveRecord::Base.logger - - assert_deprecated do - log = StringIO.new - ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) - ActiveRecord::Base.logger.level = Logger::DEBUG - ActiveRecord::Base.silence do - ActiveRecord::Base.logger.warn "warn" - ActiveRecord::Base.logger.error "error" - end - assert_equal "error\n", log.string - end - ensure - ActiveRecord::Base.logger = original_logger - end - - def test_silence_sets_log_level_back_to_level_before_yield - original_logger = ActiveRecord::Base.logger - - assert_deprecated do - log = StringIO.new - ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) - ActiveRecord::Base.logger.level = Logger::WARN - ActiveRecord::Base.silence do - end - assert_equal Logger::WARN, ActiveRecord::Base.logger.level - end - ensure - ActiveRecord::Base.logger = original_logger - end - def test_benchmark_with_log_level original_logger = ActiveRecord::Base.logger log = StringIO.new @@ -1358,9 +1316,9 @@ class BasicsTest < ActiveRecord::TestCase def test_clear_cache! # preheat cache - c1 = Post.connection.schema_cache.columns['posts'] + c1 = Post.connection.schema_cache.columns('posts') ActiveRecord::Base.clear_cache! - c2 = Post.connection.schema_cache.columns['posts'] + c2 = Post.connection.schema_cache.columns('posts') assert_not_equal c1, c2 end @@ -1369,9 +1327,9 @@ class BasicsTest < ActiveRecord::TestCase UnloadablePost.send(:current_scope=, UnloadablePost.all) UnloadablePost.unloadable - assert_not_nil Thread.current[:UnloadablePost_current_scope] + assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost") ActiveSupport::Dependencies.remove_unloadable_constants! - assert_nil Thread.current[:UnloadablePost_current_scope] + assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost") ensure Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost) end @@ -1401,6 +1359,36 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 1, post.comments.length end + def test_marshal_between_processes + skip "can't marshal between processes when using an in-memory db" if in_memory_db? + skip "fork isn't supported" unless Process.respond_to?(:fork) + + # Define a new model to ensure there are no caches + if self.class.const_defined?("Post", false) + flunk "there should be no post constant" + end + + self.class.const_set("Post", Class.new(ActiveRecord::Base) { + has_many :comments + }) + + rd, wr = IO.pipe + + ActiveRecord::Base.connection_handler.clear_all_connections! + + fork do + rd.close + post = Post.new + post.comments.build + wr.write Marshal.dump(post) + wr.close + end + + wr.close + assert Marshal.load rd.read + rd.close + end + def test_marshalling_new_record_round_trip_with_associations post = Post.new post.comments.build @@ -1423,62 +1411,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal [], AbstractCompany.attribute_names end - def test_cache_key_for_existing_record_is_not_timezone_dependent - ActiveRecord::Base.time_zone_aware_attributes = true - - Time.zone = "UTC" - utc_key = Developer.first.cache_key - - Time.zone = "EST" - est_key = Developer.first.cache_key - - assert_equal utc_key, est_key - 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(:nsec)}", dev.cache_key - end - - def test_cache_key_format_for_existing_record_with_updated_at_and_custom_cache_timestamp_format - dev = CachedDeveloper.first - assert_equal "cached_developers/#{dev.id}-#{dev.updated_at.utc.to_s(:number)}", dev.cache_key - end - - def test_cache_key_changes_when_child_touched - car = Car.create - Bulb.create(car: car) - - key = car.cache_key - car.bulb.touch - car.reload - assert_not_equal key, car.cache_key - end - - def test_cache_key_format_for_existing_record_with_nil_updated_timestamps - dev = Developer.first - dev.update_columns(updated_at: nil, updated_on: nil) - assert_match(/\/#{dev.id}$/, dev.cache_key) - end - - def test_cache_key_for_updated_on - dev = Developer.first - dev.updated_at = nil - assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key - end - - def test_cache_key_for_newer_updated_at - dev = Developer.first - dev.updated_at += 3600 - assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key - end - - def test_cache_key_for_newer_updated_on - dev = Developer.first - dev.updated_on += 3600 - assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key - end - def test_touch_should_raise_error_on_a_new_object company = Company.new(:rating => 1, :name => "37signals", :firm_name => "37signals") assert_raises(ActiveRecord::ActiveRecordError) do @@ -1486,19 +1418,18 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_cache_key_format_is_precise_enough - dev = Developer.first - key = dev.cache_key - dev.touch - assert_not_equal key, dev.cache_key - end - def test_uniq_delegates_to_scoped scope = stub Bird.stubs(:all).returns(mock(:uniq => scope)) assert_equal scope, Bird.uniq end + def test_distinct_delegates_to_scoped + scope = stub + Bird.stubs(:all).returns(mock(:distinct => scope)) + assert_equal scope, Bird.distinct + end + def test_table_name_with_2_abstract_subclasses assert_equal "photos", Photo.table_name end @@ -1563,4 +1494,60 @@ class BasicsTest < ActiveRecord::TestCase klass = Class.new(ActiveRecord::Base) assert_equal ['foo'], klass.all.merge!(select: 'foo').select_values end + + test "connection_handler can be overridden" do + klass = Class.new(ActiveRecord::Base) + orig_handler = klass.connection_handler + new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new + thread_connection_handler = nil + + t = Thread.new do + klass.connection_handler = new_handler + thread_connection_handler = klass.connection_handler + end + t.join + + assert_equal klass.connection_handler, orig_handler + assert_equal thread_connection_handler, new_handler + end + + test "new threads get default the default connection handler" do + klass = Class.new(ActiveRecord::Base) + orig_handler = klass.connection_handler + handler = nil + + t = Thread.new do + handler = klass.connection_handler + end + t.join + + assert_equal handler, orig_handler + assert_equal klass.connection_handler, orig_handler + assert_equal klass.default_connection_handler, orig_handler + end + + test "changing a connection handler in a main thread does not poison the other threads" do + klass = Class.new(ActiveRecord::Base) + orig_handler = klass.connection_handler + new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new + after_handler = nil + latch1 = ActiveSupport::Concurrency::Latch.new + latch2 = ActiveSupport::Concurrency::Latch.new + + t = Thread.new do + klass.connection_handler = new_handler + latch1.release + latch2.await + after_handler = klass.connection_handler + end + + latch1.await + + klass.connection_handler = orig_handler + latch2.release + t.join + + assert_equal after_handler, new_handler + assert_equal orig_handler, klass.connection_handler + end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index acb8b5f562..38c2560d69 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -11,21 +11,39 @@ class EachTest < ActiveRecord::TestCase Post.count('id') # preheat arel's table cache end - def test_each_should_excecute_one_query_per_batch - assert_queries(Post.count + 1) do + def test_each_should_execute_one_query_per_batch + assert_queries(@total + 1) do Post.find_each(:batch_size => 1) do |post| assert_kind_of Post, post end end end - def test_each_should_not_return_query_chain_and_execcute_only_one_query + def test_each_should_not_return_query_chain_and_execute_only_one_query assert_queries(1) do result = Post.find_each(:batch_size => 100000){ } assert_nil result end end + def test_each_should_return_an_enumerator_if_no_block_is_present + assert_queries(1) do + Post.find_each(:batch_size => 100000).with_index do |post, index| + assert_kind_of Post, post + assert_kind_of Integer, index + end + end + end + + def test_each_enumerator_should_execute_one_query_per_batch + assert_queries(@total + 1) do + Post.find_each(:batch_size => 1).with_index do |post, index| + assert_kind_of Post, post + assert_kind_of Integer, index + end + end + end + def test_each_should_raise_if_select_is_set_without_id assert_raise(RuntimeError) do Post.select(:title).find_each(:batch_size => 1) { |post| post } @@ -50,8 +68,18 @@ class EachTest < ActiveRecord::TestCase Post.order("title").find_each { |post| post } end + def test_logger_not_required + previous_logger = ActiveRecord::Base.logger + ActiveRecord::Base.logger = nil + assert_nothing_raised do + Post.limit(1).find_each { |post| post } + end + ensure + ActiveRecord::Base.logger = previous_logger + end + def test_find_in_batches_should_return_batches - assert_queries(Post.count + 1) do + assert_queries(@total + 1) do Post.find_in_batches(:batch_size => 1) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first @@ -60,7 +88,7 @@ class EachTest < ActiveRecord::TestCase end def test_find_in_batches_should_start_from_the_start_option - assert_queries(Post.count) do + assert_queries(@total) do Post.find_in_batches(:batch_size => 1, :start => 2) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first @@ -68,15 +96,13 @@ class EachTest < ActiveRecord::TestCase end end - def test_find_in_batches_shouldnt_excute_query_unless_needed - post_count = Post.count - + def test_find_in_batches_shouldnt_execute_query_unless_needed assert_queries(2) do - Post.find_in_batches(:batch_size => post_count) {|batch| assert_kind_of Array, batch } + Post.find_in_batches(:batch_size => @total) {|batch| assert_kind_of Array, batch } end assert_queries(1) do - Post.find_in_batches(:batch_size => post_count + 1) {|batch| assert_kind_of Array, batch } + Post.find_in_batches(:batch_size => @total + 1) {|batch| assert_kind_of Array, batch } end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index be49e948fc..2c41656b3d 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -6,6 +6,7 @@ require 'models/edge' require 'models/organization' require 'models/possession' require 'models/topic' +require 'models/reply' require 'models/minivan' require 'models/speedometer' require 'models/ship_part' @@ -28,6 +29,10 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 53.0, value end + def test_should_resolve_aliased_attributes + assert_equal 318, Account.sum(:available_credit) + end + def test_should_return_decimal_average_of_integer_field value = Account.average(:id) assert_equal 3.5, value @@ -96,25 +101,24 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_order_by_grouped_field - c = Account.all.merge!(:group => :firm_id, :order => "firm_id").sum(:credit_limit) + c = Account.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.all.merge!(:group => :firm_id, :order => "sum_credit_limit desc, firm_id").sum(:credit_limit) + c = Account.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.all.merge!(:where => "firm_id IS NOT NULL", - :group => :firm_id, :order => "firm_id", :limit => 2).sum(:credit_limit) + c = Account.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.all.merge!(:where => "firm_id IS NOT NULL", :group => :firm_id, - :order => "firm_id", :limit => 2, :offset => 1).sum(:credit_limit) + c = Account.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 @@ -163,9 +167,17 @@ class CalculationsTest < ActiveRecord::TestCase assert_no_match(/OFFSET/, queries.first) end + def test_count_on_invalid_columns_raises + e = assert_raises(ActiveRecord::StatementInvalid) { + Account.select("credit_limit, firm_name").count + } + + assert_match %r{accounts}i, e.message + assert_match "credit_limit, firm_name", e.message + end + def test_should_group_by_summed_field_having_condition - c = Account.all.merge!(:group => :firm_id, - :having => 'sum(credit_limit) > 50').sum(:credit_limit) + c = Account.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] @@ -200,17 +212,15 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_with_conditions - c = Account.all.merge!(:where => 'firm_id > 1', - :group => :firm_id).sum(:credit_limit) + c = Account.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.all.merge!(:where => 'firm_id > 1', - :group => :firm_id, - :having => 'sum(credit_limit) > 60').sum(:credit_limit) + c = Account.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] @@ -305,8 +315,8 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_count_selected_field_with_include - assert_equal 6, Account.includes(:firm).count(:distinct => true) - assert_equal 4, Account.includes(:firm).select(:credit_limit).count(:distinct => true) + assert_equal 6, Account.includes(:firm).distinct.count + assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count end def test_should_not_perform_joined_include_by_default @@ -322,7 +332,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_count_scoped_select Account.update_all("credit_limit = NULL") - assert_equal 0, Account.all.merge!(:select => "credit_limit").count + assert_equal 0, Account.select("credit_limit").count end def test_should_count_scoped_select_with_options @@ -330,32 +340,37 @@ class CalculationsTest < ActiveRecord::TestCase Account.last.update_columns('credit_limit' => 49) Account.first.update_columns('credit_limit' => 51) - assert_equal 1, Account.all.merge!(:select => "credit_limit").where('credit_limit >= 50').count + assert_equal 1, Account.select("credit_limit").where('credit_limit >= 50').count end def test_should_count_manual_select_with_include - assert_equal 6, Account.all.merge!(:select => "DISTINCT accounts.id", :includes => :firm).count + assert_equal 6, Account.select("DISTINCT accounts.id").includes(:firm).count end def test_count_with_column_parameter assert_equal 5, Account.count(:firm_id) end - def test_count_with_uniq + def test_count_with_distinct + assert_equal 4, Account.select(:credit_limit).distinct.count assert_equal 4, Account.select(:credit_limit).uniq.count end + def test_count_with_aliased_attribute + assert_equal 6, Account.count(:available_credit) + end + def test_count_with_column_and_options_parameter 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.joins(:firm).count('companies.id') - assert_equal 4, Account.joins(:firm).count('companies.id', :distinct => true) + assert_equal 4, Account.joins(:firm).distinct.count('companies.id') end def test_should_count_field_in_joined_table_with_group_by - c = Account.all.merge!(:group => 'accounts.firm_id', :joins => :firm).count('companies.id') + c = Account.group('accounts.firm_id').joins(:firm).count('companies.id') [1,6,2,9].each { |firm_id| assert c.keys.include?(firm_id) } end @@ -395,12 +410,6 @@ class CalculationsTest < ActiveRecord::TestCase Account.where("credit_limit > 50").from('accounts').sum(:credit_limit) end - def test_sum_array_compatibility_deprecation - assert_deprecated do - assert_equal Account.sum(:credit_limit), Account.sum(&:credit_limit) - end - end - def test_average_with_from_option assert_equal Account.average(:credit_limit), Account.from('accounts').average(:credit_limit) assert_equal Account.where("credit_limit > 50").average(:credit_limit), @@ -455,7 +464,7 @@ class CalculationsTest < ActiveRecord::TestCase approved_topics_count = Topic.group(:approved).count(:author_name)[true] assert_equal approved_topics_count, 3 # Count the number of distinct authors for approved Topics - distinct_authors_for_approved_count = Topic.group(:approved).count(:author_name, :distinct => true)[true] + distinct_authors_for_approved_count = Topic.group(:approved).distinct.count(:author_name)[true] assert_equal distinct_authors_for_approved_count, 2 end @@ -463,6 +472,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [1,2,3,4], Topic.order(:id).pluck(:id) end + def test_pluck_without_column_names + assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]], + Company.order(:id).limit(1).pluck + end + def test_pluck_type_cast topic = topics(:first) relation = Topic.where(:id => topic.id) @@ -481,6 +495,10 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [contract.id], company.contracts.pluck(:id) end + def test_pluck_on_aliased_attribute + assert_equal 'The First Topic', Topic.order(:id).pluck(:heading).first + end + def test_pluck_with_serialization t = Topic.create!(:content => { :foo => :bar }) assert_equal [{:foo => :bar}], Topic.where(:id => t.id).pluck(:content) @@ -520,6 +538,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal Company.all.map(&:id).sort, Company.ids.sort end + def test_pluck_with_includes_limit_and_empty_result + assert_equal [], Topic.includes(:replies).limit(0).pluck(:id) + assert_equal [], Topic.includes(:replies).limit(1).where('0 = 1').pluck(:id) + 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) diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 7457bafd4e..c8f56e3c73 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -43,7 +43,7 @@ class CallbackDeveloper < ActiveRecord::Base end class CallbackDeveloperWithFalseValidation < CallbackDeveloper - before_validation proc { |model| model.history << [:before_validation, :returning_false]; return false } + before_validation proc { |model| model.history << [:before_validation, :returning_false]; false } before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } end @@ -520,7 +520,7 @@ class CallbacksTest < ActiveRecord::TestCase ], david.history end - def test_inheritence_of_callbacks + def test_inheritance_of_callbacks parent = ParentDeveloper.new assert !parent.after_save_called parent.save diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb index d91646efca..5e43082c33 100644 --- a/activerecord/test/cases/clone_test.rb +++ b/activerecord/test/cases/clone_test.rb @@ -29,5 +29,12 @@ module ActiveRecord topic.author_name = 'Aaron' assert_equal 'Aaron', cloned.author_name end + + def test_freezing_a_cloned_model_does_not_freeze_clone + cloned = Topic.new + clone = cloned.clone + cloned.freeze + assert_not clone.frozen? + end end end diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb index b874adc081..b72c54f97b 100644 --- a/activerecord/test/cases/coders/yaml_column_test.rb +++ b/activerecord/test/cases/coders/yaml_column_test.rb @@ -43,10 +43,20 @@ module ActiveRecord assert_equal [], coder.load([]) end - def test_load_swallows_yaml_exceptions + def test_load_doesnt_swallow_yaml_exceptions coder = YAMLColumn.new bad_yaml = '--- {' - assert_equal bad_yaml, coder.load(bad_yaml) + assert_raises(Psych::SyntaxError) do + coder.load(bad_yaml) + end + end + + def test_load_doesnt_handle_undefined_class_or_module + coder = YAMLColumn.new + missing_class_yaml = '--- !ruby/object:DoesNotExistAndShouldntEver {}\n' + assert_raises(ArgumentError) do + coder.load(missing_class_yaml) + end end end end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index bd2fbaa7db..dbb2f223cd 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -8,6 +8,7 @@ module ActiveRecord def @adapter.native_database_types {:string => "varchar"} end + @viz = @adapter.schema_creation end def test_can_set_coder @@ -35,25 +36,25 @@ module ActiveRecord def test_should_not_include_default_clause_when_default_is_null column = Column.new("title", nil, "varchar(20)") column_def = ColumnDefinition.new( - @adapter, column.name, "string", + column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) - assert_equal "title varchar(20)", column_def.to_sql + assert_equal "title varchar(20)", @viz.accept(column_def) end def test_should_include_default_clause_when_default_is_present column = Column.new("title", "Hello", "varchar(20)") column_def = ColumnDefinition.new( - @adapter, column.name, "string", + column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) - assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, column_def.to_sql + assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, @viz.accept(column_def) end def test_should_specify_not_null_if_null_option_is_false column = Column.new("title", "Hello", "varchar(20)", false) column_def = ColumnDefinition.new( - @adapter, column.name, "string", + column.name, "string", column.limit, column.precision, column.scale, column.default, column.null) - assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, column_def.to_sql + assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, @viz.accept(column_def) end if current_adapter?(:MysqlAdapter) diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb index adbe51f430..2a6d8cc2ab 100644 --- a/activerecord/test/cases/column_test.rb +++ b/activerecord/test/cases/column_test.rb @@ -6,6 +6,9 @@ module ActiveRecord class ColumnTest < ActiveRecord::TestCase def test_type_cast_boolean column = Column.new("field", nil, "boolean") + assert column.type_cast('').nil? + assert column.type_cast(nil).nil? + assert column.type_cast(true) assert column.type_cast(1) assert column.type_cast('1') @@ -15,15 +18,21 @@ module ActiveRecord assert column.type_cast('TRUE') assert column.type_cast('on') assert column.type_cast('ON') - assert !column.type_cast(false) - assert !column.type_cast(0) - assert !column.type_cast('0') - assert !column.type_cast('f') - assert !column.type_cast('F') - assert !column.type_cast('false') - assert !column.type_cast('FALSE') - assert !column.type_cast('off') - assert !column.type_cast('OFF') + + # explicitly check for false vs nil + assert_equal false, column.type_cast(false) + assert_equal false, column.type_cast(0) + assert_equal false, column.type_cast('0') + assert_equal false, column.type_cast('f') + assert_equal false, column.type_cast('F') + assert_equal false, column.type_cast('false') + assert_equal false, column.type_cast('FALSE') + assert_equal false, column.type_cast('off') + assert_equal false, column.type_cast('OFF') + assert_equal false, column.type_cast(' ') + assert_equal false, column.type_cast("\u3000\r\n") + assert_equal false, column.type_cast("\u0000") + assert_equal false, column.type_cast('SOMETHING RANDOM') end def test_type_cast_integer @@ -65,8 +74,9 @@ module ActiveRecord def test_type_cast_time column = Column.new("field", nil, "time") + assert_equal nil, column.type_cast(nil) assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast(' ') + assert_equal nil, column.type_cast('ABC') time_string = Time.now.utc.strftime("%T") assert_equal time_string, column.type_cast(time_string).strftime("%T") @@ -74,8 +84,10 @@ module ActiveRecord 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(nil) assert_equal nil, column.type_cast('') assert_equal nil, column.type_cast(' ') + assert_equal nil, column.type_cast('ABC') datetime_string = Time.now.utc.strftime("%FT%T") assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T") @@ -84,8 +96,10 @@ module ActiveRecord def test_type_cast_date column = Column.new("field", nil, "date") + assert_equal nil, column.type_cast(nil) assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast(' ') + assert_equal nil, column.type_cast(' ') + assert_equal nil, column.type_cast('ABC') date_string = Time.now.utc.strftime("%F") assert_equal date_string, column.type_cast(date_string).strftime("%F") @@ -96,6 +110,14 @@ module ActiveRecord assert_equal 1800, column.type_cast(30.minutes) assert_equal 7200, column.type_cast(2.hours) end + + def test_string_to_time_with_timezone + [:utc, :local].each do |zone| + with_timezone_config default: zone do + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), Column.string_to_time("Wed, 04 Sep 2013 03:00:00 EAT") + end + end + 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 1fd64dd0af..eb2fe5639b 100644 --- a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb +++ b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb @@ -2,6 +2,15 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters + class ConnectionPool + def insert_connection_for_test!(c) + synchronize do + @connections << c + @available.add c + end + end + end + class AbstractAdapterTest < ActiveRecord::TestCase attr_reader :adapter diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 541e983758..ecad7c942f 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -9,49 +9,46 @@ module ActiveRecord end def test_primary_key - assert_equal 'id', @cache.primary_keys['posts'] + assert_equal 'id', @cache.primary_keys('posts') end def test_primary_key_for_non_existent_table - assert_nil @cache.primary_keys['omgponies'] + assert_nil @cache.primary_keys('omgponies') end def test_caches_columns - columns = @cache.columns['posts'] - assert_equal columns, @cache.columns['posts'] + columns = @cache.columns('posts') + assert_equal columns, @cache.columns('posts') end def test_caches_columns_hash - columns_hash = @cache.columns_hash['posts'] - assert_equal columns_hash, @cache.columns_hash['posts'] + columns_hash = @cache.columns_hash('posts') + assert_equal columns_hash, @cache.columns_hash('posts') end def test_clearing - @cache.columns['posts'] - @cache.columns_hash['posts'] - @cache.tables['posts'] - @cache.primary_keys['posts'] + @cache.columns('posts') + @cache.columns_hash('posts') + @cache.tables('posts') + @cache.primary_keys('posts') @cache.clear! - assert_equal 0, @cache.columns.size - assert_equal 0, @cache.columns_hash.size - assert_equal 0, @cache.tables.size - assert_equal 0, @cache.primary_keys.size + assert_equal 0, @cache.size end def test_dump_and_load - @cache.columns['posts'] - @cache.columns_hash['posts'] - @cache.tables['posts'] - @cache.primary_keys['posts'] + @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'] + 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 diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index fe1b40d884..df17732fff 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -80,9 +80,9 @@ module ActiveRecord end def test_connections_closed_if_exception - app = Class.new(App) { def call(env); raise; end }.new + app = Class.new(App) { def call(env); raise NotImplementedError; end }.new explosive = ConnectionManagement.new(app) - assert_raises(RuntimeError) { explosive.call(@env) } + assert_raises(NotImplementedError) { explosive.call(@env) } assert !ActiveRecord::Base.connection_handler.active_connections? end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 23e64bee7e..2da51ea015 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -118,6 +118,7 @@ module ActiveRecord connection = cs.first @pool.remove connection assert_respond_to t.join.value, :execute + connection.close end def test_reap_and_active @@ -185,7 +186,7 @@ module ActiveRecord assert_not_nil connection threads = [] 4.times do |i| - threads << Thread.new(i) do |pool_count| + threads << Thread.new(i) do connection = pool.connection assert_not_nil connection connection.close @@ -329,7 +330,7 @@ module ActiveRecord end # make sure exceptions are thrown when establish_connection - # is called with a anonymous class + # is called with an anonymous class def test_anonymous_class_exception anonymous = Class.new(ActiveRecord::Base) handler = ActiveRecord::Base.connection_handler diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb new file mode 100644 index 0000000000..2a52bf574c --- /dev/null +++ b/activerecord/test/cases/core_test.rb @@ -0,0 +1,33 @@ +require 'cases/helper' +require 'models/person' +require 'models/topic' + +class NonExistentTable < ActiveRecord::Base; end + +class CoreTest < ActiveRecord::TestCase + fixtures :topics + + def test_inspect_class + assert_equal 'ActiveRecord::Base', ActiveRecord::Base.inspect + assert_equal 'LoosePerson(abstract)', LoosePerson.inspect + assert_match(/^Topic\(id: integer, title: string/, Topic.inspect) + end + + def test_inspect_instance + topic = topics(:first) + assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", important: nil, approved: false, replies_count: 1, unique_replies_count: 0, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_s(:db)}", updated_at: "#{topic.updated_at.to_s(:db)}">), topic.inspect + end + + def test_inspect_new_instance + assert_match(/Topic id: nil/, Topic.new.inspect) + end + + def test_inspect_limited_select_instance + 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 + assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect + end +end diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index fc46a249c8..ee3d8a81c2 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -51,6 +51,13 @@ class CounterCacheTest < ActiveRecord::TestCase end end + test 'reset multiple counters' do + Topic.update_counters @topic.id, replies_count: 1, unique_replies_count: 1 + assert_difference ['@topic.reload.replies_count', '@topic.reload.unique_replies_count'], -1 do + Topic.reset_counters(@topic.id, :replies, :unique_replies) + end + end + test "reset counters with string argument" do Topic.increment_counter('replies_count', @topic.id) @@ -115,10 +122,25 @@ class CounterCacheTest < ActiveRecord::TestCase end end + test 'update multiple counters' do + assert_difference ['@topic.reload.replies_count', '@topic.reload.unique_replies_count'], 2 do + Topic.update_counters @topic.id, replies_count: 2, unique_replies_count: 2 + end + end + + test "update other counters on parent destroy" do + david, joanna = dog_lovers(:david, :joanna) + joanna = joanna # squelch a warning + + assert_difference 'joanna.reload.dogs_count', -1 do + david.destroy + 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) + Person.reset_counters(michael.id, :friends_too) end end @@ -131,4 +153,11 @@ class CounterCacheTest < ActiveRecord::TestCase Subscriber.reset_counters(subscriber.id, 'books') end end + + test "the passed symbol needs to be an association name" do + e = assert_raises(ArgumentError) do + Topic.reset_counters(@topic.id, :replies_count) + end + assert_equal "'Topic' has no association called 'replies_count'", e.message + end end diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index 427076bd80..c0491bbee5 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -5,7 +5,7 @@ require 'models/task' class DateTimeTest < ActiveRecord::TestCase def test_saves_both_date_and_time with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do time_values = [1807, 2, 10, 15, 30, 45] # create DateTime value with local time zone offset local_offset = Rational(Time.local(*time_values).utc_offset, 86400) diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index e0cf4adf13..7e3d91e08c 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -39,6 +39,31 @@ class DefaultTest < ActiveRecord::TestCase end end +class DefaultStringsTest < ActiveRecord::TestCase + class DefaultString < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :default_strings do |t| + t.string :string_col, default: "Smith" + t.string :string_col_with_quotes, default: "O'Connor" + end + DefaultString.reset_column_information + end + + def test_default_strings + assert_equal "Smith", DefaultString.new.string_col + end + + def test_default_strings_containing_single_quotes + assert_equal "O'Connor", DefaultString.new.string_col_with_quotes + end + + teardown do + @connection.drop_table :default_strings + end +end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase # ActiveRecord::Base#create! (and #save and other related methods) will diff --git a/activerecord/test/cases/deprecated_dynamic_methods_test.rb b/activerecord/test/cases/deprecated_dynamic_methods_test.rb deleted file mode 100644 index 8e842d8758..0000000000 --- a/activerecord/test/cases/deprecated_dynamic_methods_test.rb +++ /dev/null @@ -1,592 +0,0 @@ -# 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 behavior 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_set_attribute - 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 - 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_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_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_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_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 @test_klass.methods.grep(/scoped_by_type/).blank? - @test_klass.scoped_by_type(nil) - assert @test_klass.methods.grep(/scoped_by_type/).present? - 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/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 7b2034dadf..9d7f57bf85 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -125,30 +125,30 @@ class DirtyTest < ActiveRecord::TestCase end def test_time_attributes_changes_without_time_zone - target = Class.new(ActiveRecord::Base) - target.table_name = 'pirates' - - target.time_zone_aware_attributes = false + with_timezone_config aware_attributes: false do + target = Class.new(ActiveRecord::Base) + target.table_name = 'pirates' - # New record - no changes. - pirate = target.new - assert !pirate.created_on_changed? - assert_nil pirate.created_on_change + # New record - no changes. + pirate = target.new + assert !pirate.created_on_changed? + assert_nil pirate.created_on_change - # Saved - no changes. - pirate.catchphrase = 'arrrr, time zone!!' - pirate.save! - assert !pirate.created_on_changed? - assert_nil pirate.created_on_change + # Saved - no changes. + pirate.catchphrase = 'arrrr, time zone!!' + pirate.save! + assert !pirate.created_on_changed? + assert_nil pirate.created_on_change - # Change created_on. - old_created_on = pirate.created_on - pirate.created_on = Time.now + 1.day - assert pirate.created_on_changed? - # kind_of does not work because - # ActiveSupport::TimeWithZone.name == 'Time' - assert_instance_of Time, pirate.created_on_was - assert_equal old_created_on, pirate.created_on_was + # Change created_on. + old_created_on = pirate.created_on + pirate.created_on = Time.now + 1.day + assert pirate.created_on_changed? + # kind_of does not work because + # ActiveSupport::TimeWithZone.name == 'Time' + assert_instance_of Time, pirate.created_on_was + assert_equal old_created_on, pirate.created_on_was + end end @@ -213,9 +213,11 @@ class DirtyTest < ActiveRecord::TestCase topic = target.create assert_nil topic.written_on - topic.written_on = "" - assert_nil topic.written_on - assert !topic.written_on_changed? + ["", nil].each do |value| + topic.written_on = value + assert_nil topic.written_on + assert !topic.written_on_changed? + end end end @@ -606,20 +608,6 @@ class DirtyTest < ActiveRecord::TestCase end end - test "partial_updates config attribute is deprecated" do - klass = Class.new(ActiveRecord::Base) - - assert klass.partial_writes? - assert_deprecated { assert klass.partial_updates? } - assert_deprecated { assert klass.partial_updates } - - assert_deprecated { klass.partial_updates = false } - - assert !klass.partial_writes? - assert_deprecated { assert !klass.partial_updates? } - assert_deprecated { assert !klass.partial_updates } - end - private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb new file mode 100644 index 0000000000..1fecfd077e --- /dev/null +++ b/activerecord/test/cases/disconnected_test.rb @@ -0,0 +1,27 @@ +require "cases/helper" + +class TestRecord < ActiveRecord::Base +end + +class TestDisconnectedAdapter < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + def setup + skip "in-memory database mustn't disconnect" if in_memory_db? + @connection = ActiveRecord::Base.connection + end + + def teardown + return if in_memory_db? + spec = ActiveRecord::Base.connection_config + ActiveRecord::Base.establish_connection(spec) + end + + test "can't execute statements while disconnected" do + @connection.execute "SELECT count(*) from products" + @connection.disconnect! + assert_raises(ActiveRecord::StatementInvalid) do + @connection.execute "SELECT count(*) from products" + end + end +end diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index fe105b9d22..1e6ccecfab 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -110,7 +110,7 @@ module ActiveRecord def test_dup_validity_is_independent repair_validations(Topic) do Topic.validates_presence_of :title - topic = Topic.new("title" => "Litterature") + topic = Topic.new("title" => "Literature") topic.valid? duped = topic.dup @@ -126,9 +126,9 @@ module ActiveRecord def test_dup_with_default_scope prev_default_scopes = Topic.default_scopes - Topic.default_scopes = [Topic.where(:approved => true)] + Topic.default_scopes = [proc { Topic.where(:approved => true) }] topic = Topic.new(:approved => false) - assert !topic.dup.approved?, "should not be overriden by default scopes" + assert !topic.dup.approved?, "should not be overridden by default scopes" ensure Topic.default_scopes = prev_default_scopes end diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index b425967678..b00e2744b9 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -1,55 +1,59 @@ require 'cases/helper' +require 'active_record/explain_subscriber' +require 'active_record/explain_registry' if ActiveRecord::Base.connection.supports_explain? class ExplainSubscriberTest < ActiveRecord::TestCase SUBSCRIBER = ActiveRecord::ExplainSubscriber.new - def test_collects_nothing_if_available_queries_for_explain_is_nil - with_queries(nil) do - SUBSCRIBER.finish(nil, nil, {}) - assert_nil Thread.current[:available_queries_for_explain] - end + def setup + ActiveRecord::ExplainRegistry.reset + ActiveRecord::ExplainRegistry.collect = true end def test_collects_nothing_if_the_payload_has_an_exception - with_queries([]) do |queries| - SUBSCRIBER.finish(nil, nil, :exception => Exception.new) - assert queries.empty? - end + SUBSCRIBER.finish(nil, nil, exception: Exception.new) + assert queries.empty? end def test_collects_nothing_for_ignored_payloads - with_queries([]) do |queries| - ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip| - SUBSCRIBER.finish(nil, nil, :name => ip) - end - assert queries.empty? + ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip| + SUBSCRIBER.finish(nil, nil, name: ip) end + assert queries.empty? + end + + def test_collects_nothing_if_collect_is_false + ActiveRecord::ExplainRegistry.collect = false + SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'select 1 from users', binds: [1, 2]) + assert queries.empty? end def test_collects_pairs_of_queries_and_binds sql = 'select 1 from users' binds = [1, 2] - with_queries([]) do |queries| - 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] - end + 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] end - def test_collects_nothing_if_unexplained_sqls - with_queries([]) do |queries| - SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => 'SHOW max_identifier_length') - assert queries.empty? - end + def test_collects_nothing_if_the_statement_is_not_whitelisted + SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'SHOW max_identifier_length') + assert queries.empty? + end + + def test_collects_nothing_if_the_statement_is_only_partially_matched + SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'select_db yo_mama') + assert queries.empty? + end + + def teardown + ActiveRecord::ExplainRegistry.reset end - def with_queries(queries) - Thread.current[:available_queries_for_explain] = queries - yield queries - ensure - Thread.current[:available_queries_for_explain] = nil + def queries + ActiveRecord::ExplainRegistry.queries end end end diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 9440cd429a..3ff22f222f 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -21,14 +21,9 @@ class FinderRespondToTest < ActiveRecord::TestCase assert_respond_to Topic, :find_by_title end - def test_should_respond_to_find_all_by_one_attribute - ensure_topic_method_is_not_cached(:find_all_by_title) - assert_respond_to Topic, :find_all_by_title - end - - def test_should_respond_to_find_all_by_two_attributes - ensure_topic_method_is_not_cached(:find_all_by_title_and_author_name) - assert_respond_to Topic, :find_all_by_title_and_author_name + def test_should_respond_to_find_by_with_bang + ensure_topic_method_is_not_cached(:find_by_title!) + assert_respond_to Topic, :find_by_title! end def test_should_respond_to_find_by_two_attributes @@ -41,36 +36,6 @@ class FinderRespondToTest < ActiveRecord::TestCase 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 - end - - def test_should_respond_to_find_or_initialize_from_two_attributes - ensure_topic_method_is_not_cached(:find_or_initialize_by_title_and_author_name) - assert_respond_to Topic, :find_or_initialize_by_title_and_author_name - end - - def test_should_respond_to_find_or_create_from_one_attribute - 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 - 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_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 diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index a9fa107749..4188b32731 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -11,6 +11,7 @@ require 'models/project' require 'models/developer' require 'models/customer' require 'models/toy' +require 'models/matey' class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations @@ -31,6 +32,13 @@ class FinderTest < ActiveRecord::TestCase assert_equal(topics(:first).title, Topic.find(1).title) end + def test_symbols_table_ref + Post.first # warm up + x = Symbol.all_symbols.count + Post.where("title" => {"xxxqqqq" => "bar"}) + assert_equal x, Symbol.all_symbols.count + end + # find should handle strings that come from URLs # (example: Category.find(params[:id])) def test_find_with_string @@ -38,16 +46,19 @@ class FinderTest < ActiveRecord::TestCase end def test_exists - assert Topic.exists?(1) - assert Topic.exists?("1") - assert Topic.exists?(:author_name => "David") - assert Topic.exists?(:author_name => "Mary", :approved => true) - assert Topic.exists?(["parent_id = ?", 1]) - assert !Topic.exists?(45) - assert !Topic.exists?(Topic.new) + assert_equal true, Topic.exists?(1) + assert_equal true, Topic.exists?("1") + assert_equal true, Topic.exists?(title: "The First Topic") + assert_equal true, Topic.exists?(heading: "The First Topic") + assert_equal true, Topic.exists?(:author_name => "Mary", :approved => true) + assert_equal true, Topic.exists?(["parent_id = ?", 1]) + assert_equal true, Topic.exists?(id: [1, 9999]) + + assert_equal false, Topic.exists?(45) + assert_equal false, Topic.exists?(Topic.new) begin - assert !Topic.exists?("foo") + assert_equal false, Topic.exists?("foo") rescue ActiveRecord::StatementInvalid # PostgreSQL complains about string comparison with integer field rescue Exception @@ -64,49 +75,62 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_returns_true_with_one_record_and_no_args - assert Topic.exists? + assert_equal true, Topic.exists? end def test_exists_returns_false_with_false_arg - assert !Topic.exists?(false) + assert_equal false, 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 - assert !Topic.exists?(nil) - assert Topic.exists? - assert !Topic.first.replies.exists?(nil) - assert Topic.first.replies.exists? + assert_equal false, Topic.exists?(nil) + assert_equal true, Topic.exists? + + assert_equal false, Topic.first.replies.exists?(nil) + assert_equal true, Topic.first.replies.exists? end # ensures +exists?+ runs valid SQL by excluding order value def test_exists_with_order - assert Topic.order(:id).uniq.exists? + assert_equal true, Topic.order(:id).distinct.exists? end 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? + assert_equal false, Topic.includes(:replies).limit(0).exists? + assert_equal false, Topic.includes(:replies).limit(1).where('0 = 1').exists? + end + + def test_exists_with_distinct_association_includes_and_limit + author = Author.first + assert_equal false, author.unique_categorized_posts.includes(:special_comments).limit(0).exists? + assert_equal true, author.unique_categorized_posts.includes(:special_comments).limit(1).exists? + end + + def test_exists_with_distinct_association_includes_limit_and_order + author = Author.first + assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists? + assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists? end def test_exists_with_empty_table_and_no_args_given Topic.delete_all - assert !Topic.exists? + assert_equal false, Topic.exists? end def test_exists_with_aggregate_having_three_mappings existing_address = customers(:david).address - assert Customer.exists?(:address => existing_address) + assert_equal true, Customer.exists?(:address => existing_address) end def test_exists_with_aggregate_having_three_mappings_with_one_difference existing_address = customers(:david).address - assert !Customer.exists?(:address => + assert_equal false, Customer.exists?(:address => Address.new(existing_address.street, existing_address.city, existing_address.country + "1")) - assert !Customer.exists?(:address => + assert_equal false, Customer.exists?(:address => Address.new(existing_address.street, existing_address.city + "1", existing_address.country)) - assert !Customer.exists?(:address => + assert_equal false, Customer.exists?(:address => Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) end @@ -455,7 +479,7 @@ class FinderTest < ActiveRecord::TestCase def test_condition_utc_time_interpolation_with_default_timezone_local with_env_tz 'America/New_York' do - with_active_record_default_timezone :local do + with_timezone_config default: :local do topic = Topic.first assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getutc]).first end @@ -464,7 +488,7 @@ class FinderTest < ActiveRecord::TestCase def test_hash_condition_utc_time_interpolation_with_default_timezone_local with_env_tz 'America/New_York' do - with_active_record_default_timezone :local do + with_timezone_config default: :local do topic = Topic.first assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getutc}).first end @@ -473,7 +497,7 @@ class FinderTest < ActiveRecord::TestCase def test_condition_local_time_interpolation_with_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do topic = Topic.first assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getlocal]).first end @@ -482,7 +506,7 @@ class FinderTest < ActiveRecord::TestCase def test_hash_condition_local_time_interpolation_with_default_timezone_utc with_env_tz 'America/New_York' do - with_active_record_default_timezone :utc do + with_timezone_config default: :utc do topic = Topic.first assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getlocal}).first end @@ -593,7 +617,7 @@ class FinderTest < ActiveRecord::TestCase def test_named_bind_with_postgresql_type_casts l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') } assert_nothing_raised(&l) - assert_equal "#{ActiveRecord::Base.quote_value('10')}::integer '2009-01-01'::date", l.call + assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call end def test_string_sanitation @@ -833,6 +857,14 @@ class FinderTest < ActiveRecord::TestCase rescue ActiveRecord::RecordNotFound => e assert_equal 'Couldn\'t find Toy with name=Hello World!', e.message end + ensure + Toy.reset_primary_key + end + + def test_find_without_primary_key + assert_raises(ActiveRecord::UnknownPrimaryKey) do + Matey.find(1) + end end def test_finder_with_offset_string @@ -854,11 +886,4 @@ class FinderTest < ActiveRecord::TestCase ensure old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') end - - def with_active_record_default_timezone(zone) - old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone - yield - ensure - ActiveRecord::Base.default_timezone = old_zone - end end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 8ad40ec3f4..bffff07089 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -84,6 +84,12 @@ class FixturesTest < ActiveRecord::TestCase assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}" end + def test_create_symbol_fixtures_is_deprecated + assert_deprecated do + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => 'Course') { Course.connection } + end + end + def test_attributes topics = create_fixtures("topics").first assert_equal("The First Topic", topics["first"]["title"]) @@ -190,11 +196,11 @@ class FixturesTest < ActiveRecord::TestCase end def test_empty_yaml_fixture - assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/yml/accounts") + assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") end def test_empty_yaml_fixture_with_a_comment_in_it - assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', FIXTURES_ROOT + "/naked/yml/companies") + assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") end def test_nonexistent_fixture_file @@ -204,19 +210,19 @@ class FixturesTest < ActiveRecord::TestCase assert Dir[nonexistent_fixture_path+"*"].empty? assert_raise(Errno::ENOENT) do - ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', nonexistent_fixture_path) + ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, nonexistent_fixture_path) end end def test_dirty_dirty_yaml_file assert_raise(ActiveRecord::Fixture::FormatError) do - ActiveRecord::FixtureSet.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses") + ActiveRecord::FixtureSet.new( Account.connection, "courses", Course, FIXTURES_ROOT + "/naked/yml/courses") end end def test_omap_fixtures assert_nothing_raised do - fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', 'Category', FIXTURES_ROOT + "/categories_ordered") + fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered") fixtures.each.with_index do |(name, fixture), i| assert_equal "fixture_no_#{i}", name @@ -245,6 +251,60 @@ class FixturesTest < ActiveRecord::TestCase def test_serialized_fixtures assert_equal ["Green", "Red", "Orange"], traffic_lights(:uk).state end + + def test_fixtures_are_set_up_with_database_env_variable + db_url_tmp = ENV['DATABASE_URL'] + ENV['DATABASE_URL'] = "sqlite3:///:memory:" + ActiveRecord::Base.stubs(:configurations).returns({}) + test_case = Class.new(ActiveRecord::TestCase) do + fixtures :accounts + + def test_fixtures + assert accounts(:signals37) + end + end + + result = test_case.new(:test_fixtures).run + + assert result.passed?, "Expected #{result.name} to pass:\n#{result}" + ensure + ENV['DATABASE_URL'] = db_url_tmp + end +end + +class HasManyThroughFixture < ActiveSupport::TestCase + def make_model(name) + Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } + end + + def test_has_many_through + pt = make_model "ParrotTreasure" + parrot = make_model "Parrot" + treasure = make_model "Treasure" + + pt.table_name = "parrots_treasures" + pt.belongs_to :parrot, :class => parrot + pt.belongs_to :treasure, :class => treasure + + parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :treasures, :through => :parrot_treasures + + parrots = File.join FIXTURES_ROOT, 'parrots' + + fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + rows = fs.table_rows + assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrots_treasures'] + end + + def load_has_and_belongs_to_many + parrot = make_model "Parrot" + parrot.has_and_belongs_to_many :treasures + + parrots = File.join FIXTURES_ROOT, 'parrots' + + fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + fs.table_rows + end end if Account.connection.respond_to?(:reset_pk_sequence!) @@ -433,7 +493,7 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase end class CheckSetTableNameFixturesTest < ActiveRecord::TestCase - set_fixture_class :funny_jokes => 'Joke' + set_fixture_class :funny_jokes => Joke fixtures :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class @@ -477,10 +537,6 @@ class CustomConnectionFixturesTest < ActiveRecord::TestCase fixtures :courses self.use_transactional_fixtures = false - def test_connection_instance_method_deprecation - assert_deprecated { courses(:ruby).connection } - end - def test_leaky_destroy assert_nothing_raised { courses(:ruby) } courses(:ruby).destroy @@ -520,7 +576,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase end class CheckEscapedYamlFixturesTest < ActiveRecord::TestCase - set_fixture_class :funny_jokes => 'Joke' + set_fixture_class :funny_jokes => Joke fixtures :funny_jokes # Set to false to blow away fixtures cache and ensure our fixtures are loaded # and thus takes into account our set_fixture_class @@ -562,7 +618,7 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase end private - def load_fixtures + def load_fixtures(config) raise 'argh' end end @@ -572,7 +628,16 @@ class LoadAllFixturesTest < ActiveRecord::TestCase fixtures :all def test_all_there - assert_equal %w(developers people tasks), fixture_table_names.sort + assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort + end +end + +class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase + self.fixture_path = Pathname.new(FIXTURES_ROOT).join('all') + fixtures :all + + def test_all_there + assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort end end diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index 490b599fb6..981a75faf6 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -61,4 +61,9 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase assert_equal 'Guille', person.first_name assert_equal 'm', person.gender end + + def test_blank_attributes_should_not_raise + person = Person.new + assert_nil person.assign_attributes(ProtectedParams.new({})) + end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 7dbb6616f8..34e8f1be0f 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -49,11 +49,58 @@ ensure old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') end -def with_active_record_default_timezone(zone) - old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone +def with_timezone_config(cfg) + verify_default_timezone_config + + old_default_zone = ActiveRecord::Base.default_timezone + old_awareness = ActiveRecord::Base.time_zone_aware_attributes + old_zone = Time.zone + + if cfg.has_key?(:default) + ActiveRecord::Base.default_timezone = cfg[:default] + end + if cfg.has_key?(:aware_attributes) + ActiveRecord::Base.time_zone_aware_attributes = cfg[:aware_attributes] + end + if cfg.has_key?(:zone) + Time.zone = cfg[:zone] + end yield ensure - ActiveRecord::Base.default_timezone = old_zone + ActiveRecord::Base.default_timezone = old_default_zone + ActiveRecord::Base.time_zone_aware_attributes = old_awareness + Time.zone = old_zone +end + +# This method makes sure that tests don't leak global state related to time zones. +EXPECTED_ZONE = nil +EXPECTED_DEFAULT_TIMEZONE = :utc +EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false +def verify_default_timezone_config + if Time.zone != EXPECTED_ZONE + $stderr.puts <<-MSG +\n#{self.to_s} + Global state `Time.zone` was leaked. + Expected: #{EXPECTED_ZONE} + Got: #{Time.zone} + MSG + end + if ActiveRecord::Base.default_timezone != EXPECTED_DEFAULT_TIMEZONE + $stderr.puts <<-MSG +\n#{self.to_s} + Global state `ActiveRecord::Base.default_timezone` was leaked. + Expected: #{EXPECTED_DEFAULT_TIMEZONE} + Got: #{ActiveRecord::Base.default_timezone} + MSG + end + if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES + $stderr.puts <<-MSG +\n#{self.to_s} + Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked. + Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES} + Got: #{ActiveRecord::Base.time_zone_aware_attributes} + MSG + end end unless ENV['FIXTURE_DEBUG'] @@ -93,7 +140,7 @@ def load_schema load SCHEMA_ROOT + "/schema.rb" - if File.exists?(adapter_specific_schema_file) + if File.exist?(adapter_specific_schema_file) load adapter_specific_schema_file end ensure @@ -119,21 +166,24 @@ class << Time 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 +class SQLSubscriber + attr_reader :logged + attr_reader :payloads + + def initialize + @logged = [] + @payloads = [] end + + def start(name, id, payload) + @payloads << payload + @logged << [payload[:sql], payload[:name], payload[:binds]] + end + + def finish(name, id, payload); end end + module InTimeZone private diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index b91146db4e..73cf99a5d7 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -68,6 +68,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_company_descends_from_active_record + assert !ActiveRecord::Base.descends_from_active_record? assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base' assert Company.descends_from_active_record?, 'Company should descend from ActiveRecord::Base' assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base' @@ -171,6 +172,20 @@ class InheritanceTest < ActiveRecord::TestCase assert_equal Firm, firm.class end + def test_new_with_abstract_class + e = assert_raises(NotImplementedError) do + AbstractCompany.new + end + assert_equal("AbstractCompany is an abstract class and can not be instantiated.", e.message) + end + + def test_new_with_ar_base + e = assert_raises(NotImplementedError) do + ActiveRecord::Base.new + end + assert_equal("ActiveRecord::Base is an abstract class and can not be instantiated.", e.message) + end + def test_new_with_invalid_type assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'InvalidType') } end @@ -179,6 +194,10 @@ class InheritanceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'Account') } end + def test_new_with_complex_inheritance + assert_nothing_raised { Client.new(type: 'VerySpecialClient') } + end + def test_new_with_autoload_paths path = File.expand_path('../../models/autoloadable', __FILE__) ActiveSupport::Dependencies.autoload_paths << path @@ -294,8 +313,12 @@ 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 -end + def test_scope_inherited_properly + assert_nothing_raised { Company.of_first_firm } + assert_nothing_raised { Client.of_first_firm } + end +end class InheritanceComputeTypeTest < ActiveRecord::TestCase fixtures :companies diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb new file mode 100644 index 0000000000..406aacb056 --- /dev/null +++ b/activerecord/test/cases/integration_test.rb @@ -0,0 +1,84 @@ +require 'cases/helper' +require 'models/company' +require 'models/developer' +require 'models/car' +require 'models/bulb' + +class IntegrationTest < ActiveRecord::TestCase + fixtures :companies, :developers + + def test_to_param_should_return_string + assert_kind_of String, Client.first.to_param + end + + def test_to_param_returns_nil_if_not_persisted + client = Client.new + assert_equal nil, client.to_param + end + + def test_to_param_returns_id_if_not_persisted_but_id_is_set + client = Client.new + client.id = 1 + assert_equal '1', client.to_param + end + + def test_cache_key_for_existing_record_is_not_timezone_dependent + utc_key = Developer.first.cache_key + + with_timezone_config zone: "EST" do + est_key = Developer.first.cache_key + assert_equal utc_key, est_key + end + 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(:nsec)}", dev.cache_key + end + + def test_cache_key_format_for_existing_record_with_updated_at_and_custom_cache_timestamp_format + dev = CachedDeveloper.first + assert_equal "cached_developers/#{dev.id}-#{dev.updated_at.utc.to_s(:number)}", dev.cache_key + end + + def test_cache_key_changes_when_child_touched + car = Car.create + Bulb.create(car: car) + + key = car.cache_key + car.bulb.touch + car.reload + assert_not_equal key, car.cache_key + end + + def test_cache_key_format_for_existing_record_with_nil_updated_timestamps + dev = Developer.first + dev.update_columns(updated_at: nil, updated_on: nil) + assert_match(/\/#{dev.id}$/, dev.cache_key) + end + + def test_cache_key_for_updated_on + dev = Developer.first + dev.updated_at = nil + assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key + end + + def test_cache_key_for_newer_updated_at + dev = Developer.first + dev.updated_at += 3600 + assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key + end + + def test_cache_key_for_newer_updated_on + dev = Developer.first + dev.updated_on += 3600 + assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key + end + + def test_cache_key_format_is_precise_enough + dev = Developer.first + key = dev.cache_key + dev.touch + assert_not_equal key, dev.cache_key + end +end diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb new file mode 100644 index 0000000000..f2d8f18ec7 --- /dev/null +++ b/activerecord/test/cases/invalid_connection_test.rb @@ -0,0 +1,22 @@ +require "cases/helper" + +class TestAdapterWithInvalidConnection < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Bird < ActiveRecord::Base + end + + def setup + # Can't just use current adapter; sqlite3 will create a database + # file on the fly. + Bird.establish_connection adapter: 'mysql', database: 'i_do_not_exist' + end + + def teardown + Bird.remove_connection + end + + test "inspect on Model class does not raise" do + assert_equal "#{Bird.name}(no database connection)", Bird.inspect + end +end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index be59ffc4ab..428145d00b 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -58,6 +58,24 @@ module ActiveRecord end end + class RemoveIndexMigration1 < SilentMigration + def self.up + create_table("horses") do |t| + t.column :name, :string + t.column :color, :string + t.index [:name, :color] + end + end + end + + class RemoveIndexMigration2 < SilentMigration + def change + change_table("horses") do |t| + t.remove_index [:name, :color] + end + end + end + class LegacyMigration < ActiveRecord::Migration def self.up create_table("horses") do |t| @@ -104,6 +122,16 @@ module ActiveRecord end end + def test_exception_on_removing_index_without_column_option + RemoveIndexMigration1.new.migrate(:up) + migration = RemoveIndexMigration2.new + migration.migrate(:up) + + assert_raises(IrreversibleMigration) do + migration.migrate(:down) + end + end + def test_migrate_up migration = InvertibleMigration.new migration.migrate(:up) diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index a0a3e6cb0d..a16ed963fe 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -8,6 +8,7 @@ require 'models/legacy_thing' require 'models/reference' require 'models/string_key_object' require 'models/car' +require 'models/bulb' require 'models/engine' require 'models/wheel' require 'models/treasure' @@ -16,6 +17,7 @@ class LockWithoutDefault < ActiveRecord::Base; end class LockWithCustomColumnWithoutDefault < ActiveRecord::Base self.table_name = :lock_without_defaults_cust + self.column_defaults # to test @column_defaults caching. self.locking_column = :custom_lock_version end @@ -26,6 +28,18 @@ end class OptimisticLockingTest < ActiveRecord::TestCase fixtures :people, :legacy_things, :references, :string_key_objects, :peoples_treasures + def test_quote_value_passed_lock_col + p1 = Person.find(1) + assert_equal 0, p1.lock_version + + Person.expects(:quote_value).with(0, Person.columns_hash[Person.locking_column]).returns('0').once + + p1.first_name = 'anika2' + p1.save! + + assert_equal 1, p1.lock_version + end + def test_non_integer_lock_existing s1 = StringKeyObject.find("record1") s2 = StringKeyObject.find("record1") @@ -193,11 +207,19 @@ class OptimisticLockingTest < ActiveRecord::TestCase def test_lock_without_default_sets_version_to_zero t1 = LockWithoutDefault.new assert_equal 0, t1.lock_version + + t1.save + t1 = LockWithoutDefault.find(t1.id) + assert_equal 0, t1.lock_version end def test_lock_with_custom_column_without_default_sets_version_to_zero t1 = LockWithCustomColumnWithoutDefault.new assert_equal 0, t1.custom_lock_version + + t1.save + t1 = LockWithCustomColumnWithoutDefault.find(t1.id) + assert_equal 0, t1.custom_lock_version end def test_readonly_attributes @@ -234,7 +256,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase car = Car.create! assert_difference 'car.wheels.count' do - car.wheels << Wheel.create! + car.wheels << Wheel.create! end assert_difference 'car.wheels.count', -1 do car.destroy @@ -250,6 +272,10 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert p.treasures.empty? assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty? end + + def test_quoted_locking_column_is_deprecated + assert_deprecated { ActiveRecord::Base.quoted_locking_column } + end end class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase @@ -341,9 +367,6 @@ end # is so cumbersome. Will deadlock Ruby threads if the underlying db.execute # blocks, so separate script called by Kernel#system is needed. # (See exec vs. async_exec in the PostgreSQL adapter.) - -# TODO: The Sybase, and OpenBase adapters currently have no support for pessimistic locking - unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? class PessimisticLockingTest < ActiveRecord::TestCase self.use_transactional_fixtures = false diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 57eac0c175..3bdc5a1302 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -56,6 +56,13 @@ class LogSubscriberTest < ActiveRecord::TestCase assert_equal 2, logger.debugs.length end + def test_sql_statements_are_not_squeezed + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.sql(event.new(0, sql: 'ruby rails')) + assert_match(/ruby rails/, logger.debugs.first) + end + def test_ignore_binds_payload_with_nil_column event = Struct.new(:duration, :payload) diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index cad759bba9..e37dca856d 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -74,6 +74,35 @@ module ActiveRecord assert_equal "hello", five.default unless mysql end + def test_add_column_with_array + if current_adapter?(:PostgreSQLAdapter) + connection.create_table :testings + connection.add_column :testings, :foo, :string, :array => true + + columns = connection.columns(:testings) + array_column = columns.detect { |c| c.name == "foo" } + + assert array_column.array + else + skip "array option only supported in PostgreSQLAdapter" + end + end + + def test_create_table_with_array_column + if current_adapter?(:PostgreSQLAdapter) + connection.create_table :testings do |t| + t.string :foo, :array => true + end + + columns = connection.columns(:testings) + array_column = columns.detect { |c| c.name == "foo" } + + assert array_column.array + else + skip "array option only supported in PostgreSQLAdapter" + end + end + def test_create_table_with_limits connection.create_table :testings do |t| t.column :foo, :string, :limit => 255 @@ -235,7 +264,7 @@ module ActiveRecord end end - def test_keeping_default_and_notnull_constaint_on_change + def test_keeping_default_and_notnull_constraints_on_change connection.create_table :testings do |t| t.column :title, :string end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index ec2926632c..aa606ac8bb 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -16,32 +16,23 @@ module ActiveRecord end def test_add_remove_single_field_using_string_arguments - assert_not TestModel.column_methods_hash.key?(:last_name) + assert_no_column TestModel, :last_name add_column 'test_models', 'last_name', :string - - TestModel.reset_column_information - - assert TestModel.column_methods_hash.key?(:last_name) + assert_column TestModel, :last_name remove_column 'test_models', 'last_name' - - TestModel.reset_column_information - assert_not TestModel.column_methods_hash.key?(:last_name) + assert_no_column TestModel, :last_name end def test_add_remove_single_field_using_symbol_arguments - assert_not TestModel.column_methods_hash.key?(:last_name) + assert_no_column TestModel, :last_name add_column :test_models, :last_name, :string - - TestModel.reset_column_information - assert TestModel.column_methods_hash.key?(:last_name) + assert_column TestModel, :last_name remove_column :test_models, :last_name - - TestModel.reset_column_information - assert_not TestModel.column_methods_hash.key?(:last_name) + assert_no_column TestModel, :last_name end def test_unabstracted_database_dependent_types @@ -168,26 +159,6 @@ module ActiveRecord assert_equal Date, bob.favorite_day.class end - # Oracle adapter stores Time or DateTime with timezone value already in _before_type_cast column - # therefore no timezone change is done afterwards when default timezone is changed - unless current_adapter?(:OracleAdapter) - # Test DateTime column and defaults, including timezone. - # FIXME: moment of truth may be Time on 64-bit platforms. - if bob.moment_of_truth.is_a?(DateTime) - - with_env_tz 'US/Eastern' do - bob.reload - assert_equal DateTime.local_offset, bob.moment_of_truth.offset - assert_not_equal 0, bob.moment_of_truth.offset - 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) - assert_equal DateTime::ITALY, bob.moment_of_truth.start - end - end - end - assert_instance_of TrueClass, bob.male? assert_kind_of BigDecimal, bob.wealth end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index e52809f0f8..2d7a7ec73a 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -55,13 +55,20 @@ module ActiveRecord default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default assert_equal 70000, default_before - rename_column "test_models", "salary", "anual_salary" + rename_column "test_models", "salary", "annual_salary" - assert TestModel.column_names.include?("anual_salary") - default_after = connection.columns("test_models").find { |c| c.name == "anual_salary" }.default + assert TestModel.column_names.include?("annual_salary") + default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default assert_equal 70000, default_after end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + def test_mysql_rename_column_preserves_auto_increment + rename_column "test_models", "id", "id_test" + assert_equal "auto_increment", connection.columns("test_models").find { |c| c.name == "id_test" }.extra + end + end + def test_rename_nonexistent_column exception = if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) ActiveRecord::StatementInvalid diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 2cad8a6d96..1b205d372f 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -242,6 +242,16 @@ module ActiveRecord add = @recorder.inverse_of :remove_belongs_to, [:table, :user] assert_equal [:add_reference, [:table, :user], nil], add end + + def test_invert_enable_extension + disable = @recorder.inverse_of :enable_extension, ['uuid-ossp'] + assert_equal [:disable_extension, ['uuid-ossp'], nil], disable + end + + def test_invert_disable_extension + enable = @recorder.inverse_of :disable_extension, ['uuid-ossp'] + assert_equal [:enable_extension, ['uuid-ossp'], nil], enable + end end end end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 0e375af6e8..04521a5f5a 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -109,16 +109,6 @@ module ActiveRecord end end - def test_deprecated_type_argument - message = "Passing a string as third argument of `add_index` is deprecated and will" + - " be removed in Rails 4.1." + - " Use add_index(:testings, [:foo, :bar], unique: true) instead" - - assert_deprecated message do - connection.add_index :testings, [:foo, :bar], "UNIQUE" - end - end - def test_unique_index_exists connection.add_index :testings, :foo, :unique => true diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index f8afb7c591..acfde2a27a 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -177,20 +177,18 @@ class MigrationTest < ActiveRecord::TestCase end def test_filtering_migrations - assert !Person.column_methods_hash.include?(:last_name) + assert_no_column Person, :last_name assert !Reminder.table_exists? name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" } ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter) - Person.reset_column_information - assert Person.column_methods_hash.include?(:last_name) + assert_column Person, :last_name 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_no_column Person, :last_name assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } end @@ -237,7 +235,7 @@ class MigrationTest < ActiveRecord::TestCase skip "not supported on #{ActiveRecord::Base.connection.class}" end - assert_not Person.column_methods_hash.include?(:last_name) + assert_no_column Person, :last_name migration = Class.new(ActiveRecord::Migration) { def version; 100 end @@ -253,17 +251,41 @@ class MigrationTest < ActiveRecord::TestCase assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message - Person.reset_column_information - assert_not Person.column_methods_hash.include?(:last_name), + assert_no_column Person, :last_name, "On error, the Migrator should revert schema changes but it did not." end + def test_migrator_one_up_with_exception_and_rollback_using_run + unless ActiveRecord::Base.connection.supports_ddl_transactions? + skip "not supported on #{ActiveRecord::Base.connection.class}" + end + + assert_no_column Person, :last_name + + migration = Class.new(ActiveRecord::Migration) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string + raise 'Something broke' + end + }.new + + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + + e = assert_raise(StandardError) { migrator.run } + + assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message + + assert_no_column Person, :last_name, + "On error, the Migrator should revert schema changes but it did not." + end + def test_migration_without_transaction unless ActiveRecord::Base.connection.supports_ddl_transactions? skip "not supported on #{ActiveRecord::Base.connection.class}" end - assert_not Person.column_methods_hash.include?(:last_name) + assert_no_column Person, :last_name migration = Class.new(ActiveRecord::Migration) { self.disable_ddl_transaction! @@ -279,33 +301,49 @@ class MigrationTest < ActiveRecord::TestCase e = assert_raise(StandardError) { migrator.migrate } assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message - Person.reset_column_information - assert Person.column_methods_hash.include?(:last_name), + assert_column Person, :last_name, "without ddl transactions, the Migrator should not rollback on error but it did." ensure Person.reset_column_information - if Person.column_methods_hash.include?(:last_name) + if Person.column_names.include?('last_name') Person.connection.remove_column('people', 'last_name') end end def test_schema_migrations_table_name + original_schema_migrations_table_name = ActiveRecord::Migrator.schema_migrations_table_name + + assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" Reminder.reset_table_name assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name + ActiveRecord::Base.schema_migrations_table_name = "changed" + Reminder.reset_table_name + assert_equal "prefix_changed_suffix", ActiveRecord::Migrator.schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" Reminder.reset_table_name - assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name + assert_equal "changed", ActiveRecord::Migrator.schema_migrations_table_name + ensure + ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name + Reminder.reset_table_name end - def test_proper_table_name - assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') - assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) - assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder) + def test_proper_table_name_on_migrator + assert_deprecated do + assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') + end + assert_deprecated do + assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) + end + assert_deprecated do + assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder) + end Reminder.reset_table_name - assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder) + assert_deprecated do + assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder) + end # Use the model's own prefix/suffix if a model is given ActiveRecord::Base.table_name_prefix = "ARprefix_" @@ -313,7 +351,9 @@ class MigrationTest < ActiveRecord::TestCase Reminder.table_name_prefix = 'prefix_' Reminder.table_name_suffix = '_suffix' Reminder.reset_table_name - assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder) + assert_deprecated do + assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder) + end Reminder.table_name_prefix = '' Reminder.table_name_suffix = '' Reminder.reset_table_name @@ -322,8 +362,39 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" Reminder.reset_table_name - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') - assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table) + assert_deprecated do + assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') + end + assert_deprecated do + assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table) + end + end + + def test_proper_table_name_on_migration + migration = ActiveRecord::Migration.new + assert_equal "table", migration.proper_table_name('table') + assert_equal "table", migration.proper_table_name(:table) + assert_equal "reminders", migration.proper_table_name(Reminder) + Reminder.reset_table_name + assert_equal Reminder.table_name, migration.proper_table_name(Reminder) + + # Use the model's own prefix/suffix if a model is given + ActiveRecord::Base.table_name_prefix = "ARprefix_" + ActiveRecord::Base.table_name_suffix = "_ARsuffix" + Reminder.table_name_prefix = 'prefix_' + Reminder.table_name_suffix = '_suffix' + Reminder.reset_table_name + assert_equal "prefix_reminders_suffix", migration.proper_table_name(Reminder) + Reminder.table_name_prefix = '' + Reminder.table_name_suffix = '' + Reminder.reset_table_name + + # Use AR::Base's prefix/suffix if string or symbol is given + ActiveRecord::Base.table_name_prefix = "prefix_" + ActiveRecord::Base.table_name_suffix = "_suffix" + Reminder.reset_table_name + assert_equal "prefix_table_suffix", migration.proper_table_name('table', migration.table_name_options) + assert_equal "prefix_table_suffix", migration.proper_table_name(:table, migration.table_name_options) end def test_rename_table_with_prefix_and_suffix @@ -639,8 +710,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase @existing_migrations = Dir[@migrations_path + "/*.rb"] copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"}) - assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") assert_equal [@migrations_path + "/4_people_have_hobbies.bukkits.rb", @migrations_path + "/5_people_have_descriptions.bukkits.rb"], copied.map(&:filename) expected = "# This migration comes from bukkits (originally 1)" @@ -663,10 +734,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy" sources[:omg] = MIGRATIONS_ROOT + "/to_copy2" ActiveRecord::Migration.copy(@migrations_path, sources) - assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") - assert File.exists?(@migrations_path + "/6_create_articles.omg.rb") - assert File.exists?(@migrations_path + "/7_create_comments.omg.rb") + assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/6_create_articles.omg.rb") + assert File.exist?(@migrations_path + "/7_create_comments.omg.rb") files_count = Dir[@migrations_path + "/*.rb"].length ActiveRecord::Migration.copy(@migrations_path, sources) @@ -681,8 +752,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") expected = [@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb", @migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb"] assert_equal expected, copied.map(&:filename) @@ -706,10 +777,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, sources) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101012_create_articles.omg.rb") - assert File.exists?(@migrations_path + "/20100726101013_create_comments.omg.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101012_create_articles.omg.rb") + assert File.exist?(@migrations_path + "/20100726101013_create_comments.omg.rb") assert_equal 4, copied.length files_count = Dir[@migrations_path + "/*.rb"].length @@ -726,8 +797,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb") files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) @@ -744,7 +815,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase @existing_migrations = Dir[@migrations_path + "/*.rb"] copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"}) - assert File.exists?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb") + assert File.exist?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb") assert_equal [@migrations_path + "/4_currencies_have_symbols.bukkits.rb"], copied.map(&:filename) expected = "# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)" @@ -801,8 +872,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") assert_equal 2, copied.length end ensure @@ -816,11 +887,20 @@ class CopyMigrationsTest < ActiveRecord::TestCase Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"}) - assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") - assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb") + assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb") assert_equal 2, copied.length end ensure clear end + + def test_check_pending_with_stdlib_logger + old, ActiveRecord::Base.logger = ActiveRecord::Base.logger, ::Logger.new($stdout) + quietly do + assert_nothing_raised { ActiveRecord::Migration::CheckPending.new(Proc.new {}).call({}) } + end + ensure + ActiveRecord::Base.logger = old + end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index b5a69c4a92..3f9854200d 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -91,12 +91,6 @@ module ActiveRecord assert_equal 'AddExpressions', migrations[0].name end - def test_deprecated_constructor - assert_deprecated do - ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid") - end - end - def test_relative_migrations list = Dir.chdir(MIGRATIONS_ROOT) do ActiveRecord::Migrator.migrations("valid/") diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 08b3408665..9124105e6d 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -1,6 +1,7 @@ require "cases/helper" require 'models/company_in_module' require 'models/shop' +require 'models/developer' class ModulesTest < ActiveRecord::TestCase fixtures :accounts, :companies, :projects, :developers, :collections, :products, :variants diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index 1209f5460f..b82409bfbe 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -5,12 +5,6 @@ require 'models/customer' class MultiParameterAttributeTest < ActiveRecord::TestCase fixtures :topics - def setup - ActiveRecord::Base.time_zone_aware_attributes = false - ActiveRecord::Base.default_timezone = :local - Time.zone = nil - end - def test_multiparameter_attributes_on_date attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" } topic = Topic.find(1) @@ -82,13 +76,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + with_timezone_config default: :local do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + end end def test_multiparameter_attributes_on_time_with_no_date @@ -148,13 +144,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", - "written_on(5i)" => "12", "written_on(6i)" => "02" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on + with_timezone_config default: :local do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", + "written_on(5i)" => "12", "written_on(6i)" => "02" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on + end end def test_multiparameter_attributes_on_time_will_ignore_hour_if_blank @@ -176,6 +174,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic.attributes = attributes assert_nil topic.written_on end + def test_multiparameter_attributes_on_time_with_seconds_will_ignore_date_if_empty attributes = { "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", @@ -187,56 +186,56 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time_with_utc - ActiveRecord::Base.default_timezone = :utc - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + with_timezone_config default: :utc do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + end end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - 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 + with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + 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 + end end def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false - Time.zone = ActiveSupport::TimeZone[-28800] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - 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) + with_timezone_config default: :local, aware_attributes: false, zone: -28800 do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + 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) + end end def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] - Topic.skip_time_zone_conversion_for_attributes = [:written_on] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on - assert_equal false, topic.written_on.respond_to?(:time_zone) + with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + Topic.skip_time_zone_conversion_for_attributes = [:written_on] + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + assert_equal false, topic.written_on.respond_to?(:time_zone) + end ensure Topic.skip_time_zone_conversion_for_attributes = [] end @@ -244,30 +243,31 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase # Oracle, and Sybase do not have a TIME datatype. unless current_adapter?(:OracleAdapter, :SybaseAdapter) def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] + with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do + attributes = { + "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", + "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time + assert topic.bonus_time.utc? + end + end + end + + def test_multiparameter_attributes_on_time_with_empty_seconds + with_timezone_config default: :local do attributes = { - "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", - "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" } topic = Topic.find(1) topic.attributes = attributes - assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time - assert topic.bonus_time.utc? + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on end end - def test_multiparameter_attributes_on_time_with_empty_seconds - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" - } - topic = Topic.find(1) - topic.attributes = attributes - 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 diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 94837341fc..2f89699df7 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -131,6 +131,20 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert_equal 's1', ship.reload.name end + def test_reuse_already_built_new_record + pirate = Pirate.new + ship_built_first = pirate.build_ship + pirate.ship_attributes = { name: 'Ship 1' } + assert_equal ship_built_first.object_id, pirate.ship.object_id + end + + def test_do_not_allow_assigning_foreign_key_when_reusing_existing_new_record + pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") + pirate.build_ship + pirate.ship_attributes = { name: 'Ship 1', pirate_id: pirate.id + 1 } + assert_equal pirate.id, pirate.ship.pirate_id + end + def test_reject_if_with_a_proc_which_returns_true_always_for_has_many Man.accepts_nested_attributes_for :interests, :reject_if => proc {|attributes| true } man = Man.create(name: "John") @@ -167,7 +181,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record Man.accepts_nested_attributes_for(:interests) man = Man.create(:name => "John") - interest = man.interests.create :topic => 'gardning' + interest = man.interests.create :topic => 'gardening' man = Man.find man.id man.interests_attributes = [{:id => interest.id, :topic => 'gardening'}] assert_equal man.interests.first.topic, man.interests[0].topic @@ -783,30 +797,12 @@ module NestedAttributesOnACollectionAssociationTests end end - def test_validate_presence_of_parent_fails_without_inverse_of - Man.accepts_nested_attributes_for(:interests) - Man.reflect_on_association(:interests).options.delete(:inverse_of) - Interest.reflect_on_association(:man).options.delete(:inverse_of) - - repair_validations(Interest) do - Interest.validates_presence_of(:man) - assert_no_difference ['Man.count', 'Interest.count'] do - man = Man.create(:name => 'John', - :interests_attributes => [{:topic=>'Cars'}, {:topic=>'Sports'}]) - assert !man.errors[:"interests.man"].empty? - end - end - ensure - Man.reflect_on_association(:interests).options[:inverse_of] = :man - Interest.reflect_on_association(:man).options[:inverse_of] = :interests - end - def test_can_use_symbols_as_object_identifier @pirate.attributes = { :parrots_attributes => { :foo => { :name => 'Lovely Day' }, :bar => { :name => 'Blown Away' } } } assert_nothing_raised(NoMethodError) { @pirate.save! } end - def test_numeric_colum_changes_from_zero_to_no_empty_string + def test_numeric_column_changes_from_zero_to_no_empty_string Man.accepts_nested_attributes_for(:interests) repair_validations(Interest) do diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb new file mode 100644 index 0000000000..43a69928b6 --- /dev/null +++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb @@ -0,0 +1,144 @@ +require "cases/helper" +require "models/pirate" +require "models/bird" + +class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase + Pirate.has_many(:birds_with_add_load, + :class_name => "Bird", + :before_add => proc { |p,b| + @@add_callback_called << b + p.birds_with_add_load.to_a + }) + Pirate.has_many(:birds_with_add, + :class_name => "Bird", + :before_add => proc { |p,b| @@add_callback_called << b }) + + Pirate.accepts_nested_attributes_for(:birds_with_add_load, + :birds_with_add, + :allow_destroy => true) + + def setup + @@add_callback_called = [] + @pirate = Pirate.new.tap do |pirate| + pirate.catchphrase = "Don't call me!" + pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}] + pirate.save! + end + @birds = @pirate.birds.to_a + end + + def bird_to_update + @birds[0] + end + + def bird_to_destroy + @birds[1] + end + + def existing_birds_attributes + @birds.map do |bird| + bird.attributes.slice("id","name") + end + end + + def new_birds + @pirate.birds_with_add.to_a - @birds + end + + def new_bird_attributes + [{'name' => "New Bird"}] + end + + def destroy_bird_attributes + [{'id' => bird_to_destroy.id.to_s, "_destroy" => true}] + end + + def update_new_and_destroy_bird_attributes + [{'id' => @birds[0].id.to_s, 'name' => 'New Name'}, + {'name' => "New Bird"}, + {'id' => bird_to_destroy.id.to_s, "_destroy" => true}] + end + + # Characterizing when :before_add callback is called + test ":before_add called for new bird when not loaded" do + assert_not @pirate.birds_with_add.loaded? + @pirate.birds_with_add_attributes = new_bird_attributes + assert_new_bird_with_callback_called + end + + test ":before_add called for new bird when loaded" do + @pirate.birds_with_add.load_target + @pirate.birds_with_add_attributes = new_bird_attributes + assert_new_bird_with_callback_called + end + + def assert_new_bird_with_callback_called + assert_equal(1, new_birds.size) + assert_equal(new_birds, @@add_callback_called) + end + + test ":before_add not called for identical assignment when not loaded" do + assert_not @pirate.birds_with_add.loaded? + @pirate.birds_with_add_attributes = existing_birds_attributes + assert_callbacks_not_called + end + + test ":before_add not called for identical assignment when loaded" do + @pirate.birds_with_add.load_target + @pirate.birds_with_add_attributes = existing_birds_attributes + assert_callbacks_not_called + end + + test ":before_add not called for destroy assignment when not loaded" do + assert_not @pirate.birds_with_add.loaded? + @pirate.birds_with_add_attributes = destroy_bird_attributes + assert_callbacks_not_called + end + + test ":before_add not called for deletion assignment when loaded" do + @pirate.birds_with_add.load_target + @pirate.birds_with_add_attributes = destroy_bird_attributes + assert_callbacks_not_called + end + + def assert_callbacks_not_called + assert_empty new_birds + assert_empty @@add_callback_called + end + + # Ensuring that the records in the association target are updated, + # whether the association is loaded before or not + test "Assignment updates records in target when not loaded" do + assert_not @pirate.birds_with_add.loaded? + @pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes + assert_assignment_affects_records_in_target(:birds_with_add) + end + + test "Assignment updates records in target when loaded" do + @pirate.birds_with_add.load_target + @pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes + assert_assignment_affects_records_in_target(:birds_with_add) + end + + test("Assignment updates records in target when not loaded" + + " and callback loads target") do + assert_not @pirate.birds_with_add_load.loaded? + @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes + assert_assignment_affects_records_in_target(:birds_with_add_load) + end + + test("Assignment updates records in target when loaded" + + " and callback loads target") do + @pirate.birds_with_add_load.load_target + @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes + assert_assignment_affects_records_in_target(:birds_with_add_load) + end + + def assert_assignment_affects_records_in_target(association_name) + association = @pirate.send(association_name) + assert association.detect {|b| b == bird_to_update }.name_changed?, + 'Update record not updated' + assert association.detect {|b| b == bird_to_destroy }.marked_for_destruction?, + 'Destroy record not marked for destruction' + end +end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index b936cca875..6cd3e2154e 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -12,13 +12,13 @@ require 'models/minimalistic' require 'models/warehouse_thing' require 'models/parrot' require 'models/minivan' +require 'models/owner' require 'models/person' require 'models/pet' require 'models/toy' require 'rexml/document' -class PersistencesTest < ActiveRecord::TestCase - +class PersistenceTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys # Oracle UPDATE does not support ORDER BY @@ -139,6 +139,19 @@ class PersistencesTest < ActiveRecord::TestCase end end + def test_becomes + assert_kind_of Reply, topics(:first).becomes(Reply) + assert_equal "The First Topic", topics(:first).becomes(Reply).title + end + + def test_becomes_includes_errors + company = Company.new(:name => nil) + assert !company.valid? + original_errors = company.errors + client = company.becomes(Client) + assert_equal original_errors, client.errors + end + def test_delete_many original_count = Topic.count Topic.delete(deleting = [1, 2]) @@ -247,15 +260,15 @@ class PersistencesTest < ActiveRecord::TestCase topic.title = "Another New Topic" topic.written_on = "2003-12-12 23:23:00" topic.save - topicReloaded = Topic.find(topic.id) - assert_equal("Another New Topic", topicReloaded.title) + topic_reloaded = Topic.find(topic.id) + assert_equal("Another New Topic", topic_reloaded.title) - topicReloaded.title = "Updated topic" - topicReloaded.save + topic_reloaded.title = "Updated topic" + topic_reloaded.save - topicReloadedAgain = Topic.find(topic.id) + topic_reloaded_again = Topic.find(topic.id) - assert_equal("Updated topic", topicReloadedAgain.title) + assert_equal("Updated topic", topic_reloaded_again.title) end def test_update_columns_not_equal_attributes @@ -263,12 +276,12 @@ class PersistencesTest < ActiveRecord::TestCase topic.title = "Still another topic" topic.save - topicReloaded = Topic.allocate - topicReloaded.init_with( + topic_reloaded = Topic.allocate + topic_reloaded.init_with( 'attributes' => topic.attributes.merge('does_not_exist' => 'test') ) - topicReloaded.title = 'A New Topic' - assert_nothing_raised { topicReloaded.save } + topic_reloaded.title = 'A New Topic' + assert_nothing_raised { topic_reloaded.save } end def test_update_for_record_with_only_primary_key @@ -296,6 +309,22 @@ class PersistencesTest < ActiveRecord::TestCase assert_equal "Reply", topic.type end + def test_update_after_create + klass = Class.new(Topic) do + def self.name; 'Topic'; end + after_create do + update_attribute("author_name", "David") + end + end + topic = klass.new + topic.title = "Another New Topic" + topic.save + + topic_reloaded = Topic.find(topic.id) + assert_equal("Another New Topic", topic_reloaded.title) + assert_equal("David", topic_reloaded.author_name) + end + def test_delete topic = Topic.find(1) assert_equal topic, topic.delete, 'topic.delete did not return self' @@ -390,10 +419,6 @@ class PersistencesTest < ActiveRecord::TestCase assert !Topic.find(1).approved? end - def test_update_attribute_does_not_choke_on_nil - assert Topic.find(1).update(nil) - end - def test_update_attribute_for_readonly_attribute minivan = Minivan.find('m1') assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') } @@ -661,6 +686,26 @@ class PersistencesTest < ActiveRecord::TestCase topic.reload assert !topic.approved? assert_equal "The First Topic", topic.title + + assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do + topic.update_attributes(id: 3, title: "Hm is it possible?") + end + assert_not_equal "Hm is it possible?", Topic.find(3).title + + topic.update_attributes(id: 1234) + assert_nothing_raised { topic.reload } + assert_equal topic.title, Topic.find(1234).title + end + + def test_update_attributes_parameters + topic = Topic.find(1) + assert_nothing_raised do + topic.update_attributes({}) + end + + assert_raises(ArgumentError) do + topic.update_attributes(nil) + end end def test_update! diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index a8a9b06ec4..626c6aeaf8 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -16,22 +16,6 @@ class PooledConnectionsTest < ActiveRecord::TestCase @per_test_teardown.each {|td| td.call } end - def checkout_connections - ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.3})) - @connections = [] - @timed_out = 0 - - 4.times do - Thread.new do - begin - @connections << ActiveRecord::Base.connection_pool.checkout - rescue ActiveRecord::ConnectionTimeoutError - @timed_out += 1 - end - end.join - end - end - # Will deadlock due to lack of Monitor timeouts in 1.9 def checkout_checkin_connections(pool_size, threads) ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5})) diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 8e5379cb1f..aa125c70c5 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -205,7 +205,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase self.use_transactional_fixtures = false - def test_primaery_key_method_with_ansi_quotes + def test_primary_key_method_with_ansi_quotes con = ActiveRecord::Base.connection con.execute("SET SESSION sql_mode='ANSI_QUOTES'") assert_equal "id", con.primary_key("topics") diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 3dd11ae89d..e2439b9a24 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -53,50 +53,40 @@ module ActiveRecord end def test_quoted_time_utc - before = ActiveRecord::Base.default_timezone - ActiveRecord::Base.default_timezone = :utc - t = Time.now - assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) - ensure - ActiveRecord::Base.default_timezone = before + with_timezone_config default: :utc do + t = Time.now + assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) + end end def test_quoted_time_local - before = ActiveRecord::Base.default_timezone - ActiveRecord::Base.default_timezone = :local - t = Time.now - assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) - ensure - ActiveRecord::Base.default_timezone = before + with_timezone_config default: :local do + t = Time.now + assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) + end end def test_quoted_time_crazy - before = ActiveRecord::Base.default_timezone - ActiveRecord::Base.default_timezone = :asdfasdf - t = Time.now - assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) - ensure - ActiveRecord::Base.default_timezone = before + with_timezone_config default: :asdfasdf do + t = Time.now + assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) + end end def test_quoted_datetime_utc - before = ActiveRecord::Base.default_timezone - ActiveRecord::Base.default_timezone = :utc - t = DateTime.now - assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) - ensure - ActiveRecord::Base.default_timezone = before + with_timezone_config default: :utc do + t = DateTime.now + assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) + end end ### # DateTime doesn't define getlocal, so make sure it does nothing def test_quoted_datetime_local - before = ActiveRecord::Base.default_timezone - ActiveRecord::Base.default_timezone = :local - t = DateTime.now - assert_equal t.to_s(:db), @quoter.quoted_date(t) - ensure - ActiveRecord::Base.default_timezone = before + with_timezone_config default: :local do + t = DateTime.now + assert_equal t.to_s(:db), @quoter.quoted_date(t) + end end def test_quote_with_quoted_id @@ -194,25 +184,6 @@ module ActiveRecord assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary)) end - def test_quote_binary_with_string_to_binary - col = Class.new(FakeColumn) { - def string_to_binary(value) - 'foo' - end - }.new(:binary) - assert_equal "'foo'", @quoter.quote('lo\l', col) - end - - def test_quote_as_mb_chars_binary_column_with_string_to_binary - col = Class.new(FakeColumn) { - def string_to_binary(value) - 'foo' - end - }.new(:binary) - string = ActiveSupport::Multibyte::Chars.new('lo\l') - assert_equal "'foo'", @quoter.quote(string, col) - end - def test_string_with_crazy_column assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo)) end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index df076c97b4..2afd25c989 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'models/author' require 'models/post' require 'models/comment' require 'models/developer' @@ -7,7 +8,7 @@ require 'models/reader' require 'models/person' class ReadOnlyTest < ActiveRecord::TestCase - fixtures :posts, :comments, :developers, :projects, :developers_projects, :people, :readers + fixtures :authors, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers def test_cant_save_readonly_record dev = Developer.find(1) @@ -34,15 +35,12 @@ class ReadOnlyTest < ActiveRecord::TestCase Developer.readonly.each { |d| assert d.readonly? } end + def test_find_with_joins_option_does_not_imply_readonly + Developer.joins(' ').each { |d| assert_not d.readonly? } + Developer.joins(' ').readonly(true).each { |d| assert d.readonly? } - def test_find_with_joins_option_implies_readonly - # Blank joins don't count. - Developer.joins(' ').each { |d| assert !d.readonly? } - Developer.joins(' ').readonly(false).each { |d| assert !d.readonly? } - - # Others do. - Developer.joins(', projects').each { |d| assert d.readonly? } - Developer.joins(', projects').readonly(false).each { |d| assert !d.readonly? } + Developer.joins(', projects').each { |d| assert_not d.readonly? } + Developer.joins(', projects').readonly(true).each { |d| assert d.readonly? } end def test_has_many_find_readonly @@ -87,7 +85,7 @@ class ReadOnlyTest < ActiveRecord::TestCase # conflicting column names unless current_adapter?(:OracleAdapter) Post.joins(', developers').scoping do - assert Post.find(1).readonly? + assert_not Post.find(1).readonly? assert Post.readonly.find(1).readonly? assert !Post.readonly(false).find(1).readonly? end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index a9d46f4fba..d7ad5ed29f 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -18,6 +18,11 @@ require 'models/subscription' require 'models/tag' require 'models/sponsor' require 'models/edge' +require 'models/hotel' +require 'models/chef' +require 'models/department' +require 'models/cake_designer' +require 'models/drink_designer' class ReflectionTest < ActiveRecord::TestCase include ActiveRecord::Reflection @@ -35,18 +40,18 @@ class ReflectionTest < ActiveRecord::TestCase def test_read_attribute_names assert_equal( - %w( id title author_name author_email_address bonus_time written_on last_read content important group approved replies_count parent_id parent_title type created_at updated_at ).sort, + %w( id title author_name author_email_address bonus_time written_on last_read content important group approved replies_count unique_replies_count parent_id parent_title type created_at updated_at ).sort, @first.attribute_names.sort ) end def test_columns - assert_equal 17, Topic.columns.length + assert_equal 18, Topic.columns.length end def test_columns_are_returned_in_the_order_they_were_declared column_names = Topic.columns.map { |column| column.name } - assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content important approved replies_count parent_id parent_title type group created_at updated_at), column_names + assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content important approved replies_count unique_replies_count parent_id parent_title type group created_at updated_at), column_names end def test_content_columns @@ -186,14 +191,6 @@ class ReflectionTest < ActiveRecord::TestCase ActiveRecord::Base.store_full_sti_class = true end - def test_reflection_of_all_associations - # FIXME these assertions bust a lot - assert_equal 39, Firm.reflect_on_all_associations.size - assert_equal 29, Firm.reflect_on_all_associations(:has_many).size - assert_equal 10, Firm.reflect_on_all_associations(:has_one).size - assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size - end - def test_reflection_should_not_raise_error_when_compared_to_other_object assert_nothing_raised { Firm.reflections[:clients] == Object.new } end @@ -235,6 +232,17 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal expected, actual end + def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case + @hotel = Hotel.create! + @department = @hotel.departments.create! + @department.chefs.create!(employable: CakeDesigner.create!) + @department.chefs.create!(employable: DrinkDesigner.create!) + + assert_equal 1, @hotel.cake_designers.size + assert_equal 1, @hotel.drink_designers.size + assert_equal 2, @hotel.chefs.size + end + def test_nested? assert !Author.reflect_on_association(:comments).nested? assert Author.reflect_on_association(:tags).nested? @@ -260,8 +268,9 @@ class ReflectionTest < ActiveRecord::TestCase reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } - through = ActiveRecord::Reflection::ThroughReflection.new(:fuu, :edge, nil, {}, Author) - through.stubs(:source_reflection).returns(stub_everything(:options => {}, :class_name => 'Edge')) + through = Class.new(ActiveRecord::Reflection::ThroughReflection) { + define_method(:source_reflection) { reflection } + }.new(:fuu, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key } end diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb new file mode 100644 index 0000000000..c171c5e14e --- /dev/null +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -0,0 +1,98 @@ +require 'cases/helper' +require 'models/post' +require 'models/comment' + +module ActiveRecord + class DelegationTest < ActiveRecord::TestCase + fixtures :posts + + def assert_responds(target, method) + assert target.respond_to?(method) + assert_nothing_raised do + method_arity = target.to_a.method(method).arity + + if method_arity.zero? + target.send(method) + elsif method_arity < 0 + if method == :shuffle! + target.send(method) + else + target.send(method, 1) + end + else + raise NotImplementedError + end + end + end + end + + class DelegationAssociationTest < DelegationTest + def target + Post.first.comments + end + + [:map, :collect].each do |method| + test "##{method} is delgated" do + assert_responds(target, method) + assert_equal(target.pluck(:body), target.send(method) {|post| post.body }) + end + + test "##{method}! is not delgated" do + assert_deprecated do + assert_responds(target, "#{method}!") + end + end + end + + [:compact!, :flatten!, :reject!, :reverse!, :rotate!, + :shuffle!, :slice!, :sort!, :sort_by!].each do |method| + test "##{method} delegation is deprecated" do + assert_deprecated do + assert_responds(target, method) + end + end + end + + [:select!, :uniq!].each do |method| + test "##{method} is implemented" do + assert_responds(target, method) + end + end + end + + class DelegationRelationTest < DelegationTest + def target + Comment.where.not(body: nil) + end + + [:map, :collect].each do |method| + test "##{method} is delgated" do + assert_responds(target, method) + assert_equal(target.pluck(:body), target.send(method) {|post| post.body }) + end + + test "##{method}! is not delgated" do + assert_deprecated do + assert_responds(target, "#{method}!") + end + end + end + + [:compact!, :flatten!, :reject!, :reverse!, :rotate!, + :shuffle!, :slice!, :sort!, :sort_by!].each do |method| + test "##{method} delegation is deprecated" do + assert_deprecated do + assert_responds(target, method) + end + end + end + + [:select!, :uniq!].each do |method| + test "##{method} is triggers an immutable error" do + assert_raises ActiveRecord::ImmutableRelation do + assert_responds(target, method) + end + end + end + end +end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb new file mode 100644 index 0000000000..020fb24afa --- /dev/null +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -0,0 +1,148 @@ +require 'cases/helper' +require 'models/post' + +module ActiveRecord + class RelationMutationTest < ActiveSupport::TestCase + class FakeKlass < Struct.new(:table_name, :name) + extend ActiveRecord::Delegation::DelegateCache + inherited self + + def arel_table + Post.arel_table + end + + def connection + Post.connection + end + + def relation_delegate_class(klass) + self.class.relation_delegate_class(klass) + end + end + + def relation + @relation ||= Relation.new FakeKlass.new('posts'), :b + end + + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order]).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 '#order!' do + assert relation.order!('name ASC').equal?(relation) + assert_equal ['name ASC'], relation.order_values + end + + test '#order! with symbol prepends the table name' do + assert relation.order!(:name).equal?(relation) + node = relation.order_values.first + assert node.ascending? + assert_equal :name, node.expr.name + assert_equal "posts", node.expr.relation.name + end + + test '#order! on non-string does not attempt regexp match for references' do + obj = Object.new + obj.expects(:=~).never + assert relation.order!(obj) + assert_equal [obj], relation.order_values + 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 '#reorder! with symbol prepends the table name' do + assert relation.reorder!(:name).equal?(relation) + node = relation.order_values.first + + assert node.ascending? + assert_equal :name, node.expr.name + assert_equal "posts", node.expr.relation.name + 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 '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 + + test 'none!' do + assert relation.none!.equal?(relation) + assert_equal [NullRelation], relation.extending_values + assert relation.is_a?(NullRelation) + end + + test 'distinct!' do + relation.distinct! :foo + assert_equal :foo, relation.distinct_value + assert_equal :foo, relation.uniq_value # deprecated access + end + + test 'uniq! was replaced by distinct!' do + relation.uniq! :foo + assert_equal :foo, relation.distinct_value + assert_equal :foo, relation.uniq_value # deprecated access + end + end +end diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb new file mode 100644 index 0000000000..14a8d97d36 --- /dev/null +++ b/activerecord/test/cases/relation/predicate_builder_test.rb @@ -0,0 +1,14 @@ +require "cases/helper" +require 'models/topic' + +module ActiveRecord + class PredicateBuilderTest < ActiveRecord::TestCase + def test_registering_new_handlers + PredicateBuilder.register_handler(Regexp, proc do |column, value| + Arel::Nodes::InfixOperation.new('~', column, value.source) + end) + + assert_match %r{["`]topics["`].["`]title["`] ~ 'rails'}i, Topic.where(title: /rails/).to_sql + end + end +end diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index 8ce44636b4..92d1e013e8 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -6,26 +6,31 @@ module ActiveRecord class WhereChainTest < ActiveRecord::TestCase fixtures :posts + def setup + super + @name = 'title' + end + def test_not_eq - expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'hello') + expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello') relation = Post.where.not(title: 'hello') assert_equal([expected], relation.where_values) end def test_not_null - expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], nil) + expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], nil) relation = Post.where.not(title: nil) assert_equal([expected], relation.where_values) end def test_not_in - expected = Arel::Nodes::NotIn.new(Post.arel_table[:title], %w[hello goodbye]) + expected = Arel::Nodes::NotIn.new(Post.arel_table[@name], %w[hello goodbye]) relation = Post.where.not(title: %w[hello goodbye]) assert_equal([expected], relation.where_values) end def test_association_not_eq - expected = Arel::Nodes::NotEqual.new(Comment.arel_table[:title], 'hello') + expected = Arel::Nodes::NotEqual.new(Comment.arel_table[@name], 'hello') relation = Post.joins(:comments).where.not(comments: {title: 'hello'}) assert_equal(expected.to_sql, relation.where_values.first.to_sql) end @@ -33,20 +38,20 @@ module ActiveRecord def test_not_eq_with_preceding_where relation = Post.where(title: 'hello').where.not(title: 'world') - expected = Arel::Nodes::Equality.new(Post.arel_table[:title], 'hello') + expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'hello') assert_equal(expected, relation.where_values.first) - expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'world') + expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'world') assert_equal(expected, relation.where_values.last) end def test_not_eq_with_succeeding_where relation = Post.where.not(title: 'hello').where(title: 'world') - expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'hello') + expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello') assert_equal(expected, relation.where_values.first) - expected = Arel::Nodes::Equality.new(Post.arel_table[:title], 'world') + expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'world') assert_equal(expected, relation.where_values.last) end @@ -65,10 +70,10 @@ module ActiveRecord def test_chaining_multiple relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails') - expected = Arel::Nodes::NotIn.new(Post.arel_table[:author_id], [1, 2]) + expected = Arel::Nodes::NotIn.new(Post.arel_table['author_id'], [1, 2]) assert_equal(expected, relation.where_values[0]) - expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'ruby on rails') + expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'ruby on rails') assert_equal(expected, relation.where_values[1]) end end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index c43c7601a2..3e460fa3d6 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -5,6 +5,7 @@ require 'models/treasure' require 'models/post' require 'models/comment' require 'models/edge' +require 'models/topic' module ActiveRecord class WhereTest < ActiveRecord::TestCase @@ -80,6 +81,38 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_decorated_polymorphic_where + treasure_decorator = Struct.new(:model) do + def self.method_missing(method, *args, &block) + Treasure.send(method, *args, &block) + end + + def is_a?(klass) + model.is_a?(klass) + end + + def method_missing(method, *args, &block) + model.send(method, *args, &block) + end + end + + treasure = Treasure.new + treasure.id = 1 + decorated_treasure = treasure_decorator.new(treasure) + + expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1) + actual = PriceEstimate.where(estimate_of: decorated_treasure) + + assert_equal expected.to_sql, actual.to_sql + end + + def test_aliased_attribute + expected = Topic.where(heading: 'The First Topic') + actual = Topic.where(title: 'The First Topic') + + assert_equal expected.to_sql, actual.to_sql + end + def test_where_error assert_raises(ActiveRecord::StatementInvalid) do Post.where(:id => { 'posts.author_id' => 10 }).first diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index fd0b05cb77..70d113fb39 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -1,19 +1,25 @@ require "cases/helper" require 'models/post' require 'models/comment' +require 'models/author' +require 'models/rating' module ActiveRecord class RelationTest < ActiveRecord::TestCase - fixtures :posts, :comments + fixtures :posts, :comments, :authors class FakeKlass < Struct.new(:table_name, :name) + extend ActiveRecord::Delegation::DelegateCache + + inherited self + + def self.connection + Post.connection + end end def test_construction - relation = nil - assert_nothing_raised do - relation = Relation.new FakeKlass, :b - end + relation = Relation.new FakeKlass, :b assert_equal FakeKlass, relation.klass assert_equal :b, relation.table assert !relation.loaded, 'relation is not loaded' @@ -74,8 +80,8 @@ module ActiveRecord end def test_table_name_delegates_to_klass - relation = Relation.new FakeKlass.new('foo'), :b - assert_equal 'foo', relation.table_name + relation = Relation.new FakeKlass.new('posts'), :b + assert_equal 'posts', relation.table_name end def test_scope_for_create @@ -109,6 +115,12 @@ module ActiveRecord assert_equal({}, relation.scope_for_create) end + def test_bad_constants_raise_errors + assert_raises(NameError) do + ActiveRecord::Relation::HelloWorld + end + end + def test_empty_eager_loading? relation = Relation.new FakeKlass, :b assert !relation.eager_loading? @@ -169,114 +181,46 @@ module ActiveRecord end test 'merging a hash interpolates conditions' do - klass = stub_everything - klass.stubs(:sanitize_sql).with(['foo = ?', 'bar']).returns('foo = bar') + klass = Class.new(FakeKlass) do + def self.sanitize_sql(args) + raise unless args == ['foo = ?', 'bar'] + 'foo = bar' + end + end relation = Relation.new(klass, :b) relation.merge!(where: ['foo = ?', 'bar']) assert_equal ['foo = bar'], relation.where_values end - end - - class RelationMutationTest < ActiveSupport::TestCase - class FakeKlass < Struct.new(:table_name, :name) - def quoted_table_name - %{"#{table_name}"} - end - end - def relation - @relation ||= Relation.new FakeKlass.new('posts'), :b - end - - (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order]).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 "#order!" do - assert relation.order!('name ASC').equal?(relation) - assert_equal ['name ASC'], relation.order_values - end - - test "#order! with symbol prepends the table name" do - assert relation.order!(:name).equal?(relation) - assert_equal ['"posts".name ASC'], relation.order_values - 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 + def test_merging_readonly_false + relation = Relation.new FakeKlass, :b + readonly_false_relation = relation.readonly(false) + # test merging in both directions + assert_equal false, relation.merge(readonly_false_relation).readonly_value + assert_equal false, readonly_false_relation.merge(relation).readonly_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 + def test_relation_merging_with_merged_joins_as_symbols + special_comments_with_ratings = SpecialComment.joins(:ratings) + posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) + assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length end - test 'create_with!' do - assert relation.create_with!(foo: 'bar').equal?(relation) - assert_equal({foo: 'bar'}, relation.create_with_value) - end + def test_respond_to_for_non_selected_element + post = Post.select(:title).first + assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception" - test 'merge!' do - assert relation.merge!(where: :foo).equal?(relation) - assert_equal [:foo], relation.where_values + silence_warnings { post = Post.select("'title' as post_title").first } + assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception" end - test 'merge with a proc' do - assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values + def test_relation_merging_with_merged_joins_as_strings + join_string = "LEFT OUTER JOIN #{Rating.quoted_table_name} ON #{SpecialComment.quoted_table_name}.id = #{Rating.quoted_table_name}.comment_id" + special_comments_with_ratings = SpecialComment.joins join_string + posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) + assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length end - test 'none!' do - assert relation.none!.equal?(relation) - assert_equal [NullRelation], relation.extending_values - assert relation.is_a?(NullRelation) - end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 26cbb03892..c9c7ac04b3 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -42,6 +42,11 @@ class RelationTest < ActiveRecord::TestCase end def test_two_scopes_with_includes_should_not_drop_any_include + # heat habtm cache + car = Car.incl_engines.incl_tyres.first + car.tyres.length + car.engines.length + car = Car.incl_engines.incl_tyres.first assert_no_queries { car.tyres.length } assert_no_queries { car.engines.length } @@ -139,6 +144,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal relation.to_a, Topic.select('a.*').from(relation, :a).to_a end + def test_finding_with_subquery_with_binds + relation = Post.first.comments + assert_equal relation.to_a, Comment.select('*').from(relation).to_a + assert_equal relation.to_a, Comment.select('subquery.*').from(relation).to_a + assert_equal relation.to_a, Comment.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) @@ -170,6 +182,10 @@ class RelationTest < ActiveRecord::TestCase assert_equal topics(:fourth).title, topics.first.title end + def test_order_with_hash_and_symbol_generates_the_same_sql + assert_equal Topic.order(:id).to_sql, Topic.order(:id => :asc).to_sql + end + def test_raising_exception_on_invalid_hash_params assert_raise(ArgumentError) { Topic.order(:name, "id DESC", :id => :DeSc) } end @@ -180,7 +196,7 @@ class RelationTest < ActiveRecord::TestCase end def test_finding_with_order_concatenated - topics = Topic.order('title').order('author_name') + topics = Topic.order('author_name').order('title') assert_equal 4, topics.to_a.size assert_equal topics(:fourth).title, topics.first.title end @@ -258,7 +274,7 @@ class RelationTest < ActiveRecord::TestCase 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 [], Developer.none.pluck(:id, :name) assert_equal 0, Developer.none.delete_all assert_equal 0, Developer.none.update_all(:name => 'David') assert_equal 0, Developer.none.delete(1) @@ -278,8 +294,9 @@ class RelationTest < ActiveRecord::TestCase def test_null_relation_calculations_methods assert_no_queries do - assert_equal 0, Developer.none.count - assert_equal nil, Developer.none.calculate(:average, 'salary') + assert_equal 0, Developer.none.count + assert_equal 0, Developer.none.calculate(:count, nil) + assert_equal nil, Developer.none.calculate(:average, 'salary') end end @@ -288,6 +305,10 @@ class RelationTest < ActiveRecord::TestCase assert_equal({}, Developer.none.where_values_hash) end + def test_null_relation_where_values_hash + assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash) + end + def test_joins_with_nil_argument assert_nothing_raised { DependentFirm.joins(nil).first } end @@ -367,7 +388,7 @@ class RelationTest < ActiveRecord::TestCase def test_respond_to_dynamic_finders 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| + ["find_by_title", "find_by_title_and_author_name"].each do |method| assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}" end end @@ -465,6 +486,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal Developer.where(name: 'David').map(&:id).sort, developers end + def test_includes_with_select + query = Post.select('comments_count AS ranking').order('ranking').includes(:comments) + .where(comments: { id: 1 }) + + assert_equal ['comments_count AS ranking'], query.select_values + assert_equal 1, query.to_a.size + end + def test_loading_with_one_association posts = Post.preload(:comments) post = posts.find { |p| p.id == 1 } @@ -480,6 +509,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.find(1).last_comment, post.last_comment end + def test_to_sql_on_eager_join + expected = assert_sql { + Post.eager_load(:last_comment).order('comments.id DESC').to_a + }.first + actual = Post.eager_load(:last_comment).order('comments.id DESC').to_sql + assert_equal expected, actual + end + def test_loading_with_one_association_with_non_preload posts = Post.eager_load(:last_comment).order('comments.id DESC') post = posts.find { |p| p.id == 1 } @@ -492,6 +529,7 @@ class RelationTest < ActiveRecord::TestCase expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do + assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } end @@ -596,6 +634,36 @@ class RelationTest < ActiveRecord::TestCase relation = Author.where(:id => Author.where(:id => david.id)) assert_equal [david], relation.to_a } + + assert_queries(1) { + relation = Author.where('id in (?)', Author.where(id: david).select(:id)) + assert_equal [david], relation.to_a + } + + assert_queries(1) do + relation = Author.where('id in (:author_ids)', author_ids: Author.where(id: david).select(:id)) + assert_equal [david], relation.to_a + end + end + + def test_find_all_using_where_with_relation_with_bound_values + david = authors(:david) + davids_posts = david.posts.order(:id).to_a + + assert_queries(1) do + relation = Post.where(id: david.posts.select(:id)) + assert_equal davids_posts, relation.order(:id).to_a + end + + assert_queries(1) do + relation = Post.where('id in (?)', david.posts.select(:id)) + assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as bind variables' + end + + assert_queries(1) do + relation = Post.where('id in (:post_ids)', post_ids: david.posts.select(:id)) + assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as named bind variables' + end end def test_find_all_using_where_with_relation_and_alternate_primary_key @@ -789,11 +857,11 @@ class RelationTest < ActiveRecord::TestCase def test_count_with_distinct posts = Post.all - assert_equal 3, posts.count(:comments_count, :distinct => true) - assert_equal 11, posts.count(:comments_count, :distinct => false) + assert_equal 3, posts.distinct(true).count(:comments_count) + assert_equal 11, posts.distinct(false).count(:comments_count) - assert_equal 3, posts.select(:comments_count).count(:distinct => true) - assert_equal 11, posts.select(:comments_count).count(:distinct => false) + assert_equal 3, posts.distinct(true).select(:comments_count).count + assert_equal 11, posts.distinct(false).select(:comments_count).count end def test_count_explicit_columns @@ -1173,20 +1241,20 @@ class RelationTest < ActiveRecord::TestCase end def test_default_scope_order_with_scope_order - assert_equal 'honda', CoolCar.order_using_new_style.limit(1).first.name - assert_equal 'honda', FastCar.order_using_new_style.limit(1).first.name + assert_equal 'zyke', CoolCar.order_using_new_style.limit(1).first.name + assert_equal 'zyke', FastCar.order_using_new_style.limit(1).first.name end def test_order_using_scoping car1 = CoolCar.order('id DESC').scoping do - CoolCar.all.merge!(:order => 'id asc').first + CoolCar.all.merge!(order: 'id asc').first end - assert_equal 'honda', car1.name + assert_equal 'zyke', car1.name car2 = FastCar.order('id DESC').scoping do - FastCar.all.merge!(:order => 'id asc').first + FastCar.all.merge!(order: 'id asc').first end - assert_equal 'honda', car2.name + assert_equal 'zyke', car2.name end def test_unscoped_block_style @@ -1206,20 +1274,9 @@ class RelationTest < ActiveRecord::TestCase assert_equal "id", Post.all.primary_key end - def test_eager_loading_with_conditions_on_joins - scope = Post.includes(:comments) - - # This references the comments table, and so it should cause the comments to be eager - # loaded via a JOIN, rather than by subsequent queries. - scope = scope.joins( - Post.arel_table.create_join( - Post.arel_table, - Post.arel_table.create_on(Comment.arel_table[:id].eq(3)) - ) - ) - + def test_disable_implicit_join_references_is_deprecated assert_deprecated do - assert scope.eager_loading? + ActiveRecord::Base.disable_implicit_join_references = true end end @@ -1269,7 +1326,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal posts(:welcome), comments(:greetings).post end - def test_uniq + def test_distinct tag1 = Tag.create(:name => 'Foo') tag2 = Tag.create(:name => 'Foo') @@ -1277,14 +1334,25 @@ class RelationTest < ActiveRecord::TestCase assert_equal ['Foo', 'Foo'], query.map(&:name) assert_sql(/DISTINCT/) do + assert_equal ['Foo'], query.distinct.map(&:name) assert_equal ['Foo'], query.uniq.map(&:name) end assert_sql(/DISTINCT/) do + assert_equal ['Foo'], query.distinct(true).map(&:name) assert_equal ['Foo'], query.uniq(true).map(&:name) end + assert_equal ['Foo', 'Foo'], query.distinct(true).distinct(false).map(&:name) assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) end + def test_doesnt_add_having_values_if_options_are_blank + scope = Post.having('') + assert_equal [], scope.having_values + + scope = Post.having([]) + assert_equal [], scope.having_values + end + def test_references_triggers_eager_loading scope = Post.includes(:comments) assert !scope.eager_loading? @@ -1330,6 +1398,24 @@ class RelationTest < ActiveRecord::TestCase assert_equal [], scope.references_values end + def test_automatically_added_reorder_references + scope = Post.reorder('comments.body') + assert_equal %w(comments), scope.references_values + + scope = Post.reorder('comments.body', 'yaks.body') + assert_equal %w(comments yaks), scope.references_values + + # Don't infer yaks, let's not go down that road again... + scope = Post.reorder('comments.body, yaks.body') + assert_equal %w(comments), scope.references_values + + scope = Post.reorder('comments.body asc') + assert_equal %w(comments), scope.references_values + + scope = Post.reorder('foo(comments.body)') + assert_equal [], scope.references_values + end + def test_presence topics = Topic.all @@ -1524,4 +1610,43 @@ class RelationTest < ActiveRecord::TestCase assert merged.to_sql.include?("wtf") assert merged.to_sql.include?("bbq") end + + def test_merging_removes_rhs_bind_parameters + left = Post.where(id: Arel::Nodes::BindParam.new('?')) + column = Post.columns_hash['id'] + left.bind_values += [[column, 20]] + right = Post.where(id: 10) + + merged = left.merge(right) + assert_equal [], merged.bind_values + end + + def test_merging_keeps_lhs_bind_parameters + column = Post.columns_hash['id'] + binds = [[column, 20]] + + right = Post.where(id: Arel::Nodes::BindParam.new('?')) + right.bind_values += binds + left = Post.where(id: 10) + + merged = left.merge(right) + assert_equal binds, merged.bind_values + end + + def test_merging_reorders_bind_params + post = Post.first + id_column = Post.columns_hash['id'] + title_column = Post.columns_hash['title'] + + bv = Post.connection.substitute_at id_column, 0 + + right = Post.where(id: bv) + right.bind_values += [[id_column, post.id]] + + left = Post.where(title: bv) + left.bind_values += [[title_column, post.title]] + + merged = left.merge(right) + assert_equal post, merged.first + end end diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb new file mode 100644 index 0000000000..b6c583dbf5 --- /dev/null +++ b/activerecord/test/cases/result_test.rb @@ -0,0 +1,32 @@ +require "cases/helper" + +module ActiveRecord + class ResultTest < ActiveRecord::TestCase + def result + Result.new(['col_1', 'col_2'], [ + ['row 1 col 1', 'row 1 col 2'], + ['row 2 col 1', 'row 2 col 2'] + ]) + end + + def test_to_hash_returns_row_hashes + assert_equal [ + {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'}, + {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'} + ], result.to_hash + end + + def test_each_with_block_returns_row_hashes + result.each do |row| + assert_equal ['col_1', 'col_2'], row.keys + end + end + + def test_each_without_block_returns_an_enumerator + result.each.with_index do |row, index| + assert_equal ['col_1', 'col_2'], row.keys + assert_kind_of Integer, index + end + end + end +end diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 817897ceac..766b2ff2ef 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -1,10 +1,21 @@ require "cases/helper" require 'models/binary' +require 'models/author' +require 'models/post' class SanitizeTest < ActiveRecord::TestCase def setup end + def test_sanitize_sql_hash_handles_associations + quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") + quoted_column_name = ActiveRecord::Base.connection.quote_column_name("name") + quoted_table_name = ActiveRecord::Base.connection.quote_table_name("adorable_animals") + expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}" + + assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}}) + end + def test_sanitize_sql_array_handles_string_interpolation quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi") assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"]) @@ -22,4 +33,17 @@ class SanitizeTest < ActiveRecord::TestCase assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"]) assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars]) end + + def test_sanitize_sql_array_handles_relations + david = Author.create!(name: 'David') + david_posts = david.posts.select(:id) + + sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i + + select_author_sql = Post.send(:sanitize_sql_array, ['id in (?)', david_posts]) + assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for bind variables') + + select_author_sql = Post.send(:sanitize_sql_array, ['id in (:post_ids)', post_ids: david_posts]) + assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for named bind variables') + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 1147b9a09e..1ee8e60924 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -177,13 +177,19 @@ 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"], name: "company_index"', index_definition + if current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) || current_adapter?(:PostgreSQLAdapter) + assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition + else + assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition + end 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 + assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition + elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) + assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition else assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition end @@ -196,6 +202,11 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r(primary_key: "movieid"), match[1], "non-standard primary key not preserved" end + def test_schema_dump_should_use_false_as_default + output = standard_dump + assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output + end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_schema_dump_should_not_add_default_value_for_mysql_text_field output = standard_dump @@ -219,6 +230,12 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t.text\s+"medium_text",\s+limit: 16777215$}, output assert_match %r{t.text\s+"long_text",\s+limit: 2147483647$}, output end + + def test_schema_dumps_index_type + output = standard_dump + assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output + assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output + end end def test_schema_dump_includes_decimal_options @@ -230,6 +247,11 @@ class SchemaDumperTest < ActiveRecord::TestCase end if current_adapter?(:PostgreSQLAdapter) + def test_schema_dump_includes_bigint_default + output = standard_dump + assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output + end + def test_schema_dump_includes_extensions connection = ActiveRecord::Base.connection skip unless connection.supports_extensions? @@ -282,7 +304,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_uuid_shorthand_definition output = standard_dump - if %r{create_table "poistgresql_uuids"} =~ output + if %r{create_table "postgresql_uuids"} =~ output assert_match %r{t.uuid "guid"}, output end end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 2b4aadc7ed..76f395ba83 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -1,334 +1,6 @@ -require "cases/helper" +require 'cases/helper' require 'models/post' -require 'models/author' require 'models/developer' -require 'models/project' -require 'models/comment' -require 'models/category' -require 'models/person' -require 'models/reference' - -class RelationScopingTest < ActiveRecord::TestCase - fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects - - def test_reverse_order - assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order - end - - def test_reverse_order_with_arel_node - assert_equal Developer.order("id DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).reverse_order - end - - def test_reverse_order_with_multiple_arel_nodes - assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).order(Developer.arel_table[:name].desc).reverse_order - end - - def test_reverse_order_with_arel_nodes_and_strings - assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order("id DESC").order(Developer.arel_table[:name].desc).reverse_order - end - - def test_double_reverse_order_produces_original_order - assert_equal Developer.order("name DESC"), Developer.order("name DESC").reverse_order.reverse_order - end - - def test_scoped_find - Developer.where("name = 'David'").scoping do - assert_nothing_raised { Developer.find(1) } - end - end - - def test_scoped_find_first - developer = Developer.find(10) - Developer.where("salary = 100000").scoping do - assert_equal developer, Developer.order("name").first - end - end - - def test_scoped_find_last - highest_salary = Developer.order("salary DESC").first - - Developer.order("salary").scoping do - assert_equal highest_salary, Developer.last - end - end - - def test_scoped_find_last_preserves_scope - 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 - assert_equal lowest_salary, Developer.first - end - end - - def test_scoped_find_combines_and_sanitizes_conditions - Developer.where("salary = 9000").scoping do - assert_equal developers(:poor_jamis), Developer.where("name = 'Jamis'").first - end - end - - def test_scoped_find_all - Developer.where("name = 'David'").scoping do - assert_equal [developers(:david)], Developer.all - end - end - - def test_scoped_find_select - Developer.select("id, name").scoping do - developer = Developer.where("name = 'David'").first - assert_equal "David", developer.name - assert !developer.has_attribute?(:salary) - end - end - - def test_scope_select_concatenates - Developer.select("id, name").scoping do - developer = Developer.select('salary').where("name = 'David'").first - 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.where("name = 'David'").scoping do - assert_equal 1, Developer.count - end - - Developer.where('salary = 100000').scoping do - assert_equal 8, Developer.count - assert_equal 1, Developer.where("name LIKE 'fixture_1%'").count - end - end - - 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).to_a - 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.joins('JOIN developers_projects ON id = developer_id').scoping do - Developer.where('developers_projects.project_id = 2').to_a - 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_create_with_where - new_comment = VerySpecialComment.where(:post_id => 1).scoping do - VerySpecialComment.create :body => "Wonderful world" - end - - assert_equal 1, new_comment.post_id - assert Post.find(1).comments.include?(new_comment) - end - - def test_scoped_create_with_create_with - new_comment = VerySpecialComment.create_with(:post_id => 1).scoping do - VerySpecialComment.create :body => "Wonderful world" - end - - assert_equal 1, new_comment.post_id - assert Post.find(1).comments.include?(new_comment) - end - - def test_scoped_create_with_create_with_has_higher_priority - new_comment = VerySpecialComment.where(:post_id => 2).create_with(:post_id => 1).scoping do - VerySpecialComment.create :body => "Wonderful world" - end - - assert_equal 1, new_comment.post_id - assert Post.find(1).comments.include?(new_comment) - end - - def test_ensure_that_method_scoping_is_correctly_restored - begin - Developer.where("name = 'Jamis'").scoping do - raise "an exception" - end - rescue - end - - assert !Developer.all.where_values.include?("name = 'Jamis'") - end - - def test_default_scope_filters_on_joins - assert_equal 1, DeveloperFilteredOnJoins.all.count - assert_equal DeveloperFilteredOnJoins.all.first, developers(:david).becomes(DeveloperFilteredOnJoins) - end - - def test_update_all_default_scope_filters_on_joins - DeveloperFilteredOnJoins.update_all(:salary => 65000) - assert_equal 65000, Developer.find(developers(:david).id).salary - - # has not changed jamis - assert_not_equal 65000, Developer.find(developers(:jamis).id).salary - end - - def test_delete_all_default_scope_filters_on_joins - assert_not_equal [], DeveloperFilteredOnJoins.all - - DeveloperFilteredOnJoins.delete_all() - - assert_equal [], DeveloperFilteredOnJoins.all - assert_not_equal [], Developer.all - end -end - -class NestedRelationScopingTest < ActiveRecord::TestCase - fixtures :authors, :developers, :projects, :comments, :posts - - def test_merge_options - Developer.where('salary = 80000').scoping do - Developer.limit(10).scoping do - devs = Developer.all - assert_match '(salary = 80000)', devs.to_sql - assert_equal 10, devs.taken - end - end - end - - def test_merge_inner_scope_has_priority - Developer.limit(5).scoping do - Developer.limit(10).scoping do - assert_equal 10, Developer.all.size - end - end - end - - def test_replace_options - Developer.where(:name => 'David').scoping do - Developer.unscoped do - assert_equal 'Jamis', Developer.where(:name => 'Jamis').first[:name] - end - - assert_equal 'David', Developer.first[:name] - end - end - - def test_three_level_nested_exclusive_scoped_find - Developer.where("name = 'Jamis'").scoping do - assert_equal 'Jamis', Developer.first.name - - Developer.unscoped.where("name = 'David'") do - assert_equal 'David', Developer.first.name - - Developer.unscoped.where("name = 'Maiha'") do - assert_equal nil, Developer.first - end - - # ensure that scoping is restored - assert_equal 'David', Developer.first.name - end - - # ensure that scoping is restored - assert_equal 'Jamis', Developer.first.name - end - end - - def test_nested_scoped_create - comment = Comment.create_with(:post_id => 1).scoping do - Comment.create_with(:post_id => 2).scoping do - 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 = Comment.create_with(:body => "Hey guys, nested scopes are broken. Please fix!").scoping do - Comment.unscoped.create_with(:post_id => 1).scoping do - assert Comment.new.body.blank? - Comment.create :body => "Hey guys" - end - end - - assert_equal 1, comment.post_id - assert_equal 'Hey guys', comment.body - end -end - -class HasManyScopingTest< ActiveRecord::TestCase - fixtures :comments, :posts, :people, :references - - def setup - @welcome = Post.find(1) - end - - def test_forwarding_of_static_methods - assert_equal 'a comment...', Comment.what_are_you - assert_equal 'a comment...', @welcome.comments.what_are_you - end - - def test_forwarding_to_scoped - assert_equal 4, Comment.search_by_type('Comment').size - assert_equal 2, @welcome.comments.search_by_type('Comment').size - end - - def test_nested_scope_finder - Comment.where('1=0').scoping do - assert_equal 0, @welcome.comments.count - assert_equal 'a comment...', @welcome.comments.what_are_you - end - - Comment.where('1=1').scoping do - assert_equal 2, @welcome.comments.count - assert_equal 'a comment...', @welcome.comments.what_are_you - end - end - - def test_should_maintain_default_scope_on_associations - magician = BadReference.find(1) - assert_equal [magician], people(:michael).bad_references - end - - def test_should_default_scope_on_associations_is_overriden_by_association_conditions - reference = references(:michael_unicyclist).becomes(BadReference) - assert_equal [reference], people(:michael).fixed_bad_references - end - - def test_should_maintain_default_scope_on_eager_loaded_associations - michael = Person.where(:id => people(:michael).id).includes(:bad_references).first - magician = BadReference.find(1) - assert_equal [magician], michael.bad_references - end -end - -class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase - fixtures :posts, :categories, :categories_posts - - def setup - @welcome = Post.find(1) - end - - def test_forwarding_of_static_methods - assert_equal 'a category...', Category.what_are_you - assert_equal 'a category...', @welcome.categories.what_are_you - end - - def test_nested_scope_finder - Category.where('1=0').scoping do - assert_equal 0, @welcome.categories.count - assert_equal 'a category...', @welcome.categories.what_are_you - end - - Category.where('1=1').scoping do - assert_equal 2, @welcome.categories.count - assert_equal 'a category...', @welcome.categories.what_are_you - end - end -end class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts @@ -383,31 +55,36 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_default_scoping_with_threads + skip "in-memory database mustn't disconnect" if in_memory_db? + 2.times do - Thread.new { assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') }.join + Thread.new { + assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') + DeveloperOrderedBySalary.connection.close + }.join end end def test_default_scope_with_inheritance wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash - assert_equal "Jamis", wheres[:name] - assert_equal 50000, wheres[:salary] + assert_equal "Jamis", wheres['name'] + assert_equal 50000, wheres['salary'] end def test_default_scope_with_module_includes wheres = ModuleIncludedPoorDeveloperCalledJamis.all.where_values_hash - assert_equal "Jamis", wheres[:name] - assert_equal 50000, wheres[:salary] + assert_equal "Jamis", wheres['name'] + assert_equal 50000, wheres['salary'] end def test_default_scope_with_multiple_calls wheres = MultiplePoorDeveloperCalledJamis.all.where_values_hash - assert_equal "Jamis", wheres[:name] - assert_equal 50000, wheres[:salary] + assert_equal "Jamis", wheres['name'] + assert_equal 50000, wheres['salary'] end def test_scope_overwrites_default - expected = Developer.all.merge!(:order => ' name DESC, salary DESC').to_a.collect { |dev| dev.name } + expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect { |dev| dev.name } received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name } assert_equal expected, received end @@ -419,7 +96,7 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_order_after_reorder_combines_orders - expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] } + expected = Developer.order('name DESC, id 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 @@ -445,17 +122,25 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_unscope_with_where_attributes - expected = Developer.order('salary DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect { |dev| dev.name } + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect(&:name) assert_equal expected, received - expected_2 = Developer.order('salary DESC').collect { |dev| dev.name } - received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect { |dev| dev.name } + expected_2 = Developer.order('salary DESC').collect(&:name) + received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect(&:name) assert_equal expected_2, received_2 - expected_3 = Developer.order('salary DESC').collect { |dev| dev.name } - received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect { |dev| dev.name } + expected_3 = Developer.order('salary DESC').collect(&:name) + received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect(&:name) assert_equal expected_3, received_3 + + expected_4 = Developer.order('salary DESC').collect(&:name) + received_4 = DeveloperOrderedBySalary.where.not("name" => "Jamis").unscope(where: :name).collect(&:name) + assert_equal expected_4, received_4 + + expected_5 = Developer.order('salary DESC').collect(&:name) + received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name) + assert_equal expected_5, received_5 end def test_unscope_multiple_where_clauses @@ -481,9 +166,8 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_order_to_unscope_reordering - expected = DeveloperOrderedBySalary.all.collect { |dev| [dev.name, dev.id] } - received = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order).collect { |dev| [dev.name, dev.id] } - assert_equal expected, received + scope = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order) + assert !(scope.to_sql =~ /order/i) end def test_unscope_reverse_order @@ -526,6 +210,16 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end + def test_unscope_and_scope + developer_klass = Class.new(Developer) do + scope :by_name, -> name { unscope(where: :name).where(name: name) } + end + + expected = developer_klass.where(name: 'Jamis').collect { |dev| [dev.name, dev.id] } + received = developer_klass.where(name: 'David').by_name('Jamis').collect { |dev| [dev.name, dev.id] } + assert_equal expected, received + end + def test_unscope_errors_with_invalid_value assert_raises(ArgumentError) do Developer.includes(:projects).where(name: "Jamis").unscope(:stupidly_incorrect_value) @@ -563,14 +257,14 @@ class DefaultScopingTest < ActiveRecord::TestCase Developer.select("id").unscope("select") end - assert_raises(ArgumentError) do + assert_raises(ArgumentError) do Developer.select("id").unscope(5) end end 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 } + expected = Developer.all.merge!(order: 'salary desc').to_a.collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect { |dev| dev.salary } assert_equal expected, received end @@ -634,7 +328,11 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis)) + + assert_equal 11, DeveloperCalledJamis.unscoped.length + assert_equal 1, DeveloperCalledJamis.poor.length assert_equal 10, DeveloperCalledJamis.unscoped.poor.length + assert_equal 10, DeveloperCalledJamis.unscoped { DeveloperCalledJamis.poor }.length end def test_default_scope_select_ignored_by_aggregations @@ -675,9 +373,11 @@ class DefaultScopingTest < ActiveRecord::TestCase threads << Thread.new do Thread.current[:long_default_scope] = true assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close end threads << Thread.new do assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close end threads.each(&:join) end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index b593270352..72c9787b84 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -6,7 +6,7 @@ require 'models/reply' require 'models/author' require 'models/developer' -class NamedScopeTest < ActiveRecord::TestCase +class NamedScopingTest < ActiveRecord::TestCase fixtures :posts, :authors, :topics, :comments, :author_addresses def test_implements_enumerable @@ -60,11 +60,6 @@ class NamedScopeTest < ActiveRecord::TestCase assert Topic.approved.respond_to?(:length) end - def test_respond_to_respects_include_private_parameter - assert !Topic.approved.respond_to?(:tables_in_string) - assert Topic.approved.respond_to?(:tables_in_string, true) - end - def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified assert !Topic.all.merge!(:where => {:approved => true}).to_a.empty? @@ -271,6 +266,19 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal 'lifo', topic.author_name end + # Method delegation for scope names which look like /\A[a-zA-Z_]\w*[!?]?\z/ + # has been done by evaluating a string with a plain def statement. For scope + # names which contain spaces this approach doesn't work. + def test_spaces_in_scope_names + klass = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + scope :"title containing space", -> { where("title LIKE '% %'") } + scope :approved, -> { where(:approved => true) } + end + assert_equal klass.send(:"title containing space"), klass.where("title LIKE '% %'") + assert_equal klass.approved.send(:"title containing space"), klass.approved.where("title LIKE '% %'") + end + def test_find_all_should_behave_like_select assert_equal Topic.base.to_a.select(&:approved), Topic.base.to_a.find_all(&:approved) end @@ -427,23 +435,17 @@ class NamedScopeTest < ActiveRecord::TestCase end end - def test_eager_scopes_are_deprecated + def test_eager_default_scope_relations_are_remove klass = Class.new(ActiveRecord::Base) klass.table_name = 'posts' - assert_deprecated do - klass.scope :welcome_2, klass.where(:id => posts(:welcome).id) + assert_raises(ArgumentError) do + klass.send(:default_scope, klass.where(:id => posts(:welcome).id)) end - assert_equal [posts(:welcome).title], klass.welcome_2.map(&:title) end - def test_eager_default_scope_relations_are_deprecated - klass = Class.new(ActiveRecord::Base) - klass.table_name = 'posts' - - assert_deprecated do - klass.send(:default_scope, klass.where(:id => posts(:welcome).id)) - end - assert_equal [posts(:welcome).title], klass.all.map(&:title) + def test_subclass_merges_scopes_properly + assert_equal 1, SpecialComment.where(body: 'go crazy').created.count end + end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb new file mode 100644 index 0000000000..0018fc06f2 --- /dev/null +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -0,0 +1,331 @@ +require "cases/helper" +require 'models/post' +require 'models/author' +require 'models/developer' +require 'models/project' +require 'models/comment' +require 'models/category' +require 'models/person' +require 'models/reference' + +class RelationScopingTest < ActiveRecord::TestCase + fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects + + def test_reverse_order + assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order + end + + def test_reverse_order_with_arel_node + assert_equal Developer.order("id DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).reverse_order + end + + def test_reverse_order_with_multiple_arel_nodes + assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).order(Developer.arel_table[:name].desc).reverse_order + end + + def test_reverse_order_with_arel_nodes_and_strings + assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order("id DESC").order(Developer.arel_table[:name].desc).reverse_order + end + + def test_double_reverse_order_produces_original_order + assert_equal Developer.order("name DESC"), Developer.order("name DESC").reverse_order.reverse_order + end + + def test_scoped_find + Developer.where("name = 'David'").scoping do + assert_nothing_raised { Developer.find(1) } + end + end + + def test_scoped_find_first + developer = Developer.find(10) + Developer.where("salary = 100000").scoping do + assert_equal developer, Developer.order("name").first + end + end + + def test_scoped_find_last + highest_salary = Developer.order("salary DESC").first + + Developer.order("salary").scoping do + assert_equal highest_salary, Developer.last + end + end + + def test_scoped_find_last_preserves_scope + 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 + assert_equal lowest_salary, Developer.first + end + end + + def test_scoped_find_combines_and_sanitizes_conditions + Developer.where("salary = 9000").scoping do + assert_equal developers(:poor_jamis), Developer.where("name = 'Jamis'").first + end + end + + def test_scoped_find_all + Developer.where("name = 'David'").scoping do + assert_equal [developers(:david)], Developer.all + end + end + + def test_scoped_find_select + Developer.select("id, name").scoping do + developer = Developer.where("name = 'David'").first + assert_equal "David", developer.name + assert !developer.has_attribute?(:salary) + end + end + + def test_scope_select_concatenates + Developer.select("id, name").scoping do + developer = Developer.select('salary').where("name = 'David'").first + 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.where("name = 'David'").scoping do + assert_equal 1, Developer.count + end + + Developer.where('salary = 100000').scoping do + assert_equal 8, Developer.count + assert_equal 1, Developer.where("name LIKE 'fixture_1%'").count + end + end + + 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).to_a + 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.joins('JOIN developers_projects ON id = developer_id').scoping do + Developer.where('developers_projects.project_id = 2').to_a + 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_create_with_where + new_comment = VerySpecialComment.where(:post_id => 1).scoping do + VerySpecialComment.create :body => "Wonderful world" + end + + assert_equal 1, new_comment.post_id + assert Post.find(1).comments.include?(new_comment) + end + + def test_scoped_create_with_create_with + new_comment = VerySpecialComment.create_with(:post_id => 1).scoping do + VerySpecialComment.create :body => "Wonderful world" + end + + assert_equal 1, new_comment.post_id + assert Post.find(1).comments.include?(new_comment) + end + + def test_scoped_create_with_create_with_has_higher_priority + new_comment = VerySpecialComment.where(:post_id => 2).create_with(:post_id => 1).scoping do + VerySpecialComment.create :body => "Wonderful world" + end + + assert_equal 1, new_comment.post_id + assert Post.find(1).comments.include?(new_comment) + end + + def test_ensure_that_method_scoping_is_correctly_restored + begin + Developer.where("name = 'Jamis'").scoping do + raise "an exception" + end + rescue + end + + assert !Developer.all.where_values.include?("name = 'Jamis'") + end + + def test_default_scope_filters_on_joins + assert_equal 1, DeveloperFilteredOnJoins.all.count + assert_equal DeveloperFilteredOnJoins.all.first, developers(:david).becomes(DeveloperFilteredOnJoins) + end + + def test_update_all_default_scope_filters_on_joins + DeveloperFilteredOnJoins.update_all(:salary => 65000) + assert_equal 65000, Developer.find(developers(:david).id).salary + + # has not changed jamis + assert_not_equal 65000, Developer.find(developers(:jamis).id).salary + end + + def test_delete_all_default_scope_filters_on_joins + assert_not_equal [], DeveloperFilteredOnJoins.all + + DeveloperFilteredOnJoins.delete_all() + + assert_equal [], DeveloperFilteredOnJoins.all + assert_not_equal [], Developer.all + end +end + +class NestedRelationScopingTest < ActiveRecord::TestCase + fixtures :authors, :developers, :projects, :comments, :posts + + def test_merge_options + Developer.where('salary = 80000').scoping do + Developer.limit(10).scoping do + devs = Developer.all + assert_match '(salary = 80000)', devs.to_sql + assert_equal 10, devs.taken + end + end + end + + def test_merge_inner_scope_has_priority + Developer.limit(5).scoping do + Developer.limit(10).scoping do + assert_equal 10, Developer.all.size + end + end + end + + def test_replace_options + Developer.where(:name => 'David').scoping do + Developer.unscoped do + assert_equal 'Jamis', Developer.where(:name => 'Jamis').first[:name] + end + + assert_equal 'David', Developer.first[:name] + end + end + + def test_three_level_nested_exclusive_scoped_find + Developer.where("name = 'Jamis'").scoping do + assert_equal 'Jamis', Developer.first.name + + Developer.unscoped.where("name = 'David'") do + assert_equal 'David', Developer.first.name + + Developer.unscoped.where("name = 'Maiha'") do + assert_equal nil, Developer.first + end + + # ensure that scoping is restored + assert_equal 'David', Developer.first.name + end + + # ensure that scoping is restored + assert_equal 'Jamis', Developer.first.name + end + end + + def test_nested_scoped_create + comment = Comment.create_with(:post_id => 1).scoping do + Comment.create_with(:post_id => 2).scoping do + 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 = Comment.create_with(:body => "Hey guys, nested scopes are broken. Please fix!").scoping do + Comment.unscoped.create_with(:post_id => 1).scoping do + assert Comment.new.body.blank? + Comment.create :body => "Hey guys" + end + end + + assert_equal 1, comment.post_id + assert_equal 'Hey guys', comment.body + end +end + +class HasManyScopingTest< ActiveRecord::TestCase + fixtures :comments, :posts, :people, :references + + def setup + @welcome = Post.find(1) + end + + def test_forwarding_of_static_methods + assert_equal 'a comment...', Comment.what_are_you + assert_equal 'a comment...', @welcome.comments.what_are_you + end + + def test_forwarding_to_scoped + assert_equal 4, Comment.search_by_type('Comment').size + assert_equal 2, @welcome.comments.search_by_type('Comment').size + end + + def test_nested_scope_finder + Comment.where('1=0').scoping do + assert_equal 0, @welcome.comments.count + assert_equal 'a comment...', @welcome.comments.what_are_you + end + + Comment.where('1=1').scoping do + assert_equal 2, @welcome.comments.count + assert_equal 'a comment...', @welcome.comments.what_are_you + end + end + + def test_should_maintain_default_scope_on_associations + magician = BadReference.find(1) + assert_equal [magician], people(:michael).bad_references + end + + def test_should_default_scope_on_associations_is_overridden_by_association_conditions + reference = references(:michael_unicyclist).becomes(BadReference) + assert_equal [reference], people(:michael).fixed_bad_references + end + + def test_should_maintain_default_scope_on_eager_loaded_associations + michael = Person.where(:id => people(:michael).id).includes(:bad_references).first + magician = BadReference.find(1) + assert_equal [magician], michael.bad_references + end +end + +class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase + fixtures :posts, :categories, :categories_posts + + def setup + @welcome = Post.find(1) + end + + def test_forwarding_of_static_methods + assert_equal 'a category...', Category.what_are_you + assert_equal 'a category...', @welcome.categories.what_are_you + end + + def test_nested_scope_finder + Category.where('1=0').scoping do + assert_equal 0, @welcome.categories.count + assert_equal 'a category...', @welcome.categories.what_are_you + end + + Category.where('1=1').scoping do + assert_equal 2, @welcome.categories.count + assert_equal 'a category...', @welcome.categories.what_are_you + end + end +end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 726338db14..bc67da8d27 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' require 'models/topic' +require 'models/reply' require 'models/person' require 'models/traffic_light' require 'bcrypt' @@ -18,12 +19,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase 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) @@ -216,16 +211,15 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_serialize_attribute_via_select_method_when_time_zone_available - ActiveRecord::Base.time_zone_aware_attributes = true - Topic.serialize(:content, MyObject) + with_timezone_config aware_attributes: true do + Topic.serialize(:content, MyObject) - myobj = MyObject.new('value1', 'value2') - topic = Topic.create(content: myobj) + myobj = MyObject.new('value1', 'value2') + topic = Topic.create(content: myobj) - assert_equal(myobj, Topic.select(:content).find(topic.id).content) - assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content } - ensure - ActiveRecord::Base.time_zone_aware_attributes = false + assert_equal(myobj, Topic.select(:content).find(topic.id).content) + assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content } + end end def test_serialize_attribute_can_be_serialized_in_an_integer_column @@ -241,4 +235,14 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal [], light.state assert_equal [], light.long_state end + + def test_serialized_column_should_not_be_wrapped_twice + Topic.serialize(:content, MyObject) + + myobj = MyObject.new('value1', 'value2') + Topic.create(content: myobj) + Topic.create(content: myobj) + type = Topic.column_types["content"] + assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type) + end end diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb new file mode 100644 index 0000000000..76da49707f --- /dev/null +++ b/activerecord/test/cases/statement_cache_test.rb @@ -0,0 +1,64 @@ +require 'cases/helper' +require 'models/book' +require 'models/liquid' +require 'models/molecule' +require 'models/electron' + +module ActiveRecord + class StatementCacheTest < ActiveRecord::TestCase + def setup + @connection = ActiveRecord::Base.connection + end + + def test_statement_cache_with_simple_statement + cache = ActiveRecord::StatementCache.new do + Book.where(name: "my book").where("author_id > 3") + end + + Book.create(name: "my book", author_id: 4) + + books = cache.execute + assert_equal "my book", books[0].name + end + + def test_statement_cache_with_nil_statement_raises_error + assert_raise(ArgumentError) do + ActiveRecord::StatementCache.new do + nil + end + end + end + + def test_statement_cache_with_complex_statement + cache = ActiveRecord::StatementCache.new do + Liquid.joins(:molecules => :electrons).where('molecules.name' => 'dioxane', 'electrons.name' => 'lepton') + end + + salty = Liquid.create(name: 'salty') + molecule = salty.molecules.create(name: 'dioxane') + molecule.electrons.create(name: 'lepton') + + liquids = cache.execute + assert_equal "salty", liquids[0].name + end + + def test_statement_cache_values_differ + cache = ActiveRecord::StatementCache.new do + Book.where(name: "my book") + end + + 3.times do + Book.create(name: "my book") + end + + first_books = cache.execute + + 3.times do + Book.create(name: "my book") + end + + additional_books = cache.execute + assert first_books != additional_books + end + end +end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 3e32d866ee..0c9f7ccd55 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -151,8 +151,15 @@ class StoreTest < ActiveRecord::TestCase 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 } + test "stored_attributes are tracked per class" do + first_model = Class.new(ActiveRecord::Base) do + store_accessor :data, :color + end + second_model = Class.new(ActiveRecord::Base) do + store_accessor :data, :width, :height + end + + assert_equal [:color], first_model.stored_attributes[:data] + assert_equal [:width, :height], second_model.stored_attributes[:data] end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 3bfbc92afd..e9000fef25 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -305,4 +305,11 @@ module ActiveRecord end end end + + class DatabaseTasksCheckSchemaFileTest < ActiveRecord::TestCase + def test_check_schema_file + Kernel.expects(:abort).with(regexp_matches(/awesome-file.sql/)) + ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql") + end + end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index dadcca5b7f..bdcf31043a 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -71,7 +71,7 @@ module ActiveRecord return skip("only tested on mysql") end - @connection = stub(:create_database => true, :execute => true) + @connection = stub("Connection", create_database: true) @error = Mysql::Error.new "Invalid permissions" @configuration = { 'adapter' => 'mysql', @@ -90,6 +90,7 @@ module ActiveRecord end def test_root_password_is_requested + assert_permissions_granted_for "pat" skip "only if mysql is available" unless defined?(::Mysql) $stdin.expects(:gets).returns("secret\n") @@ -97,6 +98,7 @@ module ActiveRecord end def test_connection_established_as_root + assert_permissions_granted_for "pat" ActiveRecord::Base.expects(:establish_connection).with( 'adapter' => 'mysql', 'database' => nil, @@ -108,6 +110,7 @@ module ActiveRecord end def test_database_created_by_root + assert_permissions_granted_for "pat" @connection.expects(:create_database). with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') @@ -115,12 +118,18 @@ module ActiveRecord 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;") + assert_permissions_granted_for "pat" + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + def test_do_not_grant_privileges_for_root_user + @configuration['username'] = 'root' + @configuration['password'] = '' ActiveRecord::Tasks::DatabaseTasks.create @configuration end def test_connection_established_as_normal_user + assert_permissions_granted_for "pat" ActiveRecord::Base.expects(:establish_connection).returns do ActiveRecord::Base.expects(:establish_connection).with( 'adapter' => 'mysql', @@ -142,6 +151,13 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.create @configuration end + + private + def assert_permissions_granted_for(db_user) + db_name = @configuration['database'] + db_password = @configuration['password'] + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") + end end class MySQLDBDropTest < ActiveRecord::TestCase @@ -264,6 +280,15 @@ module ActiveRecord assert_match(/Could not dump the database structure/, warnings) end + + def test_structure_dump_with_port_number + filename = "awesome-file.sql" + Kernel.expects(:system).with("mysqldump", "--port", "10000", "--result-file", filename, "--no-data", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump( + @configuration.merge('port' => 10000), + filename) + end end class MySQLStructureLoadTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 3006a87589..6ea225178f 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -206,7 +206,7 @@ module ActiveRecord @connection.expects(:schema_search_path).returns("foo") ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - assert File.exists?(filename) + assert File.exist?(filename) ensure FileUtils.rm(filename) end @@ -225,9 +225,16 @@ module ActiveRecord Kernel.stubs(:system) end - def test_structure_dump + def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with("psql -f #{filename} my-app-db") + Kernel.expects(:system).with("psql -q -f #{filename} my-app-db") + + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + + def test_structure_load_accepts_path_with_spaces + filename = "awesome file.sql" + Kernel.expects(:system).with("psql -q -f awesome\\ file.sql my-app-db") ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 7209c0f14d..da3471adf9 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -159,8 +159,8 @@ module ActiveRecord filename = "awesome-file.sql" ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, '/rails/root' - assert File.exists?(dbfile) - assert File.exists?(filename) + assert File.exist?(dbfile) + assert File.exist?(filename) ensure FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) @@ -182,7 +182,7 @@ module ActiveRecord open(filename, 'w') { |f| f.puts("select datetime('now', 'localtime');") } ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, '/rails/root' - assert File.exists?(dbfile) + assert File.exist?(dbfile) ensure FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index f3f7054794..8c6d189b0c 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -1,9 +1,108 @@ -ActiveSupport::Deprecation.silence do - require 'active_record/test_case' -end +require 'active_support/test_case' + +module ActiveRecord + # = Active Record Test Case + # + # Defines some test assertions to test against SQL queries. + class TestCase < ActiveSupport::TestCase #:nodoc: + def teardown + SQLCounter.clear_log + end + + def assert_date_from_db(expected, actual, message = nil) + # SybaseAdapter doesn't have a separate column type just for dates, + # so the time is in the string and incorrectly formatted + if current_adapter?(:SybaseAdapter) + assert_equal expected.to_s, actual.to_date.to_s, message + else + assert_equal expected.to_s, actual.to_s, message + end + end + + def assert_sql(*patterns_to_match) + SQLCounter.clear_log + yield + SQLCounter.log_all + ensure + failed_patterns = [] + patterns_to_match.each do |pattern| + 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, options = {}) + ignore_none = options.fetch(:ignore_none) { num == :any } + SQLCounter.clear_log + x = yield + 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 + x + end + + def assert_no_queries(options = {}, &block) + options.reverse_merge! ignore_none: true + assert_queries(0, options, &block) + end + + def assert_column(model, column_name, msg=nil) + assert has_column?(model, column_name), msg + end + + def assert_no_column(model, column_name, msg=nil) + assert_not has_column?(model, column_name), msg + end + + def has_column?(model, column_name) + model.reset_column_information + model.column_names.include?(column_name.to_s) + end + end -ActiveRecord::TestCase.class_eval do - def sqlite3? connection - connection.class.name.split('::').last == "SQLite3Adapter" + class SQLCounter + class << self + attr_accessor :ignored_sql, :log, :log_all + def clear_log; self.log = []; self.log_all = []; end + end + + self.clear_log + + self.ignored_sql = [/^PRAGMA/, /^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, 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, /^SHOW search_path/i] + sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] + + [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| + ignored_sql.concat db_ignored_sql + end + + attr_reader :ignore + + def initialize(ignore = Regexp.union(self.class.ignored_sql)) + @ignore = ignore + end + + def call(name, start, finish, message_id, values) + sql = values[:sql] + + # FIXME: this seems bad. we should probably have a better way to indicate + # the query was cached + return if 'CACHE' == values[:name] + + self.class.log_all << sql + self.class.log << sql unless ignore =~ sql + end end + + ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 777a2b70dd..ff1b01556d 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -176,6 +176,106 @@ class TimestampTest < ActiveRecord::TestCase assert_not_equal time, owner.updated_at end + def test_touching_a_record_touches_polymorphic_record + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Toy'; end + end + + wheel_klass = Class.new(ActiveRecord::Base) do + def self.name; 'Wheel'; end + belongs_to :wheelable, :polymorphic => true, :touch => true + end + + toy = klass.first + time = 3.days.ago + toy.update_columns(updated_at: time) + + wheel = wheel_klass.new + wheel.wheelable = toy + wheel.save + wheel.touch + + assert_not_equal time, toy.updated_at + end + + def test_changing_parent_of_a_record_touches_both_new_and_old_parent_record + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Toy'; end + belongs_to :pet, touch: true + end + + toy1 = klass.find(1) + old_pet = toy1.pet + + toy2 = klass.find(2) + new_pet = toy2.pet + time = 3.days.ago.at_beginning_of_hour + + old_pet.update_columns(updated_at: time) + new_pet.update_columns(updated_at: time) + + toy1.pet = new_pet + toy1.save! + + old_pet.reload + new_pet.reload + + assert_not_equal time, new_pet.updated_at + assert_not_equal time, old_pet.updated_at + end + + def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Toy'; end + end + + wheel_klass = Class.new(ActiveRecord::Base) do + def self.name; 'Wheel'; end + belongs_to :wheelable, :polymorphic => true, :touch => true + end + + toy1 = klass.find(1) + toy2 = klass.find(2) + + wheel = wheel_klass.new + wheel.wheelable = toy1 + wheel.save! + + time = 3.days.ago.at_beginning_of_hour + + toy1.update_columns(updated_at: time) + toy2.update_columns(updated_at: time) + + wheel.wheelable = toy2 + wheel.save! + + toy1.reload + toy2.reload + + assert_not_equal time, toy1.updated_at + assert_not_equal time, toy2.updated_at + end + + def test_clearing_association_touches_the_old_record + klass = Class.new(ActiveRecord::Base) do + def self.name; 'Toy'; end + belongs_to :pet, touch: true + end + + toy = klass.find(1) + pet = toy.pet + time = 3.days.ago.at_beginning_of_hour + + pet.update_columns(updated_at: time) + + toy.pet = nil + toy.save! + + pet.reload + + assert_not_equal time, pet.updated_at + end + def test_timestamp_attributes_for_create toy = Toy.first assert_equal toy.send(:timestamp_attributes_for_create), [:created_at, :created_on] diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 766a5c0c90..5644a35385 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -182,9 +182,9 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_call_after_rollback_when_commit_fails - @first.class.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction) + @first.class.connection.singleton_class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction) begin - @first.class.connection.class.class_eval do + @first.class.connection.singleton_class.class_eval do def commit_db_transaction; raise "boom!"; end end @@ -194,8 +194,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert !@first.save rescue nil assert_equal [:after_rollback], @first.history ensure - @first.class.connection.class.send(:remove_method, :commit_db_transaction) - @first.class.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction) + @first.class.connection.singleton_class.send(:remove_method, :commit_db_transaction) + @first.class.connection.singleton_class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction) end end @@ -281,38 +281,6 @@ class TransactionCallbacksTest < ActiveRecord::TestCase 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 - class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase self.use_transactional_fixtures = false diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 6d66342fa5..980981903a 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -117,6 +117,20 @@ class TransactionTest < ActiveRecord::TestCase assert !Topic.find(1).approved? end + def test_raising_exception_in_nested_transaction_restore_state_in_save + topic = Topic.new + + def topic.after_save_for_transaction + raise 'Make the transaction rollback' + end + + assert_raises(RuntimeError) do + Topic.transaction { topic.save } + end + + assert topic.new_record?, "#{topic.inspect} should be new record" + end + def test_update_should_rollback_on_failure author = Author.find(1) posts_count = author.posts.size @@ -361,6 +375,36 @@ class TransactionTest < ActiveRecord::TestCase assert_equal "Three", @three end if Topic.connection.supports_savepoints? + def test_using_named_savepoints + Topic.transaction do + @first.approved = true + @first.save! + Topic.connection.create_savepoint("first") + + @first.approved = false + @first.save! + Topic.connection.rollback_to_savepoint("first") + assert @first.reload.approved? + + @first.approved = false + @first.save! + Topic.connection.release_savepoint("first") + assert_not @first.reload.approved? + end + end if Topic.connection.supports_savepoints? + + def test_releasing_named_savepoints + Topic.transaction do + Topic.connection.create_savepoint("another") + Topic.connection.release_savepoint("another") + + # The savepoint is now gone and we can't remove it again. + assert_raises(ActiveRecord::StatementInvalid) do + Topic.connection.release_savepoint("another") + end + end + end + def test_rollback_when_commit_raises Topic.connection.expects(:begin_db_transaction) Topic.connection.expects(:commit_db_transaction).raises('OH NOES') @@ -377,7 +421,9 @@ class TransactionTest < ActiveRecord::TestCase topic = Topic.new(:title => 'test') topic.freeze e = assert_raise(RuntimeError) { topic.save } - assert_equal "can't modify frozen Hash", e.message + assert_match(/frozen/i, e.message) # Not good enough, but we can't do much + # about it since there is no specific error + # for frozen objects. assert !topic.persisted?, 'not persisted' assert_nil topic.id assert topic.frozen?, 'not frozen' @@ -410,16 +456,6 @@ class TransactionTest < ActiveRecord::TestCase assert !@second.destroyed?, 'not destroyed' end - if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE) - def test_outside_transaction_works - assert assert_deprecated { Topic.connection.outside_transaction? } - Topic.connection.begin_db_transaction - assert assert_deprecated { !Topic.connection.outside_transaction? } - Topic.connection.rollback_db_transaction - assert assert_deprecated { Topic.connection.outside_transaction? } - end - end - def test_sqlite_add_column_in_transaction return true unless current_adapter?(:SQLite3Adapter) @@ -536,22 +572,22 @@ if current_adapter?(:PostgreSQLAdapter) # This will cause transactions to overlap and fail unless they are performed on # separate database connections. def test_transaction_per_thread - assert_nothing_raised do - threads = (1..3).map do - Thread.new do - Topic.transaction do - topic = Topic.find(1) - topic.approved = !topic.approved? - topic.save! - topic.approved = !topic.approved? - topic.save! - end - Topic.connection.close + skip "in memory db can't share a db between threads" if in_memory_db? + + threads = 3.times.map do + Thread.new do + Topic.transaction do + topic = Topic.find(1) + topic.approved = !topic.approved? + assert topic.save! + topic.approved = !topic.approved? + assert topic.save! end + Topic.connection.close end - - threads.each { |t| t.join } end + + threads.each { |t| t.join } end # Test for dirty reads among simultaneous transactions. @@ -603,14 +639,5 @@ if current_adapter?(:PostgreSQLAdapter) assert_equal original_salary, Developer.find(1).salary end - - test "#transaction_joinable= is deprecated" do - Developer.transaction do - conn = Developer.connection - assert conn.current_transaction.joinable? - assert_deprecated { conn.transaction_joinable = false } - assert !conn.current_transaction.joinable? - end - end end end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index 7ac34bc71e..602f633c45 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -10,29 +10,33 @@ require 'models/interest' class AssociationValidationTest < ActiveRecord::TestCase fixtures :topics, :owners - repair_validations(Topic, Reply, Owner) + repair_validations(Topic, Reply) def test_validates_size_of_association - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors[:pets].any? - o.pets.build('name' => 'apet') - assert o.valid? + repair_validations Owner do + assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } + o = Owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? + o.pets.build('name' => 'apet') + assert o.valid? + end end def test_validates_size_of_association_using_within - assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 } - o = Owner.new('name' => 'nopets') - assert !o.save - assert o.errors[:pets].any? - - o.pets.build('name' => 'apet') - assert o.valid? - - 2.times { o.pets.build('name' => 'apet') } - assert !o.save - assert o.errors[:pets].any? + repair_validations Owner do + assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 } + o = Owner.new('name' => 'nopets') + assert !o.save + assert o.errors[:pets].any? + + o.pets.build('name' => 'apet') + assert o.valid? + + 2.times { o.pets.build('name' => 'apet') } + assert !o.save + assert o.errors[:pets].any? + end end def test_validates_associated_many @@ -91,12 +95,14 @@ class AssociationValidationTest < ActiveRecord::TestCase end def test_validates_size_of_association_utf8 - assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } - o = Owner.new('name' => 'あいうえおかきくけこ') - assert !o.save - assert o.errors[:pets].any? - o.pets.build('name' => 'あいうえおかきくけこ') - assert o.valid? + repair_validations Owner do + assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 } + o = Owner.new('name' => 'あいうえおかきくけこ') + assert !o.save + assert o.errors[:pets].any? + o.pets.build('name' => 'あいうえおかきくけこ') + assert o.valid? + end end def test_validates_presence_of_belongs_to_association__parent_is_new_record @@ -118,21 +124,4 @@ class AssociationValidationTest < ActiveRecord::TestCase end end - def test_validates_associated_models_in_the_same_context - Topic.validates_presence_of :title, :on => :custom_context - Topic.validates_associated :replies - Reply.validates_presence_of :title, :on => :custom_context - - t = Topic.new('title' => '') - r = t.replies.new('title' => '') - - assert t.valid? - assert !t.valid?(:custom_context) - - t.title = "Longer" - assert !t.valid?(:custom_context), "Should NOT be valid if the associated object is not valid in the same context." - - r.title = "Longer" - assert t.valid?(:custom_context), "Should be valid if the associated object is not valid in the same context." - end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 29b45944aa..2b33f01783 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -54,7 +54,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert !t2.save, "Shouldn't save t2 as unique" assert_equal ["has already been taken"], t2.errors[:title] - t2.title = "Now Im really also unique" + t2.title = "Now I am really also unique" assert t2.save, "Should now save t2 as unique" end @@ -268,7 +268,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase end def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer - Topic.validates_uniqueness_of(:title, :case_sensitve => true) + Topic.validates_uniqueness_of(:title, :case_sensitive => true) Topic.create!('title' => 101) t2 = Topic.new('title' => 101) diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb index 11912ca1cc..c02b3241cd 100644 --- a/activerecord/test/cases/validations_repair_helper.rb +++ b/activerecord/test/cases/validations_repair_helper.rb @@ -6,7 +6,7 @@ module ActiveRecord def repair_validations(*model_classes) teardown do model_classes.each do |k| - k.reset_callbacks(:validate) + k.clear_validators! end end end @@ -16,7 +16,7 @@ module ActiveRecord yield ensure model_classes.each do |k| - k.reset_callbacks(:validate) + k.clear_validators! end end end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 68fa15de50..78fa2f935a 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -161,21 +161,17 @@ end class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase def test_should_serialize_datetime_with_timezone - 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 - ensure - Time.zone = timezone + with_timezone_config zone: "Pacific Time (US & Canada)" do + 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 + end end def test_should_serialize_datetime_with_timezone_reloaded - 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 - ensure - Time.zone = timezone + with_timezone_config zone: "Pacific Time (US & Canada)" do + 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 + end end end diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 302913e095..83a710b1b7 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -5,16 +5,10 @@ class YamlSerializationTest < ActiveRecord::TestCase fixtures :topics def test_to_yaml_with_time_with_zone_should_not_raise_exception - tz = Time.zone - Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"] - ActiveRecord::Base.time_zone_aware_attributes = true - - topic = Topic.new(:written_on => DateTime.now) - assert_nothing_raised { topic.to_yaml } - - ensure - Time.zone = tz - ActiveRecord::Base.time_zone_aware_attributes = false + with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do + topic = Topic.new(:written_on => DateTime.now) + assert_nothing_raised { topic.to_yaml } + end end def test_roundtrip diff --git a/activerecord/test/fixtures/all/admin b/activerecord/test/fixtures/all/admin new file mode 120000 index 0000000000..984d12a043 --- /dev/null +++ b/activerecord/test/fixtures/all/admin @@ -0,0 +1 @@ +../to_be_linked/
\ No newline at end of file diff --git a/activerecord/test/fixtures/dog_lovers.yml b/activerecord/test/fixtures/dog_lovers.yml index d3e5e4a1aa..3f4c6c9e4c 100644 --- a/activerecord/test/fixtures/dog_lovers.yml +++ b/activerecord/test/fixtures/dog_lovers.yml @@ -2,3 +2,6 @@ david: id: 1 bred_dogs_count: 0 trained_dogs_count: 1 +joanna: + id: 2 + dogs_count: 1 diff --git a/activerecord/test/fixtures/dogs.yml b/activerecord/test/fixtures/dogs.yml index 16d19be2c5..b5eb2c7b74 100644 --- a/activerecord/test/fixtures/dogs.yml +++ b/activerecord/test/fixtures/dogs.yml @@ -1,3 +1,4 @@ sophie: id: 1 trainer_id: 1 + dog_lover_id: 2 diff --git a/activerecord/test/fixtures/friendships.yml b/activerecord/test/fixtures/friendships.yml index 1ee09175bf..ae0abe0162 100644 --- a/activerecord/test/fixtures/friendships.yml +++ b/activerecord/test/fixtures/friendships.yml @@ -1,4 +1,4 @@ Connection 1: id: 1 - person_id: 1 - friend_id: 2
\ No newline at end of file + friend_id: 1 + follower_id: 2 diff --git a/activerecord/test/fixtures/people.yml b/activerecord/test/fixtures/people.yml index e640a38f1f..0ec05e8d56 100644 --- a/activerecord/test/fixtures/people.yml +++ b/activerecord/test/fixtures/people.yml @@ -5,6 +5,7 @@ michael: number1_fan_id: 3 gender: M followers_count: 1 + friends_too_count: 1 david: id: 2 first_name: David @@ -12,6 +13,7 @@ david: number1_fan_id: 1 gender: M followers_count: 1 + friends_too_count: 1 susan: id: 3 first_name: Susan @@ -19,3 +21,4 @@ susan: number1_fan_id: 1 gender: F followers_count: 1 + friends_too_count: 1 diff --git a/activerecord/test/fixtures/pets.yml b/activerecord/test/fixtures/pets.yml index a1601a53f0..2ec4f53e6d 100644 --- a/activerecord/test/fixtures/pets.yml +++ b/activerecord/test/fixtures/pets.yml @@ -12,3 +12,8 @@ mochi: pet_id: 3 name: mochi owner_id: 2 + +bulbul: + pet_id: 4 + name: bulbul + owner_id: 1 diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml index bfc6b238b1..2da541c539 100644 --- a/activerecord/test/fixtures/sponsors.yml +++ b/activerecord/test/fixtures/sponsors.yml @@ -8,5 +8,5 @@ boring_club_sponsor_for_groucho: sponsorable_type: Member crazy_club_sponsor_for_groucho: sponsor_club: crazy_club - sponsorable_id: 2 + sponsorable_id: 3 sponsorable_type: Member diff --git a/activerecord/test/fixtures/tasks.yml b/activerecord/test/fixtures/tasks.yml index 402ca85faf..c38b32b0e5 100644 --- a/activerecord/test/fixtures/tasks.yml +++ b/activerecord/test/fixtures/tasks.yml @@ -1,4 +1,4 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html first_task: id: 1 starting: 2005-03-30t06:30:00.00+01:00 diff --git a/activerecord/test/fixtures/to_be_linked/accounts.yml b/activerecord/test/fixtures/to_be_linked/accounts.yml new file mode 100644 index 0000000000..9e341a15af --- /dev/null +++ b/activerecord/test/fixtures/to_be_linked/accounts.yml @@ -0,0 +1,2 @@ +signals37: + name: 37signals diff --git a/activerecord/test/fixtures/to_be_linked/users.yml b/activerecord/test/fixtures/to_be_linked/users.yml new file mode 100644 index 0000000000..e2884beda5 --- /dev/null +++ b/activerecord/test/fixtures/to_be_linked/users.yml @@ -0,0 +1,10 @@ +david: + name: David + account: signals37 + +jamis: + name: Jamis + account: signals37 + settings: + :symbol: symbol + string: string diff --git a/activerecord/test/fixtures/toys.yml b/activerecord/test/fixtures/toys.yml index 037e335e0a..ae9044ec62 100644 --- a/activerecord/test/fixtures/toys.yml +++ b/activerecord/test/fixtures/toys.yml @@ -2,3 +2,13 @@ bone: toy_id: 1 name: Bone pet_id: 1 + +doll: + toy_id: 2 + name: Doll + pet_id: 2 + +bulbuli: + toy_id: 3 + name: Bulbuli + pet_id: 4 diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 8423411474..794d1af43d 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -8,12 +8,14 @@ class Author < ActiveRecord::Base 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_special_categorizations, :class_name => 'PostWithSpecialCategorization' - has_many :posts_with_extension, :class_name => "Post" 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, through: :posts do + def ratings + Rating.joins(:comment).merge(self) + end + end has_many :comments_containing_the_letter_e, :through => :posts, :source => :comments 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 @@ -27,11 +29,17 @@ class Author < ActiveRecord::Base 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 :welcome_posts_with_comment, + -> { where(title: 'Welcome to the weblog').where('comments_count = ?', 1) }, + class_name: 'Post' + has_many :welcome_posts_with_comments, + -> { where(title: 'Welcome to the weblog').where(Post.arel_table[:comments_count].gt(0)) }, + class_name: 'Post' + 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, -> { 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 :ordered_uniq_comments, -> { distinct.order('comments.id') }, :through => :posts, :source => :comments + has_many :ordered_uniq_comments_desc, -> { distinct.order('comments.id DESC') }, :through => :posts, :source => :comments has_many :readonly_comments, -> { readonly }, :through => :posts, :source => :comments has_many :special_posts @@ -78,20 +86,20 @@ class Author < ActiveRecord::Base 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, -> { uniq }, :through => :categorizations, :source => :post + has_many :unique_categorized_posts, -> { distinct }, :through => :categorizations, :source => :post has_many :nothings, :through => :kateggorisatons, :class_name => 'Category' has_many :author_favorites has_many :favorite_authors, -> { order('name') }, :through => :author_favorites - has_many :taggings, :through => :posts + has_many :taggings, :through => :posts, :source => :taggings has_many :taggings_2, :through => :posts, :source => :tagging has_many :tags, :through => :posts 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 :similar_posts, -> { distinct }, :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 diff --git a/activerecord/test/models/auto_id.rb b/activerecord/test/models/auto_id.rb index d720e2be5e..82c6544bd5 100644 --- a/activerecord/test/models/auto_id.rb +++ b/activerecord/test/models/auto_id.rb @@ -1,4 +1,4 @@ class AutoId < ActiveRecord::Base - def self.table_name () "auto_id_tests" end - def self.primary_key () "auto_id" end + self.table_name = "auto_id_tests" + self.primary_key = "auto_id" end diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index ce81a37966..5458a28cc9 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, -> { uniq }, :through => :citations, :source => :reference_of + has_many :references, -> { distinct }, :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 0109ef4f83..4361188e21 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -37,3 +37,9 @@ class CustomBulb < Bulb self.frickinawesome = true if name == 'Dude' end end + +class FunkyBulb < Bulb + before_destroy do + raise "before_destroy was called" + end +end diff --git a/activerecord/test/models/cake_designer.rb b/activerecord/test/models/cake_designer.rb new file mode 100644 index 0000000000..9c57ef573a --- /dev/null +++ b/activerecord/test/models/cake_designer.rb @@ -0,0 +1,3 @@ +class CakeDesigner < ActiveRecord::Base + has_one :chef, as: :employable +end diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index ac42f444e1..6d257dbe7e 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -1,11 +1,9 @@ class Car < ActiveRecord::Base - has_many :bulbs + has_many :funky_bulbs, class_name: 'FunkyBulb', dependent: :destroy 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, -> { where :frickinawesome => true }, :class_name => "Bulb" has_many :tyres has_many :engines, :dependent => :destroy diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb new file mode 100644 index 0000000000..67a4e54f06 --- /dev/null +++ b/activerecord/test/models/chef.rb @@ -0,0 +1,3 @@ +class Chef < ActiveRecord::Base + belongs_to :employable, polymorphic: true +end diff --git a/activerecord/test/models/citation.rb b/activerecord/test/models/citation.rb index 545aa8110d..3d87eb795c 100644 --- a/activerecord/test/models/citation.rb +++ b/activerecord/test/models/citation.rb @@ -1,6 +1,3 @@ class Citation < ActiveRecord::Base belongs_to :reference_of, :class_name => "Book", :foreign_key => :book2_id - - belongs_to :book1, :class_name => "Book", :foreign_key => :book1_id - belongs_to :book2, :class_name => "Book", :foreign_key => :book2_id end diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index 24a65b0f2f..566e0873f1 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -1,8 +1,7 @@ class Club < ActiveRecord::Base has_one :membership - has_many :memberships + has_many :memberships, :inverse_of => false has_many :members, :through => :memberships - has_many :current_memberships has_one :sponsor has_one :sponsored_member, :through => :sponsor, :source => :sponsorable, :source_type => "Member" belongs_to :category diff --git a/activerecord/test/models/column_name.rb b/activerecord/test/models/column_name.rb index ec07205a3a..460eb4fe20 100644 --- a/activerecord/test/models/column_name.rb +++ b/activerecord/test/models/column_name.rb @@ -1,3 +1,3 @@ class ColumnName < ActiveRecord::Base - def self.table_name () "colnametests" end -end
\ No newline at end of file + self.table_name = "colnametests" +end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 3ca8f69646..0b0b304121 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -11,6 +11,11 @@ class Company < AbstractCompany has_many :contracts has_many :developers, :through => :contracts + scope :of_first_firm, lambda { + joins(:account => :firm). + where('firms.id' => 1) + } + def arbitrary_method "I am Jack's profound disappointment" end @@ -35,17 +40,11 @@ module Namespaced end class Firm < Company - 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 :clients, -> { order "id" }, :dependent => :destroy, :before_remove => :log_before_remove, :after_remove => :log_after_remove has_many :unsorted_clients, :class_name => "Client" has_many :unsorted_clients_with_symbol, :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_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :inverse_of => :firm 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, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :destroy @@ -54,21 +53,7 @@ class Firm < Company 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, -> { 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', @@ -114,13 +99,6 @@ class DependentFirm < Company has_one :company, :foreign_key => 'client_of', :dependent => :nullify end -class RestrictedFirm < Company - 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 @@ -141,9 +119,13 @@ class Client < Company belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name 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 + has_many :accounts, :through => :firm, :source => :accounts belongs_to :account + validate do + firm + end + class RaisedOnSave < RuntimeError; end attr_accessor :raise_on_save before_save do @@ -193,7 +175,6 @@ class ExclusivelyDependentFirm < Company has_one :account, :foreign_key => "firm_id", :dependent => :delete 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 @@ -206,6 +187,8 @@ class Account < ActiveRecord::Base belongs_to :firm, :class_name => 'Company' belongs_to :unautosaved_firm, :foreign_key => "firm_id", :class_name => "Firm", :autosave => false + alias_attribute :available_credit, :credit_limit + def self.destroyed_account_ids @destroyed_account_ids ||= Hash.new { |h,k| h[k] = [] } end diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 461bb0de09..38b0b6aafa 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -10,10 +10,6 @@ module MyApplication 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/contract.rb b/activerecord/test/models/contract.rb index 2cf5aa7a85..cdf7b267b5 100644 --- a/activerecord/test/models/contract.rb +++ b/activerecord/test/models/contract.rb @@ -1,6 +1,7 @@ class Contract < ActiveRecord::Base belongs_to :company belongs_to :developer + belongs_to :firm, :foreign_key => 'company_id' before_save :hi after_save :bye diff --git a/activerecord/test/models/department.rb b/activerecord/test/models/department.rb new file mode 100644 index 0000000000..08004a0ed3 --- /dev/null +++ b/activerecord/test/models/department.rb @@ -0,0 +1,4 @@ +class Department < ActiveRecord::Base + has_many :chefs + belongs_to :hotel +end diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 81bc87bd42..a26de55758 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -1,11 +1,5 @@ require 'ostruct' -module DeveloperProjectsAssociationExtension - def find_most_recent - order("id DESC").first - end -end - module DeveloperProjectsAssociationExtension2 def find_least_recent order("id ASC").first @@ -44,6 +38,8 @@ class Developer < ActiveRecord::Base has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id' has_many :audit_logs + has_many :contracts + has_many :firms, :through => :contracts, :source => :firm scope :jamises, -> { where(:name => 'Jamis') } diff --git a/activerecord/test/models/dog.rb b/activerecord/test/models/dog.rb index 72b7d33a86..b02b8447b8 100644 --- a/activerecord/test/models/dog.rb +++ b/activerecord/test/models/dog.rb @@ -1,4 +1,5 @@ class Dog < ActiveRecord::Base - belongs_to :breeder, :class_name => "DogLover", :counter_cache => :bred_dogs_count - belongs_to :trainer, :class_name => "DogLover", :counter_cache => :trained_dogs_count + belongs_to :breeder, class_name: "DogLover", counter_cache: :bred_dogs_count + belongs_to :trainer, class_name: "DogLover", counter_cache: :trained_dogs_count + belongs_to :doglover, foreign_key: :dog_lover_id, class_name: "DogLover", counter_cache: true end diff --git a/activerecord/test/models/dog_lover.rb b/activerecord/test/models/dog_lover.rb index a33dc575c5..2c5be94aea 100644 --- a/activerecord/test/models/dog_lover.rb +++ b/activerecord/test/models/dog_lover.rb @@ -1,4 +1,5 @@ class DogLover < ActiveRecord::Base - has_many :trained_dogs, :class_name => "Dog", :foreign_key => :trainer_id - has_many :bred_dogs, :class_name => "Dog", :foreign_key => :breeder_id + has_many :trained_dogs, class_name: "Dog", foreign_key: :trainer_id, dependent: :destroy + has_many :bred_dogs, class_name: "Dog", foreign_key: :breeder_id + has_many :dogs end diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb new file mode 100644 index 0000000000..2db968ef11 --- /dev/null +++ b/activerecord/test/models/drink_designer.rb @@ -0,0 +1,3 @@ +class DrinkDesigner < ActiveRecord::Base + has_one :chef, as: :employable +end diff --git a/activerecord/test/models/friendship.rb b/activerecord/test/models/friendship.rb index 6b4f7acc38..4b411ca8e0 100644 --- a/activerecord/test/models/friendship.rb +++ b/activerecord/test/models/friendship.rb @@ -1,4 +1,6 @@ class Friendship < ActiveRecord::Base belongs_to :friend, class_name: 'Person' - belongs_to :follower, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :followers_count + # friend_too exists to test a bug, and probably shouldn't be used elsewhere + belongs_to :friend_too, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :friends_too_count + belongs_to :follower, class_name: 'Person' end diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb new file mode 100644 index 0000000000..b352cd22f3 --- /dev/null +++ b/activerecord/test/models/hotel.rb @@ -0,0 +1,6 @@ +class Hotel < ActiveRecord::Base + has_many :departments + has_many :chefs, through: :departments + has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs + has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs +end diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb index 6cfd443e75..69d4d7df1a 100644 --- a/activerecord/test/models/liquid.rb +++ b/activerecord/test/models/liquid.rb @@ -1,5 +1,4 @@ class Liquid < ActiveRecord::Base self.table_name = :liquid - has_many :molecules, -> { uniq } + has_many :molecules, -> { distinct } end - diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb index 4bff92dc98..f4d127730c 100644 --- a/activerecord/test/models/man.rb +++ b/activerecord/test/models/man.rb @@ -6,4 +6,5 @@ class Man < ActiveRecord::Base # These are "broken" inverse_of associations for the purposes of testing has_one :dirty_face, :class_name => 'Face', :inverse_of => :dirty_man has_many :secret_interests, :class_name => 'Interest', :inverse_of => :secret_man + has_one :mixed_case_monkey end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 1134b09d8b..72095f9236 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -2,14 +2,13 @@ class Member < ActiveRecord::Base has_one :current_membership has_one :selected_membership has_one :membership - 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, -> { 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 + has_one :member_detail, :inverse_of => false has_one :organization, :through => :member_detail belongs_to :member_type diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb index fe619f8732..9d253aa126 100644 --- a/activerecord/test/models/member_detail.rb +++ b/activerecord/test/models/member_detail.rb @@ -1,5 +1,5 @@ class MemberDetail < ActiveRecord::Base - belongs_to :member + belongs_to :member, :inverse_of => false belongs_to :organization has_one :member_type, :through => :member diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb index bcbb7e42c5..df7167ee93 100644 --- a/activerecord/test/models/membership.rb +++ b/activerecord/test/models/membership.rb @@ -8,6 +8,11 @@ class CurrentMembership < Membership belongs_to :club end +class SuperMembership < Membership + belongs_to :member, -> { order('members.id DESC') } + belongs_to :club +end + class SelectedMembership < Membership def self.default_scope select("'1' as foo") diff --git a/activerecord/test/models/mixed_case_monkey.rb b/activerecord/test/models/mixed_case_monkey.rb index 763baefd91..4d37371777 100644 --- a/activerecord/test/models/mixed_case_monkey.rb +++ b/activerecord/test/models/mixed_case_monkey.rb @@ -1,3 +1,5 @@ class MixedCaseMonkey < ActiveRecord::Base self.primary_key = 'monkeyID' + + belongs_to :man end diff --git a/activerecord/test/models/movie.rb b/activerecord/test/models/movie.rb index 6384b4c801..c441be2bef 100644 --- a/activerecord/test/models/movie.rb +++ b/activerecord/test/models/movie.rb @@ -1,5 +1,3 @@ class Movie < ActiveRecord::Base - def self.primary_key - "movieid" - end + self.primary_key = "movieid" end diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index fea55f4535..1c7ed4aa3e 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -1,5 +1,5 @@ class Owner < ActiveRecord::Base self.primary_key = :owner_id - has_many :pets + has_many :pets, -> { order 'pets.name desc' } has_many :toys, :through => :pets end diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index c4ee2bd19d..e76e83f314 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -21,3 +21,9 @@ end class DeadParrot < Parrot belongs_to :killer, :class_name => 'Pirate' end + +class FunkyParrot < Parrot + before_destroy do + raise "before_destroy was called" + end +end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index fa717ef8d6..1a282dbce4 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -8,7 +8,10 @@ class Person < ActiveRecord::Base 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 :friendships, foreign_key: 'friend_id' + # friends_too exists to test a bug, and probably shouldn't be used elsewhere + has_many :friends_too, foreign_key: 'friend_id', class_name: 'Friendship' + has_many :followers, through: :friendships has_many :references has_many :bad_references @@ -29,6 +32,7 @@ class Person < ActiveRecord::Base has_many :agents_posts, :through => :agents, :source => :posts has_many :agents_posts_authors, :through => :agents_posts, :source => :author + has_many :essays, primary_key: "first_name", foreign_key: "writer_id" scope :males, -> { where(:gender => 'M') } scope :females, -> { where(:gender => 'F') } diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb index 3cd5bceed5..f7970d7aab 100644 --- a/activerecord/test/models/pet.rb +++ b/activerecord/test/models/pet.rb @@ -1,5 +1,4 @@ class Pet < ActiveRecord::Base - attr_accessor :current_user self.primary_key = :pet_id @@ -13,5 +12,4 @@ class Pet < ActiveRecord::Base after_destroy do |record| Pet.after_destroy_output = record.current_user end - end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 93a7a2073c..faf539a562 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -1,4 +1,10 @@ class Post < ActiveRecord::Base + class CategoryPost < ActiveRecord::Base + self.table_name = "categories_posts" + belongs_to :category + belongs_to :post + end + module NamedExtension def author 'lifo' @@ -59,6 +65,9 @@ class Post < ActiveRecord::Base has_many :author_favorites, :through => :author has_many :author_categorizations, :through => :author, :source => :categorizations has_many :author_addresses, :through => :author + has_many :author_address_extra_with_address, + through: :author_with_address, + source: :author_address_extra has_many :comments_with_interpolated_conditions, ->(p) { where "#{"#{p.aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome' }, @@ -72,6 +81,8 @@ class Post < ActiveRecord::Base has_many :special_comments_ratings, :through => :special_comments, :source => :ratings has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings + has_many :category_posts, :class_name => 'CategoryPost' + has_many :scategories, through: :category_posts, source: :category has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' @@ -122,7 +133,6 @@ class Post < ActiveRecord::Base 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) }, diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 90273adafc..7f42a4b1f8 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -1,25 +1,11 @@ class Project < ActiveRecord::Base - has_and_belongs_to_many :developers, -> { uniq.order 'developers.name desc, developers.id desc' } + has_and_belongs_to_many :developers, -> { distinct.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 :developers_named_david, -> { where("name = 'David'").distinct }, :class_name => "Developer" + has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').distinct }, :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}"}, diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index c88262580e..3e82e55d89 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -7,6 +7,7 @@ class Reply < Topic end class UniqueReply < Reply + belongs_to :topic, :foreign_key => 'parent_id', :counter_cache => true validates_uniqueness_of :content, :scope => 'parent_id' end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 17035bf338..40c8e97fc2 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -34,7 +34,6 @@ class Topic < ActiveRecord::Base has_many :replies, :dependent => :destroy, :foreign_key => "parent_id" has_many :approved_replies, -> { approved }, class_name: 'Reply', foreign_key: "parent_id", counter_cache: 'replies_count' - has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title" has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id" has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id" diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index f25f72c481..a9a6514c9d 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -14,6 +14,16 @@ ActiveRecord::Schema.define do add_index :binary_fields, :var_binary + create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t| + t.string :awesome + t.string :pizza + t.string :snacks + end + + add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome' + add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' + add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -42,7 +52,7 @@ SQL ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE enum_tests ( - enum_column ENUM('true','false') + enum_column ENUM('text','blob','tiny','medium','long') ) SQL end diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index 5401c12ed5..f2cffca52c 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -14,6 +14,16 @@ ActiveRecord::Schema.define do add_index :binary_fields, :var_binary + create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t| + t.string :awesome + t.string :pizza + t.string :snacks + end + + add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome' + add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' + add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -53,7 +63,7 @@ SQL ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE enum_tests ( - enum_column ENUM('true','false') + enum_column ENUM('text','blob','tiny','medium','long') ) SQL diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index d8271ac8d1..6b7012a172 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -32,6 +32,7 @@ ActiveRecord::Schema.define do char3 text default 'a text field', positive_integer integer default 1, negative_integer integer default -1, + bigint_default bigint default 0::bigint, decimal_number decimal(3,2) default 2.78, multiline_default text DEFAULT '--- [] diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index ae779c702a..88a686d436 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -1,3 +1,5 @@ +# encoding: utf-8 + ActiveRecord::Schema.define do def except(adapter_names_to_exclude) unless [adapter_names_to_exclude].flatten.include?(adapter_name) @@ -183,6 +185,7 @@ ActiveRecord::Schema.define do add_index :companies, [:firm_id, :type, :rating], :name => "company_index" add_index :companies, [:firm_id, :type], :name => "company_partial_index", :where => "rating > 10" + add_index :companies, :name, :name => 'company_name_index', :using => :btree create_table :vegetables, :force => true do |t| t.string :name @@ -230,14 +233,16 @@ ActiveRecord::Schema.define do t.integer :access_level, :default => 1 end - create_table :dog_lovers, :force => true do |t| - t.integer :trained_dogs_count, :default => 0 - t.integer :bred_dogs_count, :default => 0 + create_table :dog_lovers, force: true do |t| + t.integer :trained_dogs_count, default: 0 + t.integer :bred_dogs_count, default: 0 + t.integer :dogs_count, default: 0 end create_table :dogs, :force => true do |t| t.integer :trainer_id t.integer :breeder_id + t.integer :dog_lover_id end create_table :edges, :force => true, :id => false do |t| @@ -280,7 +285,7 @@ ActiveRecord::Schema.define do create_table :friendships, :force => true do |t| t.integer :friend_id - t.integer :person_id + t.integer :follower_id end create_table :goofy_string_id, :force => true, :id => false do |t| @@ -494,6 +499,7 @@ ActiveRecord::Schema.define do t.integer :lock_version, :null => false, :default => 0 t.string :comments t.integer :followers_count, :default => 0 + t.integer :friends_too_count, :default => 0 t.references :best_friend t.references :best_friend_of t.integer :insures, null: false, default: 0 @@ -671,6 +677,7 @@ ActiveRecord::Schema.define do end t.boolean :approved, :default => true t.integer :replies_count, :default => 0 + t.integer :unique_replies_count, :default => 0 t.integer :parent_id t.string :parent_title t.string :type @@ -776,9 +783,26 @@ ActiveRecord::Schema.define do end create_table :weirds, :force => true do |t| t.string 'a$b' + t.string 'なまえ' t.string 'from' end + create_table :hotels, force: true do |t| + end + create_table :departments, force: true do |t| + t.integer :hotel_id + end + create_table :cake_designers, force: true do |t| + end + create_table :drink_designers, force: true do |t| + end + create_table :chefs, force: true do |t| + t.integer :employable_id + t.string :employable_type + t.integer :department_id + end + + except 'SQLite' do # fk_test_has_fk should be before fk_test_has_pk create_table :fk_test_has_fk, :force => true do |t| diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index e9ddeb32cf..b7aff4f47d 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -1,9 +1,6 @@ ActiveRecord::Schema.define do - # 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 - end + create_table :table_with_autoincrement, :force => true do |t| + t.column :name, :string end execute "DROP TABLE fk_test_has_fk" rescue nil |