diff options
Diffstat (limited to 'activerecord')
152 files changed, 2899 insertions, 2927 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 771f1333f0..9f10a31512 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,1848 +1,15 @@ -## Rails 4.0.0 (unreleased) ## +* Mute `psql` output when running rake db:schema:load. -* Models with multiple counter cache associations now update correctly on destroy. - See #7706. + *Godfrey Chan* - *Ian Young* +* Trigger a save on `has_one association=(associate)` when the associate contents have changed. -* If inverse_of is true on an association, then when one calls +find()+ on - the association, ActiveRecord will first look through the in-memory objects - in the association for a particular id. Then, it will go to the DB if it - is not found. This is accomplished by calling +find_by_scan+ in - collection associations whenever +options[:inverse_of]+ is not nil. + Fix #8856. - Fixes #9470. + *Chris Thompson* - *John Wang* - -* `rake db:create` does not change permissions of the MySQL root user. - Fixes #8079. - - *Yves Senn* - -* The length of the `version` column in the `schema_migrations` table - created by the `mysql2` adapter is 191 if the encoding is "utf8mb4". - - The "utf8" encoding in MySQL has support for a maximum of 3 bytes per character, - and only contains characters from the BMP. The recently added - [utf8mb4](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html) - encoding extends the support to four bytes. As of this writing, said encoding - is supported in the betas of the `mysql2` gem. - - Setting the encoding to "utf8mb4" has - [a few implications](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-upgrading.html). - This change addresses the max length for indexes, which is 191 instead of 255. - - *Xavier Noria* - -* Counter caches on associations will now stay valid when attributes are - updated (not just when records are created or destroyed), for example, - when calling `update_attributes`. The following code now works: - - class Comment < ActiveRecord::Base - belongs_to :post, counter_cache: true - end - - class Post < ActiveRecord::Base - has_many :comments - end - - post = Post.create - comment = Comment.create - - post.comments << comment - post.save.reload.comments_count # => 1 - comment.update_attributes(post_id: nil) - - post.save.reload.comments_count # => 0 - - Updating the id of a `belongs_to` object with the id of a new object will - also keep the count accurate. - - *John Wang* - -* Referencing join tables implicitly was deprecated. There is a - possibility that these deprecation warnings are shown even if you - don't make use of that feature. You can now disable the feature entirely. - Fixes #9712. - - Example: - - # in your configuration - config.active_record.disable_implicit_join_references = true - - # or directly - ActiveRecord::Base.disable_implicit_join_references = true - - *Yves Senn* - -* The `:distinct` option for `Relation#count` is deprecated. You - should use `Relation#distinct` instead. - - Example: - - # Before - Post.select(:author_name).count(distinct: true) - - # After - Post.select(:author_name).distinct.count - - *Yves Senn* - -* Rename `Relation#uniq` to `Relation#distinct`. `#uniq` is still - available as an alias but we encourage to use `#distinct` instead. - Also `Relation#uniq_value` is aliased to `Relation#distinct_value`, - this is a temporary solution and you should migrate to `distinct_value`. - - *Yves Senn* - -* Fix quoting for sqlite migrations using copy_table_contents() with binary - columns. - - These would fail with "SQLite3::SQLException: unrecognized token" because - the column was not being passed to quote() so the data was not quoted - correctly. - - *Matthew M. Boedicker* - -* Promotes `change_column_null` to the migrations API. This macro sets/removes - `NOT NULL` constraints, and accepts an optional argument to replace existing - `NULL`s if needed. The adapters for SQLite, MySQL, PostgreSQL, and (at least) - Oracle, already implement this method. - - *Xavier Noria* - -* 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. - - 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. - - *Ezekiel Smithburg* - -* 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. - - Example: - - sql = Post.connection.unprepared_statement do - Post.first.comments.to_sql - 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' - - ### AFTER ### - - User.active.inactive - # => SELECT * FROM posts WHERE state = 'active' AND state = 'inactive' - - 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. - - If you really do want the "last where wins" logic, you can opt-in to - it like so: - - User.active.merge(User.inactive) - - Fixes #7365. - - *Neeraj Singh* and *Jon Leighton* - -* 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 MySQL 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. - - Example: - - class ChangeEnum < ActiveRecord::Migration - disable_ddl_transaction! - - def up - execute "ALTER TYPE model_size ADD VALUE 'new_value'" - end - end - - *Yves Senn* - -* Assigning "0.0" to a nullable numeric column does not make it dirty. - Fixes #9034. - - Example: - - 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] }) - - *Yves Senn* - -* Added functionality to unscope relations in a relations chain. For - instance, if you are passed in a chain of relations as follows: - - User.where(name: "John").order('id DESC') - - but you want to get rid of order, then this feature allows you to do: - - User.where(name: "John").order('id DESC').unscope(:order) - == User.where(name: "John") - - 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. - - *John Wang* - -* Postgresql timestamp with time zone (timestamptz) datatype now returns a - ActiveSupport::TimeWithZone instance instead of a string - - *Troy Kruthoff* - -* The `#append` method for collection associations behaves like`<<`. - `#prepend` is not defined and `<<` or `#append` should be used. - Fixes #7364. - - *Yves Senn* - -* Added support for creating a table via Rails migration generator. - For example, - - rails g migration create_books title:string content:text - - will generate a migration that creates a table called books with - the listed attributes, without creating a model. - - *Sammy Larbi* - -* Fix bug that raises the wrong exception when the exception handled by PostgreSQL adapter - doesn't respond to `#result`. - Fixes #8617. - - *kennyj* - -* Support PostgreSQL specific column types when using `change_table`. - Fixes #9480. - - Example: - - change_table :authors do |t| - t.hstore :books - t.json :metadata - end - - *Yves Senn* - -* Revert 408227d9c5ed7d, 'quote numeric'. This introduced some regressions. - - *Steve Klabnik* - -* 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. - - *Igor Fedoronchuk* - -* Do not try to touch invalid (and thus not persisted) parent record - for a `belongs_to :parent, touch: true` association - - *Olek Janiszewski* - -* Fix when performing an ordered join query. The bug only - affected queries where the order was given with a symbol. - Fixes #9275. - - Example: - - # This will expand the order :name to "authors".name. - Author.joins(:books).where('books.published = 1').order(:name) - - -## Rails 4.0.0.beta1 (February 25, 2013) ## - -* Fix overriding of attributes by `default_scope` on `ActiveRecord::Base#dup`. - - *Hiroshige UMINO* - -* 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: - - Example: - - ball = Ball.new - ball.touch :updated_at # => raises error - - It is not until the ball object has been persisted that it can be touched. - This follows the behavior of update_column. - - *John Wang* - -* Preloading ordered `has_many :through` associations no longer applies - invalid ordering to the `:through` association. - Fixes #8663. - - *Yves Senn* - -* 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. - - You can still use `ActiveRecord::Relation#explain` to see the EXPLAIN output for - any given relation. - - *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] - - *Yves Senn* - -* Rename related indexes on `rename_table` and `rename_column`. This - does not affect indexes with custom names. - - *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. - - *Yves Senn* - -* Fixing issue #776. - - 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). - - 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. - - 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. - - *John Wang* - -* Descriptive error message when the necessary AR adapter gem was not found. - Fixes #7313. - - *Yves Senn* - -* Active Record now raises an error when blank arguments are passed to query - methods for which blank arguments do not make sense. - - Example: - - Post.includes() # => raises error - - *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 - - 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 - - If you need the old behavior you can convert the dates to times manually. - For example: - - >> Post.new(created_at: Date.today).created_at - => Wed, 13 Feb 2013 00:00:00 UTC +00:00 - - >> Post.new(created_at: Date.today.to_time).created_at - => Wed, 13 Feb 2013 07:00:00 UTC +00:00 - - *Andrew White* - -* Preloading `has_many :through` associations with conditions won't - cache the `:through` association. This will prevent invalid - subsets to be cached. - Fixes #8423. - - Example: - - class User - has_many :posts - has_many :recent_comments, -> { where('created_at > ?', 1.week.ago) }, :through => :posts - end - - a_user = User.includes(:recent_comments).first - - # This is preloaded. - a_user.recent_comments - - # This is not preloaded, fetched now. - a_user.posts - - *Yves Senn* - -* Don't run `after_commit` callbacks when creating through an association - if saving the record fails. - - *James Miller* - -* Allow store accessors to be overrided like other attribute methods, e.g.: - - class User < ActiveRecord::Base - store :settings, accessors: [ :color, :homepage ], coder: JSON - - def color - super || 'red' - end - end - - *Sergey Nartimov* - -* 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. - - Example: - - App.where(apikey: 0) # => SELECT * FROM users WHERE apikey = '0' - - *Dylan Smith* - -* Schema dumper supports dumping the enabled database extensions to `schema.rb` - (currently only supported by postgresql). - - *Justin George* - -* The database adapters 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: - - Example: - - DATABASE_URL=sqlite3://localhost/test_db?timeout=500 - DATABASE_URL=postgresql://localhost/test_db?prepared_statements=false - - *Aaron Stone + Rafael Mendonça França* - -* `Relation#merge` now only overwrites where values on the LHS of the - merge. Consider: - - left = Person.where(age: [13, 14, 15]) - right = Person.where(age: [13, 14]).where(age: [14, 15]) - - `left` results in the following SQL: - - WHERE age IN (13, 14, 15) - - `right` results in the following SQL: - - WHERE age IN (13, 14) AND age IN (14, 15) - - Previously, `left.merge(right)` would result in all but the last - condition being removed: - - WHERE age IN (14, 15) - - Now it results in the LHS condition(s) for `age` being removed, but - the RHS remains as it is: - - WHERE age IN (13, 14) AND age IN (14, 15) - - *Jon Leighton* - -* Fix handling of dirty time zone aware attributes - - 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. - - 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 - - *Lilibeth De La Cruz* - -* When `#count` is used in conjunction with `#uniq` we perform `count(:distinct => true)`. - Fixes #6865. - - Example: - - relation.uniq.count # => SELECT COUNT(DISTINCT *) - - *Yves Senn + Kaspar Schiess* - -* PostgreSQL ranges type support. Includes: int4range, int8range, - numrange, tsrange, tstzrange, daterange - - Ranges can be created with inclusive and exclusive bounds. - - Example: - - create_table :Room do |t| - t.daterange :availability - end - - Room.create(availability: (Date.today..Float::INFINITY)) - Room.first.availability # => Wed, 19 Sep 2012..Infinity - - One thing to note: Range class does not support exclusive lower - bound. - - *Alexander Grebennik* - -* Added a state instance variable to each transaction. Will allow other objects - to know whether a transaction has been committed or rolled back. - - *John Wang* - -* Collection associations `#empty?` always respects built records. - Fixes #8879. - - Example: - - widget = Widget.new - widget.things.build - widget.things.empty? # => false - - *Yves Senn* - -* Support for PostgreSQL's `ltree` data type. - - *Rob Worley* - -* 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. - - *Marc-André Lafortune* - -* Serialized attributes can be serialized in integer columns. - Fixes #8575. - - *Rafael Mendonça França* - -* Keep index names when using `alter_table` with sqlite3. - Fixes #3489. - - *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* - -* Do not log the binding values for binary columns. - - *Matthew M. Boedicker* - -* Fix counter cache columns not updated when replacing `has_many :through` - associations. - - *Matthew Robertson* - -* Recognize migrations placed in directories containing numbers and 'rb'. - Fixes #8492. - - *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. - - *Rafael Mendonça França* - -* 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 - - *Aaron Stone* - -* Allow `Relation#where` with no arguments to be chained with new `not` query method. - - Example: - - Developer.where.not(name: 'Aaron') - - *Akira Matsuda* - -* Unscope `update_column(s)` query to ignore default scope. - - 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. - - class User < ActiveRecord::Base - default_scope -> { where(active: true) } - end - - user = User.first - user.active = false - user.save! - - user.update_column(:active, true) # => false - - In this situation we want to skip the default_scope clause and just - update the record based on the primary key. With this change: - - user.update_column(:active, true) # => true - - Fixes #8436. - - *Carlos Antonio da Silva* - -* SQLite adapter no longer corrupts binary data if the data contains `%00`. - - *Chris Feist* - -* 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. - - *kennyj* - -* Do not instantiate intermediate Active Record objects when eager loading. - These records caused `after_find` to run more than expected. - Fixes #3313. - - *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* - -* Ensure that associations take a symbol argument. *Steve Klabnik* - -* 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. - - *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* - -* Fix postgresql adapter to handle BC timestamps correctly - - HistoryEvent.create!(name: "something", occured_at: Date.new(0) - 5.years) - - *Bogdan Gusiev* - -* 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. - - *Victor Costan* - -* 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* - -* `rename_index` can be used inside a `change_table` block. - - change_table :accounts do |t| - t.rename_index :user_id, :account_id - end - - *Jarek Radosz* - -* `#pluck` can be used on a relation with `select` clause. Fix #7551 - - Example: - - Topic.select([:approved, :id]).order(:id).pluck(:id) - - *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. - - *Yves Senn* - -* Deprecate the possibility to pass a string as third argument of `add_index`. - Pass `unique: true` instead. - - 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* - -* Fix `find_in_batches` crashing when IDs are strings and start option is not specified. - - *Alexis Bernard* - -* `AR::Base#attributes_before_type_cast` now returns unserialized values for serialized attributes. - - *Nikita Afanasenko* - -* Use query cache/uncache when using `DATABASE_URL`. - Fixes #6951. - - *kennyj* - -* Fix bug where `update_columns` and `update_column` would not let you update the primary key column. - - *Henrik Nyh* - -* The `create_table` method raises an `ArgumentError` when the primary key column is redefined. - Fixes #6378. - - *Yves Senn* - -* `ActiveRecord::AttributeMethods#[]` raises `ActiveModel::MissingAttributeError` - error if the given attribute is missing. Fixes #5433. - - class Person < ActiveRecord::Base - belongs_to :company - end - - # Before: - person = Person.select('id').first - person[:name] # => nil - person.name # => ActiveModel::MissingAttributeError: missing_attribute: name - person[:company_id] # => nil - person.company # => nil - - # 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 - - *Francesco Rodriguez* - -* 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: - - User.where(first_name: 'Penélope').first_or_create - - will execute: - - User.where(first_name: 'Penélope').create - - Causing all the `create` callbacks to execute within the context of - the scope. This could affect queries that occur within callbacks. - - User.find_or_create_by(first_name: 'Penélope') - - will execute: - - User.create(first_name: 'Penélope') - - Which obviously does not affect the scoping of queries within - callbacks. - - The `find_or_create_by` version also reads better, frankly. - - If you need to add extra attributes during create, you can do one of: - - User.create_with(active: true).find_or_create_by(first_name: 'Jon') - User.find_or_create_by(first_name: 'Jon') { |u| u.active = true } - - 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. - - *Jon Leighton* - -* Fix bug with presence validation of associations. Would incorrectly add duplicated errors - when the association was blank. Bug introduced in 1fab518c6a75dac5773654646eb724a59741bc13. - - *Scott Willson* - -* Fix bug where sum(expression) returns string '0' for no matching records. - Fixes #7439 - - *Tim Macfarlane* - -* PostgreSQL adapter correctly fetches default values when using multiple schemas and domains in a db. Fixes #7914 - - *Arturo Pie* - -* Learn ActiveRecord::QueryMethods#order work with hash arguments - - 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 - - User.order(:name, email: :desc) - # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC - - *Tima Maslyuchenko* - -* 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. - - *Alexey Muranov* - -* The postgres adapter now supports tables with capital letters. - Fixes #5920. - - *Yves Senn* - -* `CollectionAssociation#count` returns `0` without querying if the - parent record is not persisted. - - Before: - - person.pets.count - # SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" IS NULL - # => 0 - - After: - - person.pets.count - # fires without sql query - # => 0 - - *Francesco Rodriguez* - -* Fix `reset_counters` crashing on `has_many :through` associations. - Fixes #7822. - - *lulalala* - -* Support for partial inserts. - - 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. - - 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). - - The `partial_updates` configuration option is now renamed to - `partial_writes` to reflect the fact that it now impacts both inserts - and updates. - - *Jon Leighton* - -* Allow before and after validations to take an array of lifecycle events - - *John Foley* - -* Support for specifying transaction isolation level - - If your database supports setting the isolation level for a transaction, you can set - it like so: - - Post.transaction(isolation: :serializable) do - # ... - 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: - - * The adapter does not support setting the isolation level - * You are joining an existing open transaction - * You are creating a nested (savepoint) transaction - - 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. - - *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. - - *Ernie Miller* - -* Explain only normal CRUD sql (select / update / insert / delete). - Fix problem that explains unexplainable sql. - Closes #7544 #6458. - - *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`. - - *Matt Jones* - -* Accept `belongs_to` (including polymorphic) association keys in queries. - - The following queries are now equivalent: - - Post.where(author: author) - Post.where(author_id: author) - - PriceEstimate.where(estimate_of: treasure) - PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure) - - *Peter Brown* - -* Use native `mysqldump` command instead of `structure_dump` method - when dumping the database structure to a sql file. Fixes #5547. - - *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 - - 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. - - 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). - - If the `pg_array_parser` gem is available, it will be used when - parsing PostgreSQL's array representation. - - *Dan McClain* - -* 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`. - - *Ernie Miller* - -* `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* - -* Map interval with precision to string datatype in PostgreSQL. Fixes #7518. - - *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* - -* Fix `ActiveRecord::Relation#pluck` when columns or tables are reserved words. - - *Ian Lesperance* - -* Allow JSON columns to be created in PostgreSQL and properly encoded/decoded. - to/from database. - - *Dickson S. Guedes* - -* Fix time column type casting for invalid time string values to correctly return `nil`. - - *Adam Meehan* - -* Allow to pass Symbol or Proc into `:limit` option of #accepts_nested_attributes_for. - - *Mikhail Dieterle* - -* ActiveRecord::SessionStore has been extracted from Active Record as `activerecord-session_store` - gem. Please read the `README.md` file on the gem for the usage. - - *Prem Sichanugrist* - -* Fix `reset_counters` when there are multiple `belongs_to` association with the - same foreign key and one of them have a counter cache. - Fixes #5200. - - *Dave Desrochers* - -* `serialized_attributes` and `_attr_readonly` become class method only. Instance reader methods are deprecated. +* Abort a rake task when missing db/structure.sql like `db:schema:load` task. *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* - -* Allow Relation#merge to take a proc. - - This was requested by DHH to allow creating of one's own custom - association macros. - - For example: - - module Commentable - def has_many_comments(extra) - has_many :comments, -> { where(:foo).merge(extra) } - end - end - - class Post < ActiveRecord::Base - extend Commentable - has_many_comments -> { where(:bar) } - end - - *Jon Leighton* - -* Add CollectionProxy#scope. - - This can be used to get a Relation from an association. - - Previously we had a #scoped method, but we're deprecating that for - AR::Base, so it doesn't make sense to have it here. - - This was requested by DHH, to facilitate code like this: - - Project.scope.order('created_at DESC').page(current_page).tagged_with(@tag).limit(5).scoping do - @topics = @project.topics.scope - @todolists = @project.todolists.scope - @attachments = @project.attachments.scope - @documents = @project.documents.scope - end - - *Jon Leighton* - -* Add `Relation#load`. - - This method explicitly loads the records and then returns `self`. - - Rather than deciding between "do I want an array or a relation?", - most people are actually asking themselves "do I want to eager load - or lazy load?" Therefore, this method provides a way to explicitly - eager-load without having to switch from a `Relation` to an array. - - Example: - - @posts = Post.where(published: true).load - - *Jon Leighton* - -* `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 - - 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. - - Note that calling `Model.all` with options (e.g. - `Model.all(conditions: '...')` was already deprecated, but it will - still return an array in order to make the transition easier. - - `Model.scoped` is deprecated in favour of `Model.all`. - - `Relation#all` still returns an array, but is deprecated (since it - would serve no purpose if we made it return a `Relation`). - - *Jon Leighton* - -* `:finder_sql` and `:counter_sql` options on collection associations - are deprecated. Please transition to using scopes. - - *Jon Leighton* - -* `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many` - associations are deprecated. Please transition to using `has_many - :through`. - - *Jon Leighton* - -* 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. - - 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"} - - *Sebastian Martinez + Rafael Mendonça França* - -* The migration generator now creates a join table with (commented) indexes every time - the migration name contains the word `join_table`: - - rails g migration create_join_table_for_artists_and_musics artist_id:index music_id - - *Aleksey Magusev* - -* Add `add_reference` and `remove_reference` schema statements. Aliases, `add_belongs_to` - and `remove_belongs_to` are acceptable. References are reversible. - - Examples: - - # Create a user_id column - add_reference(:products, :user) - # Create a supplier_id, supplier_type columns and appropriate index - add_reference(:products, :supplier, polymorphic: true, index: true) - # Remove polymorphic reference - remove_reference(:products, :supplier, polymorphic: true) - - *Aleksey Magusev* - -* Add `:default` and `:null` options to `column_exists?`. - - column_exists?(:testings, :taggable_id, :integer, null: false) - column_exists?(:testings, :taggable_type, :string, default: 'Photo') - - *Aleksey Magusev* - -* `ActiveRecord::Relation#inspect` now makes it clear that you are - dealing with a `Relation` object rather than an array:. - - User.where(age: 30).inspect - # => <ActiveRecord::Relation [#<User ...>, #<User ...>, ...]> - - User.where(age: 30).to_a.inspect - # => [#<User ...>, #<User ...>] - - The number of records displayed will be limited to 10. - - *Brian Cardarella, Jon Leighton & Damien Mathieu* - -* Add `collation` and `ctype` support to PostgreSQL. These are available for PostgreSQL 8.4 or later. - Example: - - development: - adapter: postgresql - host: localhost - database: rails_development - username: foo - password: bar - encoding: UTF8 - collation: ja_JP.UTF8 - ctype: ja_JP.UTF8 - - *kennyj* - -* Changed `validates_presence_of` on an association so that children objects - do not validate as being present if they are marked for destruction. This - prevents you from saving the parent successfully and thus putting the parent - in an invalid state. - - *Nick Monje & Brent Wheeldon* - -* `FinderMethods#exists?` now returns `false` with the `false` argument. - - *Egor Lynko* - -* Added support for specifying the precision of a timestamp in the postgresql - adapter. So, instead of having to incorrectly specify the precision using the - `:limit` option, you may use `:precision`, as intended. For example, in a migration: - - def change - create_table :foobars do |t| - t.timestamps precision: 0 - end - end - - *Tony Schneider* - -* Allow `ActiveRecord::Relation#pluck` to accept multiple columns. Returns an - array of arrays containing the typecasted values: - - Person.pluck(:id, :name) - # SELECT people.id, people.name FROM people - # [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] - - *Jeroen van Ingen & Carlos Antonio da Silva* - -* Improve the derivation of HABTM join table name to take account of nesting. - It now takes the table names of the two models, sorts them lexically and - then joins them, stripping any common prefix from the second table name. - - Some examples: - - Top level models (Category <=> Product) - Old: categories_products - New: categories_products - - Top level models with a global table_name_prefix (Category <=> Product) - Old: site_categories_products - New: site_categories_products - - Nested models in a module without a table_name_prefix method (Admin::Category <=> Admin::Product) - Old: categories_products - New: categories_products - - Nested models in a module with a table_name_prefix method (Admin::Category <=> Admin::Product) - Old: categories_products - New: admin_categories_products - - Nested models in a parent model (Catalog::Category <=> Catalog::Product) - Old: categories_products - New: catalog_categories_products - - Nested models in different parent models (Catalog::Category <=> Content::Page) - Old: categories_pages - New: catalog_categories_content_pages - - *Andrew White* - -* Move HABTM validity checks to `ActiveRecord::Reflection`. One side effect of - this is to move when the exceptions are raised from the point of declaration - to when the association is built. This is consistent with other association - validity checks. - - *Andrew White* - -* Added `stored_attributes` hash which contains the attributes stored using - `ActiveRecord::Store`. This allows you to retrieve the list of attributes - you've defined. - - class User < ActiveRecord::Base - store :settings, accessors: [:color, :homepage] - end - - User.stored_attributes[:settings] # [:color, :homepage] - - *Joost Baaij & Carlos Antonio da Silva* - -* PostgreSQL default log level is now 'warning', to bypass the noisy notice - messages. You can change the log level using the `min_messages` option - available in your config/database.yml. - - *kennyj* - -* Add uuid datatype support to PostgreSQL adapter. - - *Konstantin Shabanov* - -* Added `ActiveRecord::Migration.check_pending!` that raises an error if - migrations are pending. - - *Richard Schneeman* - -* Added `#destroy!` which acts like `#destroy` but will raise an - `ActiveRecord::RecordNotDestroyed` exception instead of returning `false`. - - *Marc-André Lafortune* - -* 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 - 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* - -* 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') } - - The former has numerous issues. It is a common newbie gotcha to do - the following: - - scope :recent, where(published_at: Time.now - 2.weeks) - - Or a more subtle variant: - - scope :recent, -> { where(published_at: Time.now - 2.weeks) } - scope :recent_red, recent.where(color: 'red') - - 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: - - scope :remove_conditions, except(:where) - where(...).remove_conditions # => still has conditions - - *Jon Leighton* - -* Remove IdentityMap - - IdentityMap has never graduated to be an "enabled-by-default" feature, due - to some inconsistencies with associations, as described in this commit: - - https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6 - - Hence the removal from the codebase, until such issues are fixed. - - *Carlos Antonio da Silva* - -* Added the schema cache dump feature. - - `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. - - Usage notes: - - 1) execute rake task. - RAILS_ENV=production bundle exec rake db:schema:cache:dump - => generate db/schema_cache.dump - - 2) add config.active_record.use_schema_cache_dump = true in config/production.rb. BTW, true is default. - - 3) boot rails. - RAILS_ENV=production bundle exec rails server - => use db/schema_cache.dump - - 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 - - *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* - - -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..822e460918 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -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/activerecord.gemspec b/activerecord/activerecord.gemspec index 3e3475f709..337106cb92 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,6 +24,6 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '~> 4.0.0.beta2' - s.add_dependency 'activerecord-deprecated_finders', '~> 0.0.3' + s.add_dependency 'arel', '~> 4.0.0' + s.add_dependency 'activerecord-deprecated_finders', '~> 1.0.2' 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/lib/active_record.rb b/activerecord/lib/active_record.rb index c33f03f13f..0330c0f37f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -35,8 +35,8 @@ module ActiveRecord autoload :Base autoload :Callbacks autoload :Core - autoload :CounterCache autoload :ConnectionHandling + autoload :CounterCache autoload :DynamicMatchers autoload :Explain autoload :Inheritance @@ -50,12 +50,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,8 +71,8 @@ module ActiveRecord autoload :Aggregations autoload :Associations - autoload :AttributeMethods autoload :AttributeAssignment + autoload :AttributeMethods autoload :AutosaveAssociation autoload :Relation @@ -143,6 +145,10 @@ module ActiveRecord autoload :MySQLDatabaseTasks, 'active_record/tasks/mysql_database_tasks' autoload :PostgreSQLDatabaseTasks, 'active_record/tasks/postgresql_database_tasks' + + autoload :FirebirdDatabaseTasks, 'active_record/tasks/firebird_database_tasks' + autoload :SqlserverDatabaseTasks, 'active_record/tasks/sqlserver_database_tasks' + autoload :OracleDatabaseTasks, 'active_record/tasks/oracle_database_tasks' end autoload :TestCase diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index eb08f72286..5e5995f566 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -988,7 +988,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. # @@ -1073,6 +1073,9 @@ 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>.) @@ -1094,6 +1097,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 @@ -1115,11 +1119,11 @@ module ActiveRecord # similar callbacks may affect the :dependent behavior, and the # :dependent 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 diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 4c4b0f08e5..db0553ea76 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -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" # @@ -92,7 +92,7 @@ module ActiveRecord # 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 @@ -113,7 +113,7 @@ module ActiveRecord 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 @@ -217,7 +217,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 @@ -235,6 +236,7 @@ module ActiveRecord 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 end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index a9525436fb..aa5551fe0c 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -101,6 +101,7 @@ module ActiveRecord scope.includes! item.includes_values scope.where_values += item.where_values + scope.order_values |= item.order_values 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 9ac561b997..543a0247d1 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -25,9 +25,10 @@ module ActiveRecord::Associations::Builder 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? - @_after_create_counter_called = true + if record = #{name} + record.class.increment_counter(:#{cache_column}, record.id) + @_after_create_counter_called = true + end end def belongs_to_counter_cache_before_destroy_for_#{name} @@ -66,8 +67,19 @@ module ActiveRecord::Associations::Builder def add_touch_callbacks(reflection) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def belongs_to_touch_after_save_or_destroy_for_#{name} - record = #{name} + foreign_key_field = #{reflection.foreign_key.inspect} + old_foreign_id = attribute_was(foreign_key_field) + + if old_foreign_id + klass = association(#{name.inspect}).klass + old_record = klass.find_by(klass.primary_key => old_foreign_id) + if old_record + old_record.touch #{options[:touch].inspect if options[:touch] != true} + end + end + + record = #{name} unless record.nil? || record.new_record? record.touch #{options[:touch].inspect if options[:touch] != true} end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 2385c90c1a..2a00ac1386 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -79,8 +79,20 @@ module ActiveRecord if block_given? load_target.find(*args) { |*block_args| yield(*block_args) } else - if options[:finder_sql] || options[:inverse_of] + if options[:finder_sql] find_by_scan(*args) + elsif options[:inverse_of] + args = args.flatten + raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args.blank? + + result = find_by_scan(*args) + + result_size = Array(result).size + if !result || result_size != args.size + scope.raise_record_not_found_exception!(args, result_size, args.size) + else + result + end else scope.find(*args) end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index c2add32aa6..56e57cc36e 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -92,7 +92,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 +228,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 @@ -303,7 +304,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 @@ -832,8 +833,6 @@ module ActiveRecord @association.include?(record) end - alias_method :new, :build - def proxy_association @association end @@ -848,10 +847,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 } + @association.scope.tap do |scope| + scope.proxy_association = @association end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 98bd010f70..920038a543 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -25,9 +25,8 @@ module ActiveRecord 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? transaction_if(save) do remove_target!(options[:dependent]) if target && !target.destroyed? diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index f40368cfeb..28e081c03c 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -5,10 +5,31 @@ module ActiveRecord 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 - + attr_reader :join_parts, :reflections, :alias_tracker, :base_klass + + # 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) - @active_record = base + @base_klass = base @table_joins = joins @join_parts = [JoinBase.new(base)] @associations = {} @@ -54,10 +75,12 @@ module ActiveRecord parent }.uniq - remove_duplicate_results!(active_record, records, @associations) + remove_duplicate_results!(base_klass, records, @associations) records end + protected + def remove_duplicate_results!(base, records, associations) case associations when Symbol, String @@ -88,8 +111,6 @@ module ActiveRecord end end - protected - def cache_joined_association(association) associations = [] parent = association.parent @@ -108,8 +129,8 @@ module ActiveRecord 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?" + reflection = parent.reflections[associations.intern] or + raise ConfigurationError, "Association named '#{ associations }' was not found on #{ parent.base_klass.name }; perhaps you misspelled it?" unless join_association = find_join_association(reflection, parent) @reflections << reflection join_association = build_join_association(reflection, parent) 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..e4d17451dc 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -55,14 +55,19 @@ module ActiveRecord def find_parent_in(other_join_dependency) other_join_dependency.join_parts.detect do |join_part| - parent == join_part + case parent + when JoinBase + parent.base_klass == join_part.base_klass + else + parent == join_part + end end end - def join_to(relation) + def join_to(manager) tables = @tables.dup foreign_table = parent_table - foreign_klass = parent.active_record + foreign_klass = parent.base_klass # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse @@ -75,7 +80,7 @@ module ActiveRecord foreign_key = reflection.foreign_key when :has_and_belongs_to_many # Join the join table first... - relation.from(join( + manager.from(join( table, table[reflection.foreign_key]. eq(foreign_table[reflection.active_record_primary_key]) @@ -109,15 +114,30 @@ module ActiveRecord constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty? end - relation.from(join(table, constraint)) + manager.from(join(table, constraint)) # The current table in this iteration becomes the foreign table in the next foreign_table, foreign_klass = table, reflection.klass end - relation + manager end + # 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(reflection, table, key, foreign_table, foreign_key) constraint = table[key].eq(foreign_table[foreign_key]) 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..a7dacdbfd6 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -4,7 +4,7 @@ module ActiveRecord class JoinBase < JoinPart # :nodoc: def ==(other) other.class == self.class && - other.active_record == active_record + other.base_klass == base_klass end def aliased_prefix @@ -16,7 +16,7 @@ module ActiveRecord 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..b534569063 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -1,7 +1,7 @@ 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 @@ -11,12 +11,12 @@ module ActiveRecord # 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 - delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record + delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :base_klass - def initialize(active_record) - @active_record = active_record + def initialize(base_klass) + @base_klass = base_klass @cached_record = {} @column_names_with_alias = nil end @@ -70,7 +70,7 @@ module ActiveRecord end def instantiate(row) - @cached_record[record_id(row)] ||= active_record.instantiate(extract_record(row)) + @cached_record[record_id(row)] ||= base_klass.instantiate(extract_record(row)) 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 38bc7ce7da..157b627ad5 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -5,9 +5,13 @@ module ActiveRecord include ThroughAssociation def associated_records_by_owner - super.each do |owner, records| - records.uniq! if reflection_scope.distinct_value + 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/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 43520142bf..35f29b37a2 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -14,7 +14,7 @@ module ActiveRecord def target_scope scope = super chain[1..-1].each do |reflection| - scope = scope.merge( + scope.merge!( reflection.klass.all.with_default_scope. except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index ecfa556ab4..e536f5ebcc 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -81,7 +81,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 +146,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 +172,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 +187,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 aa92343f76..609c6e8cab 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -56,7 +56,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 @@ -163,8 +163,22 @@ module ActiveRecord # person.respond_to('age?') # => true # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) + name = name.to_s self.class.define_attribute_methods unless self.class.attribute_methods_generated? - super + 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.present? && 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+. @@ -328,13 +342,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 +363,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/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 25d62fdb85..7f1ebab4cd 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,6 +50,7 @@ module ActiveRecord end end + # *DEPRECATED*: Use ActiveRecord::AttributeMethods::Serialization::ClassMethods#serialized_attributes class level method instead. def serialized_attributes message = "Instance level serialized_attributes method is deprecated, please use class level method." ActiveSupport::Deprecation.warn message @@ -86,10 +93,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/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 0df3e57947..44323ce9db 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -62,14 +62,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,14 +113,14 @@ 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 diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index e262401da6..b06add096f 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -160,10 +160,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 +172,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. diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 22226b2f4f..e4c484d64b 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -83,7 +83,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 bf2f945448..816b397fcf 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. # 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 902dbd148e..566550cbe2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -8,41 +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 end - def sql_type - base.type_to_sql(type.to_sym, limit, precision, scale) - end - def primary_key? - type.to_sym == :primary_key - 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? - column_options[:column] = self - add_column_options!(column_sql, column_options) unless primary_key? - column_sql + primary_key || type.to_sym == :primary_key end - - private - - def add_column_options!(sql, options) - base.add_column_options!(sql, options) - end end # Represents the schema of an SQL table in an abstract way. This class @@ -68,19 +48,25 @@ 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 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 = {}) + options[:primary_key] = true + column(name, type, options) end # Returns a ColumnDefinition for the column with name +name+. @@ -233,20 +219,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! @@ -278,28 +258,31 @@ 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 } * ', ' - end + 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 - private - def create_column_definition(base, name, type) - ColumnDefinition.new base, name, type + column.limit = limit + 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 - def new_column_definition(base, name, type) - definition = create_column_definition base, name, type - @columns << definition - @columns_hash[name] = definition - definition + private + def create_column_definition(name, type) + ColumnDefinition.new name, type end def primary_key_column_name @@ -308,7 +291,24 @@ module ActiveRecord 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 cd4409295f..9c0c4e3ef0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -171,14 +171,14 @@ module ActiveRecord # # See also TableDefinition#column for details on how to create columns. def create_table(table_name, options = {}) - td = create_table_definition + 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 + td.primary_key pk, options.fetch(:id, :primary_key), options end yield td if block_given? @@ -187,11 +187,7 @@ 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 @@ -359,9 +355,9 @@ 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. @@ -447,7 +443,7 @@ module ActiveRecord # # add_index(:suppliers, :name) # - # generates + # generates: # # CREATE INDEX suppliers_name_index ON suppliers(name) # @@ -455,7 +451,7 @@ module ActiveRecord # # add_index(:accounts, [:branch_id, :party_id], unique: true) # - # generates + # generates: # # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id) # @@ -463,7 +459,7 @@ module ActiveRecord # # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party') # - # generates + # generates: # # CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id) # @@ -471,13 +467,13 @@ module ActiveRecord # # add_index(:accounts, :name, name: 'by_name', length: 10) # - # generates + # generates: # # CREATE INDEX by_name ON accounts(name(10)) # # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) # - # generates + # generates: # # CREATE INDEX by_name_surname ON accounts(name(10), surname(15)) # @@ -487,7 +483,7 @@ module ActiveRecord # # add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc}) # - # generates + # generates: # # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # @@ -497,11 +493,30 @@ module ActiveRecord # # add_index(:accounts, [:branch_id, :party_id], unique: true, where: "active") # - # generates + # 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. + # ====== 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}" @@ -685,6 +700,9 @@ module ActiveRecord if options[:null] == false sql << " NOT NULL" end + if options[:auto_increment] == true + sql << " AUTO_INCREMENT" + end end # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. @@ -749,12 +767,21 @@ module ActiveRecord 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_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 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 + + using = "USING #{options[:using]}" if options[:using].present? + if supports_partial_index? index_options = options[:where] ? " WHERE #{options[:where]}" : "" end @@ -769,6 +796,7 @@ module ActiveRecord index_type = options max_index_length = allowed_index_name_length + algorithm = using = nil end if index_name.length > max_index_length @@ -779,7 +807,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 = {}) @@ -829,8 +857,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_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 7949bcb5ce..26586f0974 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 @@ -100,6 +101,79 @@ module ActiveRecord @visitor = nil 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 + + 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_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 + + 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 + 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!(column_sql, column_options) + @conn.add_column_options! column_sql, column_options + column_sql + end + end + + def schema_creation + SchemaCreation.new self + end + def lease synchronize do unless in_use @@ -212,6 +286,12 @@ module ActiveRecord [] 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 @@ -353,7 +433,7 @@ module ActiveRecord def translate_exception(exception, message) # override in derived class - ActiveRecord::StatementInvalid.new(message) + ActiveRecord::StatementInvalid.new(message, exception) 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..2a7b855f95 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,34 @@ require 'arel/visitors/bind_visitor' module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter + class SchemaCreation < AbstractAdapter::SchemaCreation + private + + def visit_AddColumn(o) + add_column_position!(super, o) + end + + def add_column_position!(sql, column) + if column.first + sql << " FIRST" + elsif column.after + sql << " AFTER #{quote_column_name(column.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 +82,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 +100,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 +151,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 @@ -187,6 +211,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 +224,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 @@ -259,7 +287,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 @@ -337,6 +365,7 @@ module ActiveRecord def recreate_database(name, options = {}) drop_database(name) create_database(name, options) + reconnect! end # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. @@ -410,7 +439,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 +459,7 @@ 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]) + new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra]) end end end @@ -459,10 +492,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 +516,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 +606,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 @@ -651,6 +689,7 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index a4b3a0c584..609ccc2ed2 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -161,7 +161,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 +172,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 +192,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) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 25b8aef617..530a27d099 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -4,7 +4,7 @@ gem 'mysql2', '~> 0.3.10' 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 @@ -63,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) diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 7544c2a783..f23521430d 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: @@ -383,7 +383,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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index 3d8f0b575c..a9ef11aa83 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -2,6 +2,17 @@ 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 @@ -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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 68f2f2ca7b..1be116ce10 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -18,8 +18,19 @@ module ActiveRecord end end + class Bit < Type + def type_cast(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 @@ -63,6 +74,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) @@ -312,14 +333,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 +351,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 43f991b362..40a3b82839 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -18,27 +18,33 @@ 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)) 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,14 +57,14 @@ module ActiveRecord super end when Numeric - if column.sql_type == 'money' || [:string, :text].include?(column.type) + 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/ @@ -90,8 +96,12 @@ 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) + 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..d9b807bba4 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+. @@ -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 @@ -337,10 +376,7 @@ 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. @@ -375,6 +411,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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 940de7e4f6..bf403c3ae0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -16,7 +16,7 @@ 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, @@ -24,7 +24,7 @@ module ActiveRecord :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? } @@ -80,7 +80,7 @@ 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 @@ -330,6 +330,13 @@ module ActiveRecord class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods + def primary_key(name, type = :primary_key, options = {}) + return super unless type == :uuid + options[:default] ||= 'uuid_generate_v4()' + options[:primary_key] = true + column name, type, options + end + def column(name, type = nil, options = {}) super column = self[name] @@ -344,8 +351,8 @@ module ActiveRecord private - def create_column_definition(base, name, type) - ColumnDefinition.new base, name, type + def create_column_definition(name, type) + ColumnDefinition.new name, type end end @@ -426,6 +433,10 @@ module ActiveRecord true end + def index_algorithms + { concurrently: 'CONCURRENTLY' } + end + class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) super @@ -627,19 +638,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 - - column = options.fetch(:column) { return super } - if column.type == :uuid && options[:default] =~ /\(\)/ - sql << " DEFAULT #{options[:default]}" - else - super - end - end - # Set the authorized user for this session def session_auth=(user) clear_cache! @@ -669,6 +667,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. @@ -713,7 +715,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 @@ -900,8 +909,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/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index d3ffee3a8b..7d940fe1c9 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" @@ -458,7 +458,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 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 968dad5844..ba053700f2 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -77,8 +77,17 @@ module ActiveRecord mattr_accessor :disable_implicit_join_references, instance_writer: false self.disable_implicit_join_references = false - class_attribute :connection_handler, instance_writer: false - self.connection_handler = ConnectionAdapters::ConnectionHandler.new + class_attribute :default_connection_handler, instance_writer: false + + def self.connection_handler + ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler + end + + def self.connection_handler=(handler) + ActiveRecord::RuntimeRegistry.connection_handler = handler + end + + self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new end module ClassMethods @@ -176,6 +185,7 @@ module ActiveRecord @columns_hash = self.class.column_types.dup init_internals + init_changed_attributes ensure_proper_type populate_with_current_scope_attributes @@ -246,9 +256,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 = {} @@ -299,9 +307,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 @@ -336,9 +346,15 @@ module ActiveRecord self.class.connection end + 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)}" @@ -434,5 +450,14 @@ module ActiveRecord @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 end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index c615d59725..cd31147414 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -57,24 +57,25 @@ 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 + attr_reader :original_exception + + def initialize(message, original_exception = nil) + super(message) + @original_exception = original_exception + end 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. + # Defunct wrapper class kept for compatibility. + # +StatementInvalid+ wraps the original exception now. class WrappedDatabaseException < StatementInvalid - attr_reader :original_exception - - def initialize(message, original_exception) - super(message) - @original_exception = original_exception - end end # Raised when a record cannot be inserted because it would violate a uniqueness constraint. diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 15736575a2..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 assignment. - 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..a3bc56d600 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 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..45dc26f0ed 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -708,11 +708,18 @@ 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 @@ -752,7 +759,7 @@ module ActiveRecord 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.map! { |f| f[(fixture_path.to_s.size + 1)..-5] } else fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } end @@ -765,8 +772,7 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index f54865c86e..8df76c7f5f 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -174,7 +174,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/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index c1ba524c84..61e5c120df 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 diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 27d37766b7..6c020e1d57 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -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+. @@ -466,7 +466,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 @@ -867,11 +867,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 @@ -892,10 +896,7 @@ module ActiveRecord 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 +933,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/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 602ab9e2f4..d607f49e2b 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 @@ -338,7 +362,7 @@ module ActiveRecord assign_to_or_mark_for_destruction(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}" @@ -428,7 +452,7 @@ module ActiveRecord 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 +514,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/persistence.rb b/activerecord/lib/active_record/persistence.rb index b25d0601cb..178db07ca1 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 @@ -428,23 +428,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 902fd90c54..f78ccb01aa 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -8,7 +8,7 @@ module ActiveRecord 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, :distinct, :references, :none, :to => :all + :having, :create_with, :uniq, :distinct, :references, :none, :unscope, :to => :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all # Executes a custom SQL query against your database and returns all the results. The results will diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 99117b74c5..e36888d4a8 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -36,6 +36,20 @@ module ActiveRecord rake_tasks do require "active_record/base" + + ActiveRecord::Tasks::DatabaseTasks.env = Rails.env + ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first + ActiveRecord::Tasks::DatabaseTasks.seed_loader = Rails.application + 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' + + 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 + load "active_record/railties/databases.rake" end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index d92e268109..92bef09ff5 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 @@ -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 @@ -184,7 +178,7 @@ db_namespace = namespace :db do 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| @@ -228,7 +239,7 @@ db_namespace = namespace :db do desc 'Create a db/schema.rb file that can be portably used 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,7 +270,7 @@ 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") + filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") FileUtils.rm(filename) if File.exists?(filename) end end @@ -270,32 +278,11 @@ db_namespace = namespace :db do end namespace :structure do - def set_firebird_env(config) - ENV['ISC_USER'] = config['username'].to_s if config['username'] - ENV['ISC_PASSWORD'] = config['password'].to_s if config['password'] - end - - def firebird_db_string(config) - FireRuby::Database.db_string_for(config.symbolize_keys) - end - desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql' task :dump => [:environment, :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| @@ -307,23 +294,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 @@ -378,29 +352,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 do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke end @@ -426,7 +382,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 +392,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/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0995750ecd..60eda96f08 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -75,8 +75,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. # @@ -401,6 +406,16 @@ 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) + # + # taggings_reflection = 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 end @@ -426,6 +441,17 @@ 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 @@ -496,9 +522,16 @@ module ActiveRecord 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 } diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 037097d2dd..913f6f88f2 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -10,14 +10,14 @@ module ActiveRecord :extending] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, - :reverse_order, :distinct, :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 + attr_accessor :default_scoped, :proxy_association alias :model :klass alias :loaded? :loaded alias :default_scoped? :default_scoped @@ -39,7 +39,7 @@ module ActiveRecord reset end - def insert(values) + def insert(values) # :nodoc: primary_key_value = nil if primary_key && Hash === values @@ -56,16 +56,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 +64,7 @@ module ActiveRecord im.insert substitutes end - conn.insert( + @klass.connection.insert( im, 'SQL', primary_key, @@ -82,6 +73,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. # @@ -603,7 +617,7 @@ module ActiveRecord "\n" \ " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \ "\n" \ - "Currently, Active Record recognises the table in the string, and knows to JOIN the " \ + "Currently, Active Record recognizes 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. " \ @@ -612,7 +626,7 @@ module ActiveRecord "\n" \ " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n" \ "\n" \ - "If you don't rely on implicit join references you can disable the feature entirely" \ + "If you don't rely on implicit join references you can disable the feature entirely " \ "by setting `config.active_record.disable_implicit_join_references = true`." ) true diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index be011b22af..64e1ff9a6a 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -82,7 +82,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 @@ -135,7 +135,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'] # diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 00a506c3a7..8d6740246c 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -14,14 +14,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 +37,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 @@ -72,7 +70,7 @@ module ActiveRecord end end - module ClassMethods + module ClassMethods # :nodoc: @@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2) def new(klass, *args) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 14520381c9..72e9272cd7 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -37,7 +37,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>. @@ -130,8 +130,8 @@ module ActiveRecord last or raise RecordNotFound end - # Returns +true+ if a record exists in the table that matches the +id+ or - # conditions given, or +false+ otherwise. The argument can take six forms: + # Returns truthy if a record exists in the table that matches the +id+ or + # conditions given, or falsy otherwise. The argument can take six forms: # # * Integer - Finds the record with this primary key. # * String - Finds the record with a primary key corresponding to this @@ -176,6 +176,28 @@ module ActiveRecord false end + # 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 + + raise RecordNotFound, error + end + protected def find_with_associations @@ -259,11 +281,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 +304,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 diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index eb23e92fb8..936b83261e 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -39,7 +39,7 @@ 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 @@ -48,11 +48,12 @@ module ActiveRecord @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 @@ -66,12 +67,39 @@ module ActiveRecord 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 + + join_dependency.join_associations.each do |association| + @relation = association.join_relation(relation) + end + end + end + def merge_multi_values relation.where_values = merged_wheres relation.bind_values = merged_binds diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 257221174b..9fcd2d06c5 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 @@ -353,7 +352,7 @@ module ActiveRecord spawn.unscope!(*args) end - def unscope!(*args) + def unscope!(*args) # :nodoc: args.flatten! args.each do |scope| @@ -552,7 +551,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: @@ -934,9 +932,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 { |x| x.strip }.uniq join_list = join_nodes + custom_join_ast(manager, string_joins) 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/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index df090b972d..10c6d272cd 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -118,7 +118,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 +185,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 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..d37d33d552 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -5,8 +5,17 @@ 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 = [] + + def self.default_scopes? + ActiveSupport::Deprecation.warn( + "#default_scopes? is deprecated. Do something like #default_scopes.empty? instead." + ) + + !!self.default_scopes + end end module ClassMethods @@ -27,14 +36,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 @@ -119,11 +120,11 @@ module ActiveRecord 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..da73bead32 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -160,13 +160,8 @@ module ActiveRecord 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 + scope = all.scoping { body.call(*args) } + scope = scope.extending(extension) if extension else scope = body 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/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 4fa7cf8a7d..3e8b79c7a0 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -3,10 +3,41 @@ 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. + # + # 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 LOCAL_HOSTS = ['127.0.0.1', 'localhost'] @@ -15,12 +46,16 @@ 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) + + register_task(/firebird/, ActiveRecord::Tasks::FirebirdDatabaseTasks) + register_task(/sqlserver/, ActiveRecord::Tasks::SqlserverDatabaseTasks) + register_task(/(oci|oracle)/, ActiveRecord::Tasks::OracleDatabaseTasks) 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 +81,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 +104,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 +114,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 +123,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 +148,24 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) end + def check_schema_file(filename) + unless File.exists?(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 +183,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/firebird_database_tasks.rb b/activerecord/lib/active_record/tasks/firebird_database_tasks.rb new file mode 100644 index 0000000000..98014a38ea --- /dev/null +++ b/activerecord/lib/active_record/tasks/firebird_database_tasks.rb @@ -0,0 +1,56 @@ +module ActiveRecord + module Tasks # :nodoc: + class FirebirdDatabaseTasks # :nodoc: + delegate :connection, :establish_connection, to: ActiveRecord::Base + + def initialize(configuration) + ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter." + @configuration = configuration + end + + def create + $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + end + + def drop + $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + end + + def purge + establish_connection(:test) + connection.recreate_database! + end + + def charset + $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + end + + def structure_dump(filename) + set_firebird_env(configuration) + db_string = firebird_db_string(configuration) + Kernel.system "isql -a #{db_string} > #{filename}" + end + + def structure_load(filename) + set_firebird_env(configuration) + db_string = firebird_db_string(configuration) + Kernel.system "isql -i #{filename} #{db_string}" + end + + private + + def set_firebird_env(config) + ENV['ISC_USER'] = config['username'].to_s if config['username'] + ENV['ISC_PASSWORD'] = config['password'].to_s if config['password'] + end + + def firebird_db_string(config) + FireRuby::Database.db_string_for(config.symbolize_keys) + end + + def configuration + @configuration + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/oracle_database_tasks.rb b/activerecord/lib/active_record/tasks/oracle_database_tasks.rb new file mode 100644 index 0000000000..de3aa50e5e --- /dev/null +++ b/activerecord/lib/active_record/tasks/oracle_database_tasks.rb @@ -0,0 +1,45 @@ +module ActiveRecord + module Tasks # :nodoc: + class OracleDatabaseTasks # :nodoc: + delegate :connection, :establish_connection, to: ActiveRecord::Base + + def initialize(configuration) + ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter." + @configuration = configuration + end + + def create + $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + end + + def drop + $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + end + + def purge + establish_connection(:test) + connection.structure_drop.split(";\n\n").each { |ddl| connection.execute(ddl) } + end + + def charset + $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + end + + def structure_dump(filename) + establish_connection(configuration) + File.open(filename, "w:utf-8") { |f| f << connection.structure_dump } + end + + def structure_load(filename) + establish_connection(configuration) + IO.read(filename).split(";\n\n").each { |ddl| connection.execute(ddl) } + end + + private + + def configuration + @configuration + end + end + end +end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 0b1b030516..4413330fab 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 #{filename} #{configuration['database']}") end private diff --git a/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb new file mode 100644 index 0000000000..c718ee03a8 --- /dev/null +++ b/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb @@ -0,0 +1,48 @@ +require 'shellwords' + +module ActiveRecord + module Tasks # :nodoc: + class SqlserverDatabaseTasks # :nodoc: + delegate :connection, :establish_connection, to: ActiveRecord::Base + + def initialize(configuration) + ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter." + @configuration = configuration + end + + def create + $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + end + + def drop + $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + end + + def purge + test = configuration.deep_dup + test_database = test['database'] + test['database'] = 'master' + establish_connection(test) + connection.recreate_database!(test_database) + end + + def charset + $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' + end + + def structure_dump(filename) + Kernel.system("smoscript -s #{configuration['host']} -d #{configuration['database']} -u #{configuration['username']} -p #{configuration['password']} -f #{filename} -A -U") + end + + def structure_load(filename) + Kernel.system("sqlcmd -S #{configuration['host']} -d #{configuration['database']} -U #{configuration['username']} -P #{configuration['password']} -i #{filename}") + end + + private + + def configuration + @configuration + end + end + end +end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 4dbd668fcf..a5955ccba4 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -339,8 +339,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 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/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/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 0af7cbf74f..e28bb7b6ca 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,7 +167,7 @@ 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 diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 5d71effb1f..e6d0183b11 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -21,20 +21,44 @@ class ActiveSchemaTest < ActiveRecord::TestCase ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:define_method, :index_name_exists?) do |*| false end - expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)" + 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}) + + %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) + ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:remove_method, :index_name_exists?) 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..a75883cd3a 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 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..8a2a7ef269 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -21,20 +21,44 @@ class ActiveSchemaTest < ActiveRecord::TestCase ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:define_method, :index_name_exists?) do |*| false end - expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)" + 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}) + + %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) + ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:remove_method, :index_name_exists?) end @@ -70,8 +94,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') 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/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..16329689c0 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -29,9 +29,29 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase false end - 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'") + 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) + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:remove_method, :index_name_exists?) end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 8774bf626f..61a3a2ba0f 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -81,6 +81,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_cycle(['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 + private def assert_cycle array # test creation 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..d7d77f96e2 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -0,0 +1,87 @@ +# 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' + 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) + refute 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) + refute 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) + refute record.new_record? + assert_equal(nil, record.payload) + assert_equal(nil, ByteaDataType.where(id: record.id).first.payload) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index c03660957e..6b726ce875 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -81,42 +81,6 @@ module ActiveRecord assert_equal 'SCHEMA', @connection.logged[0][1] end - def test_reconnection_after_simulated_disconnection_with_verify - assert @connection.active? - original_connection_pid = @connection.query('select pg_backend_pid()') - - # Fail with bad connection on next query attempt. - raw_connection = @connection.raw_connection - raw_connection_class = class << raw_connection ; self ; end - raw_connection_class.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def query_fake(*args) - if !( @called ||= false ) - self.stubs(:status).returns(PGconn::CONNECTION_BAD) - @called = true - raise PGError - else - self.unstub(:status) - query_unfake(*args) - end - end - - alias query_unfake query - alias query query_fake - CODE - - begin - @connection.verify! - new_connection_pid = @connection.query('select pg_backend_pid()') - ensure - raw_connection_class.class_eval <<-CODE, __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" - end - # Must have with_manual_interventions set to true for this # test to run. # When prompted, restart the PostgreSQL server with the diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 1e6ae85a25..8c17372286 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -545,13 +545,19 @@ _SQL def test_update_bit_string new_bit_string = '11111111' - new_bit_string_varying = '11111110' + new_bit_string_varying = '0xFF' assert @first_bit_string.bit_string = new_bit_string assert @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, new_bit_string + 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_update_oid 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..e434b4861c 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -40,25 +40,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 @@ -189,6 +179,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/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 05e0f0e192..17d77c5454 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 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/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 53002c5265..c0c0e8898c 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -35,6 +35,16 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase @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 diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index a5a22bc30b..e78cb88562 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -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,7 +65,7 @@ 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 diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 003052bac4..d51d425c3c 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -25,6 +25,19 @@ module ActiveRecord @conn.intercepted = true end + def test_valid_column + column = @conn.columns('items').find { |col| col.name == 'id' } + assert @conn.valid_type?(column.type) + end + + # sqlite databses 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 = [] 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/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 3a5dea6f13..4aa6567d85 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -302,7 +302,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 @@ -1174,6 +1174,13 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries { assert_equal 5, author.posts.size, "should not cache a subset of the association" } end + 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)" do author = Author.includes(:posts).references(:posts).order(:name).where('posts.title IS NOT NULL').first assert_equal authors(:bob), author diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 781b87741d..d85570236f 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -755,6 +755,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) 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..70c6b489aa 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -583,7 +583,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase 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 @@ -882,6 +882,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 diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 4ed09a3bf7..0e48fbca9c 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -522,4 +522,20 @@ class HasOneAssociationsTest < ActiveRecord::TestCase account = Account.find(2) assert_queries { company.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 + end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index c8cad84013..ec128acf28 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -235,6 +235,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') @@ -303,6 +319,22 @@ class InverseHasManyTests < ActiveRecord::TestCase 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_raise_record_not_found_error_when_invalid_ids_are_passed + man = Man.create! + + invalid_id = 2394823094892348920348523452345 + 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 diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index a06bacafca..95c571fd03 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -245,7 +245,6 @@ class AssociationProxyTest < ActiveRecord::TestCase end class OverridingAssociationsTest < ActiveRecord::TestCase - class Person < ActiveRecord::Base; end class DifferentPerson < ActiveRecord::Base; end class PeopleList < ActiveRecord::Base diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 648596b828..d9c032392d 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -69,7 +69,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 @@ -141,13 +141,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 diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index e5cb4f8f7a..536ff4882c 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 @@ -1335,7 +1335,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? diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 7fb50e9617..bd568af06a 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -21,7 +21,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' @@ -840,7 +839,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 @@ -869,7 +868,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 @@ -880,6 +879,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 @@ -1024,7 +1046,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 @@ -1355,9 +1377,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 @@ -1555,4 +1577,61 @@ 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 + is_set = false + + t = Thread.new do + klass.connection_handler = new_handler + is_set = true + Thread.stop + after_handler = klass.connection_handler + end + + while(!is_set) + Thread.pass + end + + klass.connection_handler = orig_handler + t.wakeup + 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 87559925e7..ba6b0b1362 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -19,7 +19,7 @@ class EachTest < ActiveRecord::TestCase 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 @@ -68,7 +68,7 @@ class EachTest < ActiveRecord::TestCase end end - def test_find_in_batches_shouldnt_excute_query_unless_needed + def test_find_in_batches_shouldnt_execute_query_unless_needed post_count = Post.count assert_queries(2) do diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index c645523905..b0b647cbf7 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -96,25 +96,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 @@ -164,8 +163,7 @@ class CalculationsTest < ActiveRecord::TestCase 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 +198,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] @@ -322,7 +318,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,11 +326,11 @@ 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 @@ -366,7 +362,7 @@ class CalculationsTest < ActiveRecord::TestCase 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 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..3a4f414ae8 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") 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/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 5379a70034..ac093251a5 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -117,6 +117,7 @@ class CounterCacheTest < ActiveRecord::TestCase 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 diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 7b2034dadf..36b87033ae 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -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 diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index dbf6005a67..f73e449610 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -128,7 +128,7 @@ module ActiveRecord prev_default_scopes = Topic.default_scopes Topic.default_scopes = [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..fb53a92c89 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -1,55 +1,54 @@ 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 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_test.rb b/activerecord/test/cases/finder_test.rb index e505fe9f18..557cc7e7a0 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -31,6 +31,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 @@ -833,6 +840,8 @@ 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_finder_with_offset_string diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 8ad40ec3f4..f6cfee0cb8 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -576,6 +576,15 @@ class LoadAllFixturesTest < ActiveRecord::TestCase end end +class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase + self.fixture_path = Pathname.new(FIXTURES_ROOT).join('all') + fixtures :all + + def test_all_there + assert_equal %w(developers people tasks), fixture_table_names.sort + end +end + class FasterFixturesTest < ActiveRecord::TestCase fixtures :categories, :authors diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 99d54e7526..a9be132801 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -194,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 diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 0c896beb1d..77891b9156 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -193,11 +193,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 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_test.rb b/activerecord/test/cases/migration_test.rb index f8afb7c591..193ffb26e3 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -258,6 +258,32 @@ class MigrationTest < ActiveRecord::TestCase "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_not Person.column_methods_hash.include?(: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 + + Person.reset_column_information + assert_not Person.column_methods_hash.include?(: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}" diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 7042d7f4b6..b6e140b912 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -806,7 +806,7 @@ module NestedAttributesOnACollectionAssociationTests 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/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index b936cca875..db3bb56f1f 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 - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys # Oracle UPDATE does not support ORDER BY @@ -247,15 +247,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 +263,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 +296,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' @@ -661,6 +677,15 @@ 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! diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 9ca980fdf6..482c1b3d48 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -1,10 +1,12 @@ 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) end @@ -176,6 +178,21 @@ module ActiveRecord relation.merge!(where: ['foo = ?', 'bar']) assert_equal ['foo = bar'], relation.where_values end + + def test_relation_merging_with_merged_joins + 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 + + 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" + + 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 + end class RelationMutationTest < ActiveSupport::TestCase diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 9008c2785e..cf6af4e8f4 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1299,6 +1299,14 @@ class RelationTest < ActiveRecord::TestCase 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? diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 1147b9a09e..a48ae1036f 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 @@ -219,6 +225,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 +242,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? diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 239004a223..0f69443839 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 @@ -526,6 +198,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) @@ -634,7 +316,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 diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index b593270352..afe32af1d1 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 @@ -271,6 +271,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 @@ -446,4 +459,9 @@ class NamedScopeTest < ActiveRecord::TestCase end assert_equal [posts(:welcome).title], klass.all.map(&:title) end + + 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..765e92ccca 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' @@ -241,4 +242,17 @@ 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) + + Topic.all.each do |topic| + type = topic.instance_variable_get("@columns_hash")["content"] + assert !type.instance_variable_get("@column").is_a?(ActiveRecord::AttributeMethods::Serialization::Type) + end + 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/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/firebird_rake_test.rb b/activerecord/test/cases/tasks/firebird_rake_test.rb new file mode 100644 index 0000000000..c54989ae34 --- /dev/null +++ b/activerecord/test/cases/tasks/firebird_rake_test.rb @@ -0,0 +1,100 @@ +require 'cases/helper' + +unless defined?(FireRuby::Database) +module FireRuby + module Database; end +end +end + +module ActiveRecord + module FirebirdSetupper + def setup + @database = 'db.firebird' + @connection = stub :connection + @configuration = { + 'adapter' => 'firebird', + 'database' => @database + } + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + + @tasks = Class.new(ActiveRecord::Tasks::FirebirdDatabaseTasks) do + def initialize(configuration) + ActiveSupport::Deprecation.silence { super } + end + end + ActiveRecord::Tasks::DatabaseTasks.stubs(:class_for_adapter).returns(@tasks) unless defined? ActiveRecord::ConnectionAdapters::FirebirdAdapter + end + end + + class FirebirdDBCreateTest < ActiveRecord::TestCase + include FirebirdSetupper + + def test_db_retrieves_create + message = capture(:stderr) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + assert_match(/not supported/, message) + end + end + + class FirebirdDBDropTest < ActiveRecord::TestCase + include FirebirdSetupper + + def test_db_retrieves_drop + message = capture(:stderr) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + assert_match(/not supported/, message) + end + end + + class FirebirdDBCharsetAndCollationTest < ActiveRecord::TestCase + include FirebirdSetupper + + def test_db_retrieves_collation + assert_raise NoMethodError do + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end + end + + def test_db_retrieves_charset + message = capture(:stderr) do + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + assert_match(/not supported/, message) + end + end + + class FirebirdStructureDumpTest < ActiveRecord::TestCase + include FirebirdSetupper + + def setup + super + FireRuby::Database.stubs(:db_string_for).returns(@database) + end + + def test_structure_dump + filename = "filebird.sql" + Kernel.expects(:system).with("isql -a #{@database} > #{filename}") + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + end + + class FirebirdStructureLoadTest < ActiveRecord::TestCase + include FirebirdSetupper + + def setup + super + FireRuby::Database.stubs(:db_string_for).returns(@database) + end + + def test_structure_load + filename = "firebird.sql" + Kernel.expects(:system).with("isql -i #{filename} #{@database}") + + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + end +end diff --git a/activerecord/test/cases/tasks/oracle_rake_test.rb b/activerecord/test/cases/tasks/oracle_rake_test.rb new file mode 100644 index 0000000000..5f840febbc --- /dev/null +++ b/activerecord/test/cases/tasks/oracle_rake_test.rb @@ -0,0 +1,93 @@ +require 'cases/helper' + +module ActiveRecord + module OracleSetupper + def setup + @database = 'db.oracle' + @connection = stub :connection + @configuration = { + 'adapter' => 'oracle', + 'database' => @database + } + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + + @tasks = Class.new(ActiveRecord::Tasks::OracleDatabaseTasks) do + def initialize(configuration) + ActiveSupport::Deprecation.silence { super } + end + end + ActiveRecord::Tasks::DatabaseTasks.stubs(:class_for_adapter).returns(@tasks) unless defined? ActiveRecord::ConnectionAdapters::OracleAdapter + end + end + + class OracleDBCreateTest < ActiveRecord::TestCase + include OracleSetupper + + def test_db_retrieves_create + message = capture(:stderr) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + assert_match(/not supported/, message) + end + end + + class OracleDBDropTest < ActiveRecord::TestCase + include OracleSetupper + + def test_db_retrieves_drop + message = capture(:stderr) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + assert_match(/not supported/, message) + end + end + + class OracleDBCharsetAndCollationTest < ActiveRecord::TestCase + include OracleSetupper + + def test_db_retrieves_collation + assert_raise NoMethodError do + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end + end + + def test_db_retrieves_charset + message = capture(:stderr) do + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + assert_match(/not supported/, message) + end + end + + class OracleStructureDumpTest < ActiveRecord::TestCase + include OracleSetupper + + def setup + super + @connection.stubs(:structure_dump).returns("select sysdate from dual;") + end + + def test_structure_dump + filename = "oracle.sql" + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + assert File.exists?(filename) + ensure + FileUtils.rm_f(filename) + end + end + + class OracleStructureLoadTest < ActiveRecord::TestCase + include OracleSetupper + + def test_structure_load + filename = "oracle.sql" + + open(filename, 'w') { |f| f.puts("select sysdate from dual;") } + @connection.stubs(:execute).with("select sysdate from dual;\n") + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + ensure + FileUtils.rm_f(filename) + end + end +end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 3006a87589..f31896bc7f 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -225,9 +225,9 @@ 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 diff --git a/activerecord/test/cases/tasks/sqlserver_rake_test.rb b/activerecord/test/cases/tasks/sqlserver_rake_test.rb new file mode 100644 index 0000000000..0f1264b8ce --- /dev/null +++ b/activerecord/test/cases/tasks/sqlserver_rake_test.rb @@ -0,0 +1,87 @@ +require 'cases/helper' + +module ActiveRecord + module SqlserverSetupper + def setup + @database = 'db.sqlserver' + @connection = stub :connection + @configuration = { + 'adapter' => 'sqlserver', + 'database' => @database, + 'host' => 'localhost', + 'username' => 'username', + 'password' => 'password', + } + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + + @tasks = Class.new(ActiveRecord::Tasks::SqlserverDatabaseTasks) do + def initialize(configuration) + ActiveSupport::Deprecation.silence { super } + end + end + ActiveRecord::Tasks::DatabaseTasks.stubs(:class_for_adapter).returns(@tasks) unless defined? ActiveRecord::ConnectionAdapters::SQLServerAdapter + end + end + + class SqlserverDBCreateTest < ActiveRecord::TestCase + include SqlserverSetupper + + def test_db_retrieves_create + message = capture(:stderr) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + assert_match(/not supported/, message) + end + end + + class SqlserverDBDropTest < ActiveRecord::TestCase + include SqlserverSetupper + + def test_db_retrieves_drop + message = capture(:stderr) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + assert_match(/not supported/, message) + end + end + + class SqlserverDBCharsetAndCollationTest < ActiveRecord::TestCase + include SqlserverSetupper + + def test_db_retrieves_collation + assert_raise NoMethodError do + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end + end + + def test_db_retrieves_charset + message = capture(:stderr) do + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end + assert_match(/not supported/, message) + end + end + + class SqlserverStructureDumpTest < ActiveRecord::TestCase + include SqlserverSetupper + + def test_structure_dump + filename = "sqlserver.sql" + Kernel.expects(:system).with("smoscript -s localhost -d #{@database} -u username -p password -f #{filename} -A -U") + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + end + + class SqlserverStructureLoadTest < ActiveRecord::TestCase + include SqlserverSetupper + + def test_structure_load + filename = "sqlserver.sql" + Kernel.expects(:system).with("sqlcmd -S localhost -d #{@database} -U username -P password -i #{filename}") + + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + end +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..9485de88a6 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -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/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 29b45944aa..57457359b1 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 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/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/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/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/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 8fd4898ad6..8beb58f3fc 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -183,6 +183,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 |