diff options
Diffstat (limited to 'activerecord')
154 files changed, 2575 insertions, 803 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 2eaf388095..3fe8d7ec2e 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,6 +1,321 @@ ## Rails 4.0.0 (unreleased) ## -* Postgresql timestamp with time zone (timestamptz) datatype now returns a +* Allow ActiveRecord::Base.connection_handler to have thread affinity and be + settable, this effectively allows ActiveRecord to be used in a multi threaded + setup with multiple connections to multiple dbs. + + *Sam Saffron* + +* `rename_column` preserves auto_increment in mysql migrations. + Fixes #3493. + + *Vipul A M* + +* PostgreSQL geometric type point is supported by ActiveRecord. Fixes #7324. + + *Martin Schuerrer* + +* Add suport for concurrent indexing in PostgreSQL adapter via the + `algorithm: :concurrently` option + + add_index(:people, :last_name, algorithm: :concurrently) + + Also adds support for MySQL index algorithms (`COPY`, `INPLACE`, + `DEFAULT`) via the `algorithm: :copy` option + + add_index(:people, :last_name, algorithm: :copy) # or :inplace/:default + + *Dan McClain* + +* Add an `add_index` override in Postgresql adapter and MySQL adapter + to allow custom index type support. Fixes #6101. + + add_index(:wikis, :body, :using => 'gin') + + *Stefan Huber* and *Doabit* + +* After extraction of mass-assignment attributes (which protects [id, type] + by default) we can pass id to `update_attributes` and it will update + another record because id will be used in where statement. We never have + to change id in where statement because we try to set/replace fields for + already loaded record but we have to try to set new id for that record. + + *Dmitry Vorotilin* + +* Models with multiple counter cache associations now update correctly on destroy. + See #7706. + + *Ian Young* + +* If `:inverse_of` is true on an association, then when one calls `find()` on + the association, Active Record 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. + + Fixes #9470. + + *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* @@ -230,7 +545,7 @@ *James Miller* -* Allow store accessors to be overrided like other attribute methods, e.g.: +* Allow store accessors to be overridden like other attribute methods, e.g.: class User < ActiveRecord::Base store :settings, accessors: [ :color, :homepage ], coder: JSON @@ -253,11 +568,11 @@ *Dylan Smith* * Schema dumper supports dumping the enabled database extensions to `schema.rb` - (currently only supported by postgresql). + (currently only supported by PostgreSQL). *Justin George* -* The database adpters now converts the options passed thought `DATABASE_URL` +* 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: @@ -356,7 +671,7 @@ *John Wang* -* Collection associations `#empty?` always respects builded records. +* Collection associations `#empty?` always respects built records. Fixes #8879. Example: @@ -408,17 +723,17 @@ *Marc-André Lafortune* * Serialized attributes can be serialized in integer columns. - Fix #8575. + Fixes #8575. *Rafael Mendonça França* * Keep index names when using `alter_table` with sqlite3. - Fix #3489. + Fixes #3489. *Yves Senn* -* Add ability for postgresql adapter to disable user triggers in `disable_referential_integrity`. - Fix #5523. +* Add ability for PostgreSQL adapter to disable user triggers in `disable_referential_integrity`. + Fixes #5523. *Gary S. Weaver* @@ -441,7 +756,7 @@ *Matthew Robertson* * Recognize migrations placed in directories containing numbers and 'rb'. - Fix #8492 + Fixes #8492. *Yves Senn* @@ -499,13 +814,13 @@ * 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. - Fix #8414 + Fixes #8414. *kennyj* * Do not instantiate intermediate Active Record objects when eager loading. These records caused `after_find` to run more than expected. - Fix #3313 + Fixes #3313. *Yves Senn* @@ -526,12 +841,13 @@ * 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. Fix #8310 + result in a change being flagged. + Fixes #8310. *Alisdair McDiarmid* * Prevent mass assignment to the type column of polymorphic associations when using `build` - Fix #8265 + Fixes #8265. *Yves Senn* @@ -540,14 +856,14 @@ *Carlos Antonio da Silva* -* Fix postgresql adapter to handle BC timestamps correctly +* 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. +* 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* @@ -584,7 +900,7 @@ *Bogdan Gusiev* * `:counter_cache` option for `has_many` associations to support custom named counter caches. - Fix #7993 + Fixes #7993. *Yves Senn* @@ -608,7 +924,7 @@ *Nikita Afanasenko* * Use query cache/uncache when using `DATABASE_URL`. - Fix #6951. + Fixes #6951. *kennyj* @@ -617,7 +933,7 @@ *Henrik Nyh* * The `create_table` method raises an `ArgumentError` when the primary key column is redefined. - Fix #6378 + Fixes #6378. *Yves Senn* @@ -727,7 +1043,7 @@ *Alexey Muranov* * The postgres adapter now supports tables with capital letters. - Fix #5920 + Fixes #5920. *Yves Senn* @@ -749,7 +1065,7 @@ *Francesco Rodriguez* * Fix `reset_counters` crashing on `has_many :through` associations. - Fix #7822. + Fixes #7822. *lulalala* @@ -824,7 +1140,7 @@ *Guillermo Iguaran* * Fix the return of querying with an empty hash. - Fix #6971. + Fixes #6971. User.where(token: {}) @@ -840,7 +1156,7 @@ * Fix creation of through association models when using `collection=[]` on a `has_many :through` association from an unsaved model. - Fix #7661. + Fixes #7661. *Ernie Miller* @@ -1179,7 +1495,7 @@ *Egor Lynko* -* Added support for specifying the precision of a timestamp in the postgresql +* 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: @@ -1234,7 +1550,7 @@ * Move HABTM validity checks to `ActiveRecord::Reflection`. One side effect of this is to move when the exceptions are raised from the point of declaration - to when the association is built. This is consistant with other association + to when the association is built. This is consistent with other association validity checks. *Andrew White* @@ -1447,7 +1763,7 @@ * Added the schema cache dump feature. - `Schema cache dump` feature was implemetend. This feature can dump/load internal state of `SchemaCache` instance + `Schema cache dump` feature was implemented. 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: diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 9fc6785d99..ed1e171d58 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -130,7 +130,7 @@ A short rundown of some of the major features: SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html]. -* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]. +* Logging support for Log4r[http://log4r.rubyforge.org] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]. ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) ActiveRecord::Base.logger = Log4r::Logger.new('Application Log') diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 89a62f0873..3e3475f709 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.beta1' + s.add_dependency 'arel', '~> 4.0.0.beta2' s.add_dependency 'activerecord-deprecated_finders', '~> 0.0.3' end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 513d1012ba..eb08f72286 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -241,6 +241,7 @@ module ActiveRecord # others.destroy_all | X | X | X # others.find(*args) | X | X | X # others.exists? | X | X | X + # others.distinct | X | X | X # others.uniq | X | X | X # others.reset | X | X | X # @@ -965,7 +966,7 @@ module ActiveRecord # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they # cause the records in the join table to be removed. # - # For +has_many+, <tt>destroy</tt> and <tt>destory_all</tt> will always call the <tt>destroy</tt> method of the + # For +has_many+, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. @@ -1024,7 +1025,7 @@ module ActiveRecord # [collection<<(object, ...)] # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. # Note that this operation instantly fires update sql without waiting for the save or update call on the - # parent object. + # parent object, unless the parent object is a new record. # [collection.delete(object, ...)] # Removes one or more objects from the collection by setting their foreign keys to +NULL+. # Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>, @@ -1115,7 +1116,7 @@ module ActiveRecord # :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 asssociated objects to be deleted directly from the database (so callbacks will not execute) + # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not execute) # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed. # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects @@ -1231,7 +1232,7 @@ module ActiveRecord # its owner is destroyed: # # * <tt>:destroy</tt> causes the associated object to also be destroyed - # * <tt>:delete</tt> causes the asssociated object to be deleted directly from the database (so callbacks will not execute) + # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute) # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object @@ -1407,6 +1408,8 @@ module ActiveRecord # to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the # custom <tt>:join_table</tt> option if you need to. + # If your tables share a common prefix, it will only appear once at the beginning. For example, + # the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products". # # The join table should not have a primary key or a model associated with it. You must manually generate the # join table with a migration such as this: @@ -1433,7 +1436,7 @@ module ActiveRecord # Adds one or more objects to the collection by creating associations in the join table # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method). # Note that this operation instantly fires update sql without waiting for the save or update call on the - # parent object. + # parent object, unless the parent object is a new record. # [collection.delete(object, ...)] # Removes one or more objects from the collection by removing their associations from the join table. # This does not destroy the objects. diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 868095f068..4c4b0f08e5 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -203,7 +203,7 @@ module ActiveRecord # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of # the kind of the class of the associated objects. Meant to be used as # a sanity check when you are about to assign an associated record. - def raise_on_type_mismatch(record) + def raise_on_type_mismatch!(record) unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize) message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" raise ActiveRecord::AssociationTypeMismatch, message diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index c5fb1fe2c7..a9525436fb 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -22,7 +22,7 @@ module ActiveRecord private def column_for(table_name, column_name) - columns = alias_tracker.connection.schema_cache.columns_hash[table_name] + columns = alias_tracker.connection.schema_cache.columns_hash(table_name) columns[column_name] end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 54b1a69774..8eec4f56af 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -8,7 +8,7 @@ module ActiveRecord end def replace(record) - raise_on_type_mismatch(record) if record + raise_on_type_mismatch!(record) if record update_counters(record) replace_keys(record) diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 88ce03a3cd..eae5eed3a1 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -22,7 +22,7 @@ module ActiveRecord reflection.polymorphic_inverse_of(record.class) end - def raise_on_type_mismatch(record) + def raise_on_type_mismatch!(record) # A polymorphic association cannot have a type mismatch, by definition end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 97b1ff18e2..9ac561b997 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -21,23 +21,43 @@ module ActiveRecord::Associations::Builder def add_counter_cache_callbacks(reflection) cache_column = reflection.counter_cache_column + foreign_key = reflection.foreign_key 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 end def belongs_to_counter_cache_before_destroy_for_#{name} - unless marked_for_destruction? + unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == #{foreign_key.to_sym.inspect} record = #{name} record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil? end end + + def belongs_to_counter_cache_after_update_for_#{name} + if (@_after_create_counter_called ||= false) + @_after_create_counter_called = false + elsif self.#{foreign_key}_changed? && !new_record? && defined?(#{name.to_s.camelize}) + model = #{name.to_s.camelize} + foreign_key_was = self.#{foreign_key}_was + foreign_key = self.#{foreign_key} + + if foreign_key && model.respond_to?(:increment_counter) + model.increment_counter(:#{cache_column}, foreign_key) + end + if foreign_key_was && model.respond_to?(:decrement_counter) + model.decrement_counter(:#{cache_column}, foreign_key_was) + end + end + end CODE model.after_create "belongs_to_counter_cache_after_create_for_#{name}" model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}" + model.after_update "belongs_to_counter_cache_after_update_for_#{name}" klass = reflection.class_name.safe_constantize klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 5feb149946..2385c90c1a 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -34,7 +34,7 @@ module ActiveRecord reload end - CollectionProxy.new(klass, self) + @proxy ||= CollectionProxy.new(klass, self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -79,7 +79,7 @@ module ActiveRecord if block_given? load_target.find(*args) { |*block_args| yield(*block_args) } else - if options[:finder_sql] + if options[:finder_sql] || options[:inverse_of] find_by_scan(*args) else scope.find(*args) @@ -174,13 +174,14 @@ module ActiveRecord reflection.klass.count_by_sql(custom_counter_sql) else - if association_scope.uniq_value + relation = scope + if association_scope.distinct_value # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. column_name ||= reflection.klass.primary_key - count_options[:distinct] = true + relation = relation.distinct end - value = scope.count(column_name, count_options) + value = relation.count(column_name) limit = options[:limit] offset = options[:offset] @@ -204,6 +205,15 @@ module ActiveRecord dependent = options[:dependent] if records.first == :all + + if dependent && dependent == :destroy + message = 'In Rails 4.1 delete_all on associations would not fire callbacks. ' \ + 'It means if the :dependent option is :destroy then the associated ' \ + 'records would be deleted without loading and invoking callbacks.' + + ActiveRecord::Base.logger ? ActiveRecord::Base.logger.warn(message) : $stderr.puts(message) + end + if loaded? || dependent == :destroy delete_or_destroy(load_target, dependent) else @@ -237,14 +247,14 @@ module ActiveRecord # +count_records+, which is a method descendants have to provide. def size if !find_target? || loaded? - if association_scope.uniq_value + if association_scope.distinct_value target.uniq.size else target.size end elsif !loaded? && !association_scope.group_values.empty? load_target.size - elsif !loaded? && !association_scope.uniq_value && target.is_a?(Array) + elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array) unsaved_records = target.select { |r| r.new_record? } unsaved_records.size + count_records else @@ -297,17 +307,18 @@ module ActiveRecord end end - def uniq + def distinct seen = {} load_target.find_all do |record| seen[record.id] = true unless seen.key?(record.id) end end + alias uniq distinct # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. def replace(other_array) - other_array.each { |val| raise_on_type_mismatch(val) } + other_array.each { |val| raise_on_type_mismatch!(val) } original_target = load_target.dup if owner.new_record? @@ -343,7 +354,7 @@ module ActiveRecord callback(:before_add, record) yield(record) if block_given? - if association_scope.uniq_value && index = @target.index(record) + if association_scope.distinct_value && index = @target.index(record) @target[index] = record else @target << record @@ -454,7 +465,7 @@ module ActiveRecord def delete_or_destroy(records, method) records = records.flatten - records.each { |record| raise_on_type_mismatch(record) } + records.each { |record| raise_on_type_mismatch!(record) } existing_records = records.reject { |r| r.new_record? } if existing_records.empty? @@ -495,9 +506,9 @@ module ActiveRecord result = true records.flatten.each do |record| - raise_on_type_mismatch(record) - add_to_target(record) do |r| - result &&= insert_record(record) unless owner.new_record? + raise_on_type_mismatch!(record) + add_to_target(record) do |rec| + result &&= insert_record(rec) unless owner.new_record? end end @@ -556,7 +567,8 @@ module ActiveRecord end end - # If using a custom finder_sql, #find scans the entire collection. + # If using a custom finder_sql or if the :inverse_of option has been + # specified, then #find scans the entire collection. def find_by_scan(*args) expects_array = args.first.kind_of?(Array) ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 543204abac..c2add32aa6 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -649,11 +649,12 @@ module ActiveRecord # # #<Pet name: "Fancy-Fancy"> # # ] # - # person.pets.select(:name).uniq + # person.pets.select(:name).distinct # # => [#<Pet name: "Fancy-Fancy">] - def uniq - @association.uniq + def distinct + @association.distinct end + alias uniq distinct # Count all records using SQL. # diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 93618721bb..bb3e3db379 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -26,7 +26,7 @@ module ActiveRecord join_table[reflection.association_foreign_key] => record.id ) - owner.connection.insert stmt + owner.class.connection.insert stmt end record @@ -41,7 +41,7 @@ module ActiveRecord def delete_records(records, method) if sql = options[:delete_sql] records = load_target if records == :all - records.each { |record| owner.connection.delete(interpolate(sql, record)) } + records.each { |record| owner.class.connection.delete(interpolate(sql, record)) } else relation = join_table condition = relation[reflection.foreign_key].eq(owner.id) @@ -53,7 +53,7 @@ module ActiveRecord ) end - owner.connection.delete(relation.where(condition).compile_delete) + owner.class.connection.delete(relation.where(condition).compile_delete) end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index f59565ae77..29fae809da 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -22,10 +22,11 @@ module ActiveRecord else if options[:dependent] == :destroy # No point in executing the counter update since we're going to destroy the parent anyway - load_target.each(&:mark_for_destruction) + load_target.each { |t| t.destroyed_by_association = reflection } + destroy_all + else + delete_all end - - delete_all end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index d1458f30ba..a74dd1cdab 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -29,7 +29,7 @@ module ActiveRecord def concat(*records) unless owner.new_record? records.flatten.each do |record| - raise_on_type_mismatch(record) + raise_on_type_mismatch!(record) record.save! if record.new_record? end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index ee816d2392..98bd010f70 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -22,7 +22,7 @@ module ActiveRecord end def replace(record, save = true) - raise_on_type_mismatch(record) if record + raise_on_type_mismatch!(record) if record load_target # If target and record are nil, or target is equal to record, diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb index 8e8925f0a9..9a3fada380 100644 --- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb @@ -35,7 +35,7 @@ module ActiveRecord # record def associated_records_by_owner records = {} - super.each do |owner_key, rows| + super.each_value do |rows| rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) } end end diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb index 9a662d3f53..38bc7ce7da 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -6,7 +6,7 @@ module ActiveRecord def associated_records_by_owner super.each do |owner, records| - records.uniq! if reflection_scope.uniq_value + records.uniq! if reflection_scope.distinct_value end end end 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 e0bfdb8f3e..22405c5d74 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -21,7 +21,7 @@ module ActiveRecord # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods # :nodoc: - # Use a mutex; we don't want two thread simaltaneously trying to define + # Use a mutex; we don't want two thread simultaneously trying to define # attribute methods. @attribute_methods_mutex.synchronize do return if attribute_methods_generated? @@ -334,7 +334,7 @@ module ActiveRecord 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 +348,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/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 616ae1631f..6315dd9549 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -107,7 +107,11 @@ module ActiveRecord def changes_from_zero_to_string?(old, value) # For columns with old 0 and value non-empty string - old == 0 && value.is_a?(String) && value.present? && value != '0' + old == 0 && value.is_a?(String) && value.present? && non_zero?(value) + end + + def non_zero?(value) + value !~ /\A0+(\.0+)?\z/ end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 3e454b713a..931209b07b 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -90,7 +90,7 @@ module ActiveRecord base_name.foreign_key else if ActiveRecord::Base != self && table_exists? - connection.schema_cache.primary_keys[table_name] + connection.schema_cache.primary_keys(table_name) else 'id' end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 55542262b0..0df3e57947 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -212,6 +212,7 @@ module ActiveRecord # Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag. def reload(options = nil) @marked_for_destruction = false + @destroyed_by_association = nil super end @@ -231,6 +232,19 @@ module ActiveRecord @marked_for_destruction end + # Records the association that is being destroyed and destroying this + # record in the process. + def destroyed_by_association=(reflection) + @destroyed_by_association = reflection + end + + # Returns the association for the parent being destroyed. + # + # Used to avoid updating the counter cache unnecessarily. + def destroyed_by_association + @destroyed_by_association + end + # Returns whether or not this record has been changed in any way (including whether # any of its nested autosave associations are likewise changed) def changed_for_autosave? diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index f6cdc67b4d..8d22942a06 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 @@ -34,7 +33,7 @@ module ActiveRecord obj ||= object_class.new if object_class != Object obj - rescue *RESCUE_ERRORS + rescue ArgumentError, Psych::SyntaxError yaml end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 1754e424b8..bf2f945448 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -406,7 +406,9 @@ module ActiveRecord synchronize do stale = Time.now - @dead_connection_timeout connections.dup.each do |conn| - remove conn if conn.in_use? && stale > conn.last_use && !conn.active? + if conn.in_use? && stale > conn.last_use && !conn.active? + remove conn + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 2859fb31e8..c0a2111571 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -21,7 +21,7 @@ module ActiveRecord # limit is enforced by rails and Is less than or equal to # <tt>index_name_length</tt>. The gap between # <tt>index_name_length</tt> is to allow internal rails - # opreations to use prefixes in temporary opreations. + # operations to use prefixes in temporary operations. def allowed_index_name_length index_name_length end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index c3d15ca929..c64b542286 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -125,7 +125,8 @@ module ActiveRecord # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html - # Savepoints are supported by MySQL and PostgreSQL, but not SQLite3. + # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' + # supports savepoints. # # It is safe to call this method if a database transaction is already open, # i.e. if #transaction is called within another #transaction block. In case diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 42206de8fc..2f17e47b7c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -8,37 +8,21 @@ module ActiveRecord # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where) #:nodoc: + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :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 to_sql - column_sql = "#{base.quote_column_name(name)} #{sql_type}" - column_options = {} - column_options[:null] = null unless null.nil? - column_options[:default] = default unless default.nil? - add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key - column_sql + def primary_key? + primary_key || type.to_sym == :primary_key end - - private - - def add_column_options!(sql, options) - base.add_column_options!(sql, options.merge(:column => self)) - end end # Represents the schema of an SQL table in an abstract way. This class @@ -64,28 +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 xml(*args) - raise NotImplementedError unless %w{ - sqlite mysql mysql2 - }.include? @base.adapter_name.downcase - - options = args.extract_options! - column(args[0], :text, options) - end + def columns; @columns_hash.values; end # Appends a primary key definition to the table definition. # Can be called multiple times, but this is probably not a good idea. - def primary_key(name) - column(name, :primary_key) + def primary_key(name, type = :primary_key, options = {}) + options[:primary_key] = true + column(name, type, options) end # Returns a ColumnDefinition for the column with name +name+. @@ -238,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! @@ -288,28 +263,52 @@ module ActiveRecord end alias :belongs_to :references - # Returns a String whose contents are the column definitions - # concatenated together. This string can then be prepended and appended to - # to generate the final SQL to create the table. - def to_sql - @columns.map { |c| c.to_sql } * ', ' + def new_column_definition(name, type, options) # :nodoc: + column = create_column_definition name, type + limit = options.fetch(:limit) do + native[type][:limit] if native[type].is_a?(Hash) + end + + column.limit = limit + column.precision = options[:precision] + column.scale = options[:scale] + column.default = options[:default] + column.null = options[:null] + column.first = options[:first] + column.after = options[:after] + column.primary_key = type == :primary_key || options[:primary_key] + column end private - def new_column_definition(base, name, type) - definition = ColumnDefinition.new base, name, type - @columns << definition - @columns_hash[name] = definition - definition + def create_column_definition(name, type) + ColumnDefinition.new name, type end def primary_key_column_name - primary_key_column = columns.detect { |c| c.type == :primary_key } + primary_key_column = columns.detect { |c| c.primary_key? } primary_key_column && primary_key_column.name end def native - @base.native_database_types + @native + end + end + + class AlterTable # :nodoc: + attr_reader :adds + + def initialize(td) + @td = td + @adds = [] + end + + def name; @td.name; end + + def add_column(name, type, options) + name = name.to_s + type = type.to_sym + @adds << @td.new_column_definition(name, type, options) end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index f587bf8140..cdf0cbe218 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -5,7 +5,7 @@ module ActiveRecord # The goal of this module is to move Adapter specific column # definitions to the Adapter instead of having it in the schema # dumper itself. This code represents the normal case. - # We can then redefine how certain data types may be handled in the schema dumper on the + # We can then redefine how certain data types may be handled in the schema dumper on the # Adapter level by over-writing this code inside the database specific adapters module ColumnDumper def column_spec(column, types) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 0cce8c7596..e95e97e4a8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -5,7 +5,7 @@ module ActiveRecord module SchemaStatements include ActiveRecord::Migration::JoinTable - # Returns a Hash of mappings from the abstract data types to the native + # Returns a hash of mappings from the abstract data types to the native # database types. See TableDefinition#column for details on the recognized # abstract data types. def native_database_types @@ -20,6 +20,7 @@ module ActiveRecord # Checks to see if the table +table_name+ exists on the database. # # table_exists?(:developers) + # def table_exists?(table_name) tables.include?(table_name.to_s) end @@ -29,17 +30,18 @@ module ActiveRecord # Checks to see if an index exists on a table for a given index definition. # - # # Check an index exists - # index_exists?(:suppliers, :company_id) + # # Check an index exists + # index_exists?(:suppliers, :company_id) + # + # # Check an index on multiple columns exists + # index_exists?(:suppliers, [:company_id, :company_type]) # - # # Check an index on multiple columns exists - # index_exists?(:suppliers, [:company_id, :company_type]) + # # Check a unique index exists + # index_exists?(:suppliers, :company_id, unique: true) # - # # Check a unique index exists - # index_exists?(:suppliers, :company_id, unique: true) + # # Check an index with a custom name exists + # index_exists?(:suppliers, :company_id, name: "idx_company_id" # - # # Check an index with a custom name exists - # index_exists?(:suppliers, :company_id, name: "idx_company_id" def index_exists?(table_name, column_name, options = {}) column_names = Array(column_name) index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names) @@ -56,17 +58,18 @@ module ActiveRecord # Checks to see if a column exists in a given table. # - # # Check a column exists - # column_exists?(:suppliers, :name) + # # Check a column exists + # column_exists?(:suppliers, :name) # - # # Check a column exists of a particular type - # column_exists?(:suppliers, :name, :string) + # # Check a column exists of a particular type + # column_exists?(:suppliers, :name, :string) + # + # # Check a column exists with a specific definition + # column_exists?(:suppliers, :name, :string, limit: 100) + # column_exists?(:suppliers, :name, :string, default: 'default') + # column_exists?(:suppliers, :name, :string, null: false) + # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) # - # # Check a column exists with a specific definition - # column_exists?(:suppliers, :name, :string, limit: 100) - # column_exists?(:suppliers, :name, :string, default: 'default') - # column_exists?(:suppliers, :name, :string, null: false) - # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) def column_exists?(table_name, column_name, type = nil, options = {}) columns(table_name).any?{ |c| c.name == column_name.to_s && (!type || c.type == type) && @@ -84,27 +87,30 @@ module ActiveRecord # form or the regular form, like this: # # === Block form - # # create_table() passes a TableDefinition object to the block. - # # This form will not only create the table, but also columns for the - # # table. # - # create_table(:suppliers) do |t| - # t.column :name, :string, limit: 60 - # # Other fields here - # end + # # create_table() passes a TableDefinition object to the block. + # # This form will not only create the table, but also columns for the + # # table. + # + # create_table(:suppliers) do |t| + # t.column :name, :string, limit: 60 + # # Other fields here + # end # # === Block form, with shorthand - # # You can also use the column types as method calls, rather than calling the column method. - # create_table(:suppliers) do |t| - # t.string :name, limit: 60 - # # Other fields here - # end + # + # # You can also use the column types as method calls, rather than calling the column method. + # create_table(:suppliers) do |t| + # t.string :name, limit: 60 + # # Other fields here + # end # # === Regular form - # # Creates a table called 'suppliers' with no columns. - # create_table(:suppliers) - # # Add a column to 'suppliers'. - # add_column(:suppliers, :name, :string, {limit: 60}) + # + # # Creates a table called 'suppliers' with no columns. + # create_table(:suppliers) + # # Add a column to 'suppliers'. + # add_column(:suppliers, :name, :string, {limit: 60}) # # The +options+ hash can include the following keys: # [<tt>:id</tt>] @@ -127,37 +133,53 @@ module ActiveRecord # Defaults to false. # # ====== Add a backend specific option to the generated SQL (MySQL) - # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # + # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # # generates: - # CREATE TABLE suppliers ( - # id int(11) DEFAULT NULL auto_increment PRIMARY KEY - # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # + # CREATE TABLE suppliers ( + # id int(11) DEFAULT NULL auto_increment PRIMARY KEY + # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # # ====== Rename the primary key column - # create_table(:objects, primary_key: 'guid') do |t| - # t.column :name, :string, limit: 80 - # end + # + # create_table(:objects, primary_key: 'guid') do |t| + # t.column :name, :string, limit: 80 + # end + # # generates: - # CREATE TABLE objects ( - # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, - # name varchar(80) - # ) + # + # CREATE TABLE objects ( + # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, + # name varchar(80) + # ) # # ====== Do not add a primary key column - # create_table(:categories_suppliers, id: false) do |t| - # t.column :category_id, :integer - # t.column :supplier_id, :integer - # end + # + # create_table(:categories_suppliers, id: false) do |t| + # t.column :category_id, :integer + # t.column :supplier_id, :integer + # end + # # generates: - # CREATE TABLE categories_suppliers ( - # category_id int, - # supplier_id int - # ) + # + # CREATE TABLE categories_suppliers ( + # category_id int, + # supplier_id int + # ) # # See also TableDefinition#column for details on how to create columns. def create_table(table_name, options = {}) - td = create_table_definition - td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false + td = create_table_definition table_name, options[:temporary], options[:options] + + unless options[:id] == false + pk = options.fetch(:primary_key) { + Base.get_primary_key table_name.to_s.singularize + } + + td.primary_key pk, options.fetch(:id, :primary_key), options + end yield td if block_given? @@ -165,19 +187,15 @@ module ActiveRecord drop_table(table_name, options) end - create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE " - create_sql << "#{quote_table_name(table_name)} (" - create_sql << td.to_sql - create_sql << ") #{options[:options]}" - execute create_sql + execute schema_creation.accept td td.indexes.each_pair { |c,o| add_index table_name, c, o } end # Creates a new join table with the name created using the lexical order of the first two # arguments. These arguments can be a String or a Symbol. # - # # Creates a table called 'assemblies_parts' with no id. - # create_join_table(:assemblies, :parts) + # # Creates a table called 'assemblies_parts' with no id. + # create_join_table(:assemblies, :parts) # # You can pass a +options+ hash can include the following keys: # [<tt>:table_name</tt>] @@ -201,12 +219,16 @@ module ActiveRecord # end # # ====== Add a backend specific option to the generated SQL (MySQL) - # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # + # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # # generates: - # CREATE TABLE assemblies_parts ( - # assembly_id int NOT NULL, - # part_id int NOT NULL, - # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # + # CREATE TABLE assemblies_parts ( + # assembly_id int NOT NULL, + # part_id int NOT NULL, + # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # def create_join_table(table_1, table_2, options = {}) join_table_name = find_join_table_name(table_1, table_2, options) @@ -223,7 +245,7 @@ module ActiveRecord end # Drops the join table specified by the given arguments. - # See create_join_table for details. + # See +create_join_table+ for details. # # Although this command ignores the block if one is given, it can be helpful # to provide one in a migration's +change+ method so it can be reverted. @@ -235,66 +257,74 @@ module ActiveRecord # A block for changing columns in +table+. # - # # change_table() yields a Table instance - # change_table(:suppliers) do |t| - # t.column :name, :string, limit: 60 - # # Other column alterations here - # end + # # change_table() yields a Table instance + # change_table(:suppliers) do |t| + # t.column :name, :string, limit: 60 + # # Other column alterations here + # end # # The +options+ hash can include the following keys: # [<tt>:bulk</tt>] # Set this to true to make this a bulk alter query, such as - # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... + # + # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... # # Defaults to false. # # ====== Add a column - # change_table(:suppliers) do |t| - # t.column :name, :string, limit: 60 - # end + # + # change_table(:suppliers) do |t| + # t.column :name, :string, limit: 60 + # end # # ====== Add 2 integer columns - # change_table(:suppliers) do |t| - # t.integer :width, :height, null: false, default: 0 - # end + # + # change_table(:suppliers) do |t| + # t.integer :width, :height, null: false, default: 0 + # end # # ====== Add created_at/updated_at columns - # change_table(:suppliers) do |t| - # t.timestamps - # end + # + # change_table(:suppliers) do |t| + # t.timestamps + # end # # ====== Add a foreign key column - # change_table(:suppliers) do |t| - # t.references :company - # end # - # Creates a <tt>company_id(integer)</tt> column + # change_table(:suppliers) do |t| + # t.references :company + # end + # + # Creates a <tt>company_id(integer)</tt> column. # # ====== Add a polymorphic foreign key column + # # change_table(:suppliers) do |t| # t.belongs_to :company, polymorphic: true # end # - # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns + # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns. # # ====== Remove a column + # # change_table(:suppliers) do |t| # t.remove :company # end # # ====== Remove several columns + # # change_table(:suppliers) do |t| # t.remove :company_id # t.remove :width, :height # end # # ====== Remove an index + # # change_table(:suppliers) do |t| # t.remove_index :company_id # end # - # See also Table for details on - # all of the various column transformation + # See also Table for details on all of the various column transformation. def change_table(table_name, options = {}) if supports_bulk_alter? && options[:bulk] recorder = ActiveRecord::Migration::CommandRecorder.new(self) @@ -307,7 +337,8 @@ module ActiveRecord # Renames a table. # - # rename_table('octopuses', 'octopi') + # rename_table('octopuses', 'octopi') + # def rename_table(table_name, new_name) raise NotImplementedError, "rename_table is not implemented" end @@ -324,14 +355,15 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. def add_column(table_name, column_name, type, options = {}) - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - execute(add_column_sql) + at = create_alter_table table_name + at.add_column(column_name, type, options) + execute schema_creation.accept at end # Removes the given columns from the table definition. # - # remove_columns(:suppliers, :qualification, :experience) + # remove_columns(:suppliers, :qualification, :experience) + # def remove_columns(table_name, *column_names) raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.empty? column_names.each do |column_name| @@ -341,7 +373,7 @@ module ActiveRecord # Removes the column from the table definition. # - # remove_column(:suppliers, :qualification) + # remove_column(:suppliers, :qualification) # # The +type+ and +options+ parameters will be ignored if present. It can be helpful # to provide these in a migration's +change+ method so it can be reverted. @@ -353,24 +385,50 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. # - # change_column(:suppliers, :name, :string, limit: 80) - # change_column(:accounts, :description, :text) + # change_column(:suppliers, :name, :string, limit: 80) + # change_column(:accounts, :description, :text) + # def change_column(table_name, column_name, type, options = {}) raise NotImplementedError, "change_column is not implemented" end - # Sets a new default value for a column. + # Sets a new default value for a column: + # + # change_column_default(:suppliers, :qualification, 'new') + # change_column_default(:accounts, :authorized, 1) + # + # Setting the default to +nil+ effectively drops the default: + # + # change_column_default(:users, :email, nil) # - # change_column_default(:suppliers, :qualification, 'new') - # change_column_default(:accounts, :authorized, 1) - # change_column_default(:users, :email, nil) def change_column_default(table_name, column_name, default) raise NotImplementedError, "change_column_default is not implemented" end + # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag + # indicates whether the value can be +NULL+. For example + # + # change_column_null(:users, :nickname, false) + # + # says nicknames cannot be +NULL+ (adds the constraint), whereas + # + # change_column_null(:users, :nickname, true) + # + # allows them to be +NULL+ (drops the constraint). + # + # The method accepts an optional fourth argument to replace existing + # +NULL+s with some other value. Use that one when enabling the + # constraint if needed, since otherwise those rows would not be valid. + # + # Please note the fourth argument does not set a column's default. + def change_column_null(table_name, column_name, null, default = nil) + raise NotImplementedError, "change_column_null is not implemented" + end + # Renames a column. # - # rename_column(:suppliers, :description, :name) + # rename_column(:suppliers, :description, :name) + # def rename_column(table_name, column_name, new_column_name) raise NotImplementedError, "rename_column is not implemented" end @@ -382,60 +440,94 @@ module ActiveRecord # you pass <tt>:name</tt> as an option. # # ====== Creating a simple index - # add_index(:suppliers, :name) + # + # add_index(:suppliers, :name) + # # generates - # CREATE INDEX suppliers_name_index ON suppliers(name) + # + # CREATE INDEX suppliers_name_index ON suppliers(name) # # ====== Creating a unique index - # add_index(:accounts, [:branch_id, :party_id], unique: true) + # + # add_index(:accounts, [:branch_id, :party_id], unique: true) + # # generates - # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id) + # + # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id) # # ====== Creating a named index - # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party') + # + # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party') + # # generates + # # CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id) # # ====== Creating an index with specific key length - # add_index(:accounts, :name, name: 'by_name', length: 10) + # + # add_index(:accounts, :name, name: 'by_name', length: 10) + # # generates - # CREATE INDEX by_name ON accounts(name(10)) # - # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) + # CREATE INDEX by_name ON accounts(name(10)) + # + # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15}) + # # generates - # CREATE INDEX by_name_surname ON accounts(name(10), surname(15)) # - # Note: SQLite doesn't support index length + # CREATE INDEX by_name_surname ON accounts(name(10), surname(15)) + # + # Note: SQLite doesn't support index length. # # ====== Creating an index with a sort order (desc or asc, asc is the default) - # add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc}) + # + # add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc}) + # # generates - # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # - # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it) + # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) + # + # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it). # # ====== Creating a partial index - # add_index(:accounts, [:branch_id, :party_id], unique: true, where: "active") + # + # add_index(:accounts, [:branch_id, :party_id], unique: true, where: "active") + # + # generates + # + # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active + # + # ====== Creating an index with a specific method + # add_index(:developers, :name, :using => 'btree') # generates - # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active + # 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 + # Note: only supported by PostgreSQL and MySQL # def add_index(table_name, column_name, options = {}) index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options) execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}" end - # Remove the given index from the table. + # Removes the given index from the table. + # + # Removes the +index_accounts_on_column+ in the +accounts+ table. # - # Remove the index_accounts_on_column in the accounts table. # remove_index :accounts, :column - # Remove the index named index_accounts_on_branch_id in the accounts table. + # + # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table. + # # remove_index :accounts, column: :branch_id - # Remove the index named index_accounts_on_branch_id_and_party_id in the accounts table. + # + # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table. + # # remove_index :accounts, column: [:branch_id, :party_id] - # Remove the index named by_branch_party in the accounts table. + # + # Removes the index named +by_branch_party+ in the +accounts+ table. + # # remove_index :accounts, name: :by_branch_party + # def remove_index(table_name, options = {}) remove_index!(table_name, index_name_for_remove(table_name, options)) end @@ -444,10 +536,12 @@ module ActiveRecord execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" end - # Rename an index. + # Renames an index. + # + # Rename the +index_people_on_last_name+ index to +index_users_on_last_name+: # - # Rename the index_people_on_last_name index to index_users_on_last_name # rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name' + # def rename_index(table_name, old_name, new_name) # this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance) old_index_def = indexes(table_name).detect { |i| i.name == old_name } @@ -470,7 +564,7 @@ module ActiveRecord end end - # Verify the existence of an index with a given name. + # Verifies the existence of an index with a given name. # # The default argument is returned if the underlying implementation does not define the indexes method, # as there's no way to determine the correct answer in that case. @@ -484,13 +578,16 @@ module ActiveRecord # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. # # ====== Create a user_id column - # add_reference(:products, :user) + # + # add_reference(:products, :user) # # ====== Create a supplier_id and supplier_type columns - # add_belongs_to(:products, :supplier, polymorphic: true) + # + # add_belongs_to(:products, :supplier, polymorphic: true) # # ====== Create a supplier_id, supplier_type columns and appropriate index - # add_reference(:products, :supplier, polymorphic: true, index: true) + # + # add_reference(:products, :supplier, polymorphic: true, index: true) # def add_reference(table_name, ref_name, options = {}) polymorphic = options.delete(:polymorphic) @@ -505,10 +602,12 @@ module ActiveRecord # <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. # # ====== Remove the reference - # remove_reference(:products, :user, index: true) + # + # remove_reference(:products, :user, index: true) # # ====== Remove polymorphic reference - # remove_reference(:products, :supplier, polymorphic: true) + # + # remove_reference(:products, :supplier, polymorphic: true) # def remove_reference(table_name, ref_name, options = {}) remove_column(table_name, "#{ref_name}_id") @@ -516,11 +615,6 @@ module ActiveRecord end alias :remove_belongs_to :remove_reference - # Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the - # entire structure of the database. - def structure_dump - end - def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name @@ -594,27 +688,33 @@ 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. # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax. # # distinct("posts.id", "posts.created_at desc") + # def distinct(columns, order_by) "DISTINCT #{columns}" end - # Adds timestamps (created_at and updated_at) columns to the named table. + # Adds timestamps (+created_at+ and +updated_at+) columns to the named table. + # + # add_timestamps(:suppliers) # - # add_timestamps(:suppliers) def add_timestamps(table_name) add_column table_name, :created_at, :datetime add_column table_name, :updated_at, :datetime end - # Removes the timestamp columns (created_at and updated_at) from the table definition. + # Removes the timestamp columns (+created_at+ and +updated_at+) from the table definition. # # remove_timestamps(:suppliers) + # def remove_timestamps(table_name) remove_column table_name, :updated_at remove_column table_name, :created_at @@ -655,12 +755,20 @@ 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) index_type = options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length + if index_algorithms.key?(options[:algorithm]) + algorithm = index_algorithms[options[:algorithm]] + elsif options[:algorithm].present? + 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 @@ -675,6 +783,7 @@ module ActiveRecord index_type = options max_index_length = allowed_index_name_length + algorithm = using = nil end if index_name.length > max_index_length @@ -685,13 +794,21 @@ 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 = {}) index_name = index_name(table_name, options) unless index_name_exists?(table_name, index_name, true) + if options.is_a?(Hash) && options.has_key?(:name) + options_without_column = options.dup + options_without_column.delete :column + index_name_without_column = index_name(table_name, options_without_column) + + return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) + end + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" end @@ -727,8 +844,12 @@ module ActiveRecord end private - def create_table_definition - TableDefinition.new(self) + def create_table_definition(name, temporary, options) + TableDefinition.new native_database_types, name, temporary, options + end + + def create_alter_table(name) + AlterTable.new create_table_definition(name, false, {}) end def update_table_definition(table_name, base) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 73c80a3220..2b6685499a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -78,7 +78,7 @@ module ActiveRecord @joinable = options.fetch(:joinable, true) end - # This state is necesarry so that we correctly handle stuff that might + # This state is necessary so that we correctly handle stuff that might # happen in a commit/rollback. But it's kinda distasteful. Maybe we can # find a better way to structure it in the future. def finishing? diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ff9de712bc..1915c444ef 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,75 @@ module ActiveRecord @visitor = nil 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 @@ -118,6 +188,17 @@ module ActiveRecord @in_use = false end + def unprepared_visitor + self.class::BindSubstitution.new self + end + + def unprepared_statement + old, @visitor = @visitor, unprepared_visitor + yield + ensure + @visitor = old + end + # Returns the human-readable name of the adapter. Use mixed case - one # can always use downcase if needed. def adapter_name @@ -201,6 +282,13 @@ module ActiveRecord [] end + # A list of index algorithms, to be filled by adapters that + # support them. MySQL and PostgreSQL has support for them right + # now. + def index_algorithms + {} + end + # QUOTING ================================================== # Returns a bind substitution value given a +column+ and list of current 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 9826b18053..cc5e6ac44d 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 @@ -143,7 +164,7 @@ module ActiveRecord if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::MySQL.new self else - @visitor = BindSubstitution.new self + @visitor = unprepared_visitor end end @@ -187,6 +208,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 +221,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 @@ -332,20 +357,6 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def structure_dump #:nodoc: - if supports_views? - sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" - else - sql = "SHOW TABLES" - end - - select_all(sql, 'SCHEMA').map { |table| - table.delete('Table_type') - sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" - exec_query(sql, 'SCHEMA').first['Create Table'] + ";\n\n" - }.join - end - # Drops the database specified on the +name+ attribute # and creates it again using the provided +options+. def recreate_database(name, options = {}) @@ -425,6 +436,7 @@ module ActiveRecord 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, [], []) + indexes.last.using = row[:Index_type].downcase.to_sym end indexes.last.columns << row[:Column_name] @@ -440,7 +452,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 @@ -473,10 +485,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 @@ -501,6 +509,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 @@ -586,6 +599,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 @@ -665,6 +682,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/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 20a5ca2baa..6b319fed88 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -38,6 +38,15 @@ module ActiveRecord configure_connection end + MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191 + def initialize_schema_migrations_table + if @config[:encoding] == 'utf8mb4' + ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4) + else + ActiveRecord::SchemaMigration.create_table + end + end + def supports_explain? true end @@ -54,8 +63,8 @@ module ActiveRecord end end - def new_column(field, default, type, null, collation) # :nodoc: - Column.new(field, default, type, null, collation, strict_mode?) + def new_column(field, default, type, null, collation, extra = "") # :nodoc: + Column.new(field, default, type, null, collation, strict_mode?, extra) end def error_number(exception) diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 7544c2a783..0e01fe0fdb 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -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: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index 3d8f0b575c..14ef07a75e 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 @@ -30,8 +41,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..51f377dfd7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -63,6 +63,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) @@ -330,6 +340,7 @@ module ActiveRecord register_type 'time', OID::Time.new register_type 'path', OID::Identity.new + register_type 'point', OID::Point.new register_type 'polygon', OID::Identity.new register_type 'circle', OID::Identity.new register_type 'hstore', OID::Hstore.new diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 47e2e3928f..6329733abc 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)}'" + else + super + end end when Hash - case column.sql_type + case sql_type when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) when 'json' then super(PostgreSQLColumn.json_to_string(value), column) else super end when IPAddr - case column.sql_type + case sql_type when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) else super end @@ -51,11 +57,14 @@ module ActiveRecord super end when Numeric - return super unless column.sql_type == 'money' - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" + if sql_type == 'money' || [:string, :text].include?(column.type) + # Not truly string input, so doesn't require (or allow) escape string syntax. + "'#{value}'" + else + super + end when String - case column.sql_type + case sql_type when 'bytea' then "'#{escape_bytea(value)}'" when 'xml' then "xml '#{quote_string(value)}'" when /^bit/ @@ -87,8 +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..0e1afbae8d 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+. @@ -124,8 +160,9 @@ module ActiveRecord 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] + type = inddef.scan(/USING (.+?) /).flatten[0].to_sym - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, type) end.compact end @@ -337,10 +374,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 +409,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 c91e1b3fb9..46e41e6b48 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -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] @@ -338,13 +345,14 @@ module ActiveRecord self end + def xml(options = {}) + column(args[0], :text, options) + end + private - def new_column_definition(base, name, type) - definition = ColumnDefinition.new base, name, type - @columns << definition - @columns_hash[name] = definition - definition + def create_column_definition(name, type) + ColumnDefinition.new name, type end end @@ -425,6 +433,10 @@ module ActiveRecord true end + def index_algorithms + { concurrently: 'CONCURRENTLY' } + end + class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max) super @@ -489,7 +501,7 @@ module ActiveRecord if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::PostgreSQL.new self else - @visitor = BindSubstitution.new self + @visitor = unprepared_visitor end @connection_parameters, @config = connection_parameters, config @@ -518,8 +530,7 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - @connection.query 'SELECT 1' - true + @connection.connect_poll != PG::PGRES_POLLING_FAILED rescue PGError false end @@ -594,13 +605,13 @@ module ActiveRecord end def enable_extension(name) - exec_query("CREATE EXTENSION IF NOT EXISTS #{name}").tap { + exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap { reload_type_map } end def disable_extension(name) - exec_query("DROP EXTENSION IF EXISTS #{name} CASCADE").tap { + exec_query("DROP EXTENSION IF EXISTS \"#{name}\" CASCADE").tap { reload_type_map } end @@ -627,13 +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 - super - end - # Set the authorized user for this session def session_auth=(user) clear_cache! @@ -663,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. @@ -707,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 @@ -894,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/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 5839d1d3b4..1d7a22e831 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,7 +1,9 @@ +require 'active_support/deprecation/reporting' + module ActiveRecord module ConnectionAdapters class SchemaCache - attr_reader :primary_keys, :tables, :version + attr_reader :version attr_accessor :connection def initialize(conn) @@ -14,6 +16,15 @@ module ActiveRecord prepare_default_proc end + def primary_keys(table_name = nil) + if table_name + @primary_keys[table_name] + else + ActiveSupport::Deprecation.warn('call primary_keys with a table name!') + @primary_keys.dup + end + end + # A cached lookup for table existence. def table_exists?(name) return @tables[name] if @tables.key? name @@ -30,12 +41,22 @@ module ActiveRecord end end + def tables(name = nil) + if name + @tables[name] + else + ActiveSupport::Deprecation.warn('call tables with a name!') + @tables.dup + end + end + # Get the columns for a table def columns(table = nil) if table @columns[table] else - @columns + ActiveSupport::Deprecation.warn('call columns with a table name!') + @columns.dup end end @@ -45,7 +66,8 @@ module ActiveRecord if table @columns_hash[table] else - @columns_hash + ActiveSupport::Deprecation.warn('call columns_hash with a table name!') + @columns_hash.dup end end @@ -58,6 +80,12 @@ module ActiveRecord @version = nil end + def size + [@columns, @columns_hash, @primary_keys, @tables].map { |x| + x.size + }.inject :+ + end + # Clear out internal caches for table with +table_name+. def clear_table_cache!(table_name) @columns.delete table_name @@ -69,9 +97,9 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. @version = ActiveRecord::Migrator.current_version - [@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val| - self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h } - end + [@version] + [@columns, @columns_hash, @primary_keys, @tables].map { |val| + Hash[val] + } end def marshal_load(array) @@ -87,7 +115,7 @@ module ActiveRecord end @columns_hash.default_proc = Proc.new do |h, table_name| - h[table_name] = Hash[columns[table_name].map { |col| + h[table_name] = Hash[columns(table_name).map { |col| [col.name, col] }] end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 981c4c96a0..c3bebf7a5e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -113,7 +113,7 @@ module ActiveRecord if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::SQLite.new self else - @visitor = BindSubstitution.new self + @visitor = unprepared_visitor end end @@ -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 @@ -499,6 +499,10 @@ module ActiveRecord rename_column_indexes(table_name, column_name, new_column_name) end + def valid_type?(type) + true + end + protected def select(sql, name = nil, binds = []) #:nodoc: exec_query(sql, name, binds) @@ -583,9 +587,17 @@ module ActiveRecord quoted_columns = columns.map { |col| quote_column_name(col) } * ',' quoted_to = quote_table_name(to) + + raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }] + exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' + + column_values = columns.map do |col| + quote(row[column_mappings[col]], raw_column_mappings[col]) + end + + sql << column_values * ', ' sql << ')' exec_query sql end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 899fe7d7c7..1ed05c1bd4 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -69,8 +69,25 @@ module ActiveRecord mattr_accessor :timestamped_migrations, instance_writer: false self.timestamped_migrations = true - class_attribute :connection_handler, instance_writer: false - self.connection_handler = ConnectionAdapters::ConnectionHandler.new + ## + # :singleton-method: + # Disable implicit join references. This feature was deprecated with Rails 4. + # If you don't make use of implicit references but still see deprecation warnings + # you can disable the feature entirely. This will be the default with Rails 4.1. + mattr_accessor :disable_implicit_join_references, instance_writer: false + self.disable_implicit_join_references = false + + class_attribute :default_connection_handler, instance_writer: false + + def self.connection_handler + Thread.current[:active_record_connection_handler] || self.default_connection_handler + end + + def self.connection_handler=(handler) + Thread.current[:active_record_connection_handler] = handler + end + + self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new end module ClassMethods @@ -168,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 @@ -238,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 = {} @@ -324,6 +340,7 @@ module ActiveRecord # also be used to "borrow" the connection to do database work that isn't # easily done without going straight to SQL. def connection + ActiveSupport::Deprecation.warn("#connection is deprecated in favour of accessing it via the class") self.class.connection end @@ -418,11 +435,21 @@ module ActiveRecord @readonly = false @destroyed = false @marked_for_destruction = false + @destroyed_by_association = nil @new_record = true @txn = nil @_start_transaction_state = {} @transaction_state = nil @reflects_state = [false] end + + def init_changed_attributes + # Intentionally avoid using #column_defaults since overriden 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/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 81f92db271..81cca37939 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -11,7 +11,7 @@ module ActiveRecord # ==== Parameters # # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more counter names to reset + # * +counters+ - One or more association counters to reset # # ==== Examples # @@ -21,6 +21,7 @@ module ActiveRecord object = find(id) counters.each do |association| has_many_association = reflect_on_association(association.to_sym) + raise ArgumentError, "'#{self.name}' has no association called '#{association}'" unless has_many_association if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection has_many_association = has_many_association.through_reflection diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index b2a9a54af1..15736575a2 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -6,9 +6,10 @@ module ActiveRecord def collecting_queries_for_explain # :nodoc: current = Thread.current original, current[:available_queries_for_explain] = current[:available_queries_for_explain], [] - return yield, current[:available_queries_for_explain] + yield + return current[:available_queries_for_explain] ensure - # Note that the return value above does not depend on this assigment. + # Note that the return value above does not depend on this assignment. current[:available_queries_for_explain] = original 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..c26fc76515 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -752,7 +752,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 diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index e630897a4b..f54865c86e 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -15,6 +15,9 @@ module ActiveRecord # and if the inheritance column is attr accessible, it initializes an # instance of the given subclass instead of the base class def new(*args, &block) + if abstract_class? || self == Base + raise NotImplementedError, "#{self} is an abstract class and can not be instantiated." + end if (attrs = args.first).is_a?(Hash) if subclass = subclass_from_attrs(attrs) return subclass.new(*args, &block) @@ -167,11 +170,16 @@ module ActiveRecord # this will ignore the inheritance column and return nil def subclass_from_attrs(attrs) subclass_name = attrs.with_indifferent_access[inheritance_column] - return nil if subclass_name.blank? || subclass_name == self.name - unless subclass = subclasses.detect { |sub| sub.name == subclass_name } - raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}") + + if subclass_name.present? && subclass_name != self.name + subclass = subclass_name.safe_constantize + + unless subclasses.include?(subclass) + raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}") + end + + subclass end - subclass end end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 32d35f0ec1..48c73d7781 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -49,7 +49,7 @@ module ActiveRecord case when new_record? "#{self.class.model_name.cache_key}/new" - when timestamp = self[:updated_at] + when timestamp = max_updated_column_timestamp timestamp = timestamp.utc.to_s(cache_timestamp_format) "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" else diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 701949e57b..209de78898 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -86,7 +86,7 @@ module ActiveRecord ) ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names)) - affected_rows = connection.update stmt + affected_rows = self.class.connection.update stmt unless affected_rows == 1 raise ActiveRecord::StaleObjectError.new(self, "update") @@ -117,7 +117,7 @@ module ActiveRecord if locking_enabled? column_name = self.class.locking_column column = self.class.columns_hash[column_name] - substitute = connection.substitute_at(column, relation.bind_values.length) + substitute = self.class.connection.substitute_at(column, relation.bind_values.length) relation = relation.where(self.class.arel_table[column_name].eq(substitute)) relation.bind_values << [column, self[column_name].to_i] diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 823595a128..d3edcf3cdb 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+. @@ -330,6 +330,24 @@ module ActiveRecord # # For a list of commands that are reversible, please see # <tt>ActiveRecord::Migration::CommandRecorder</tt>. + # + # == Transactional Migrations + # + # If the database adapter supports DDL transactions, all migrations will + # automatically be wrapped in a transaction. There are queries that you + # can't execute inside a transaction though, and for these situations + # you can turn the automatic transactions off. + # + # class ChangeEnum < ActiveRecord::Migration + # disable_ddl_transaction! + # + # def up + # execute "ALTER TYPE model_size ADD VALUE 'new_value'" + # end + # end + # + # Remember that you can still open your own transactions, even if you + # are in a Migration with <tt>self.disable_ddl_transaction!</tt>. class Migration autoload :CommandRecorder, 'active_record/migration/command_recorder' @@ -351,6 +369,7 @@ module ActiveRecord class << self attr_accessor :delegate # :nodoc: + attr_accessor :disable_ddl_transaction # :nodoc: end def self.check_pending! @@ -365,8 +384,16 @@ module ActiveRecord new.migrate direction end - cattr_accessor :verbose + # Disable DDL transactions for this migration. + def self.disable_ddl_transaction! + @disable_ddl_transaction = true + end + + def disable_ddl_transaction # :nodoc: + self.class.disable_ddl_transaction + end + cattr_accessor :verbose attr_accessor :name, :version def initialize(name = self.class.name, version = nil) @@ -375,8 +402,8 @@ module ActiveRecord @connection = nil end + self.verbose = true # instantiate the delegate object after initialize is defined - self.verbose = true self.delegate = new # Reverses the migration commands for the given block and @@ -607,8 +634,17 @@ module ActiveRecord source_migrations = ActiveRecord::Migrator.migrations(path) source_migrations.each do |migration| - source = File.read(migration.filename) - source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}" + source = File.binread(migration.filename) + inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n" + if /\A#.*\b(?:en)?coding:\s*\S+/ =~ source + # If we have a magic comment in the original migration, + # insert our comment after the first newline(end of the magic comment line) + # so the magic keep working. + # Note that magic comments must be at the first line(except sh-bang). + source[/\n/] = "\n#{inserted_comment}" + else + source = "#{inserted_comment}#{source}" + end if duplicate = destination_migrations.detect { |m| m.name == migration.name } if options[:on_skip] && duplicate.scope != scope.to_s @@ -622,7 +658,7 @@ module ActiveRecord old_path, migration.filename = migration.filename, new_path last = migration - File.open(migration.filename, "w") { |f| f.write source } + File.binwrite(migration.filename, source) copied << migration options[:on_copy].call(scope, migration, old_path) if options[:on_copy] destination_migrations << migration @@ -663,7 +699,7 @@ module ActiveRecord File.basename(filename) end - delegate :migrate, :announce, :write, :to => :migration + delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration private @@ -822,7 +858,7 @@ module ActiveRecord end def current_version - migrated.sort.last || 0 + migrated.max || 0 end def current_migration @@ -856,12 +892,12 @@ module ActiveRecord Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger begin - ddl_transaction do + ddl_transaction(migration) do migration.migrate(@direction) record_version_state_after_migrating(migration.version) end rescue => e - canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : "" + canceled_msg = use_transaction?(migration) ? "this and " : "" raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace end end @@ -935,12 +971,16 @@ module ActiveRecord end # Wrap the migration in a transaction only if supported by the adapter. - def ddl_transaction - if Base.connection.supports_ddl_transactions? + def ddl_transaction(migration) + if use_transaction?(migration) Base.transaction { yield } else yield end end + + def use_transaction?(migration) + !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions? + end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 85fb4be992..ac2d2f2712 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -205,7 +205,7 @@ module ActiveRecord # Returns an array of column objects for the table associated with this class. def columns - @columns ||= connection.schema_cache.columns[table_name].map do |col| + @columns ||= connection.schema_cache.columns(table_name).map do |col| col = col.dup col.primary = (col.name == primary_key) col diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index c5bd11edbf..602ab9e2f4 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -269,23 +269,36 @@ module ActiveRecord self.nested_attributes_options = nested_attributes_options type = (reflection.collection? ? :collection : :one_to_one) - - # def pirate_attributes=(attributes) - # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options) - # end - generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 - if method_defined?(:#{association_name}_attributes=) - remove_method(:#{association_name}_attributes=) - end - def #{association_name}_attributes=(attributes) - assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) - end - eoruby + generate_association_writer(association_name, type) else raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?" end end end + + private + + # Generates a writer method for this association. Serves as a point for + # accessing the objects in the association. For example, this method + # could generate the following: + # + # def pirate_attributes=(attributes) + # assign_nested_attributes_for_one_to_one_association(:pirate, attributes) + # end + # + # This redirects the attempts to write objects in an association through + # the helper methods defined below. Makes it seem like the nested + # associations are just regular associations. + def generate_association_writer(association_name, type) + generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 + if method_defined?(:#{association_name}_attributes=) + remove_method(:#{association_name}_attributes=) + end + def #{association_name}_attributes=(attributes) + assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) + end + eoruby + end end # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's @@ -371,20 +384,7 @@ module ActiveRecord raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" end - if limit = options[:limit] - limit = case limit - when Symbol - send(limit) - when Proc - limit.call - else - limit - end - - if limit && attributes_collection.size > limit - raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." - end - end + check_record_limit!(options[:limit], attributes_collection) if attributes_collection.is_a? Hash keys = attributes_collection.keys @@ -433,6 +433,29 @@ module ActiveRecord end end + # Takes in a limit and checks if the attributes_collection has too many + # records. The method will take limits in the form of symbols, procs, and + # number-like objects (anything that can be compared with an integer). + # + # Will raise an TooManyRecords error if the attributes_collection is + # larger than the limit. + def check_record_limit!(limit, attributes_collection) + if limit + limit = case limit + when Symbol + send(limit) + when Proc + limit.call + else + limit + end + + if limit && attributes_collection.size > limit + raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." + end + end + end + # Updates a record with the +attributes+ or marks it for destruction if # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+. def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) @@ -452,6 +475,11 @@ module ActiveRecord has_destroy_flag?(attributes) || call_reject_if(association_name, attributes) end + # Determines if a record with the particular +attributes+ should be + # rejected by calling the reject_if Symbol or Proc (if defined). + # The reject_if option is defined by +accepts_nested_attributes_for+. + # + # Returns false if there is a +destroy_flag+ on the attributes. def call_reject_if(association_name, attributes) return false if has_destroy_flag?(attributes) case callback = self.nested_attributes_options[association_name][:reject_if] diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 347f023793..f881778591 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -410,7 +410,7 @@ module ActiveRecord def relation_for_destroy pk = self.class.primary_key column = self.class.columns_hash[pk] - substitute = connection.substitute_at(column, 0) + substitute = self.class.connection.substitute_at(column, 0) relation = self.class.unscoped.where( self.class.arel_table[pk].eq(substitute)) @@ -443,7 +443,7 @@ module ActiveRecord 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) + stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id_was)).arel.compile_update(bind_attrs) klass.connection.update stmt, 'SQL', db_columns_with_values end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index e04a3d0976..902fd90c54 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, :references, :none, :to => :all + :having, :create_with, :uniq, :distinct, :references, :none, :to => :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all # Executes a custom SQL query against your database and returns all the results. The results will diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 13f3bf7085..99117b74c5 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -49,7 +49,7 @@ module ActiveRecord Rails.logger.extend ActiveSupport::Logger.broadcast console end - runner do |app| + runner do require "active_record/base" end @@ -64,7 +64,7 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end - initializer "active_record.migration_error" do |app| + initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load config.app_middleware.insert_after "::ActionDispatch::Callbacks", "ActiveRecord::Migration::CheckPending" @@ -158,7 +158,7 @@ module ActiveRecord end # Expose database runtime to controller for logging. - initializer "active_record.log_runtime" do |app| + initializer "active_record.log_runtime" do require "active_record/railties/controller_runtime" ActiveSupport.on_load(:action_controller) do include ActiveRecord::Railties::ControllerRuntime diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb index 90b462fad6..604a220303 100644 --- a/activerecord/lib/active_record/railties/console_sandbox.rb +++ b/activerecord/lib/active_record/railties/console_sandbox.rb @@ -1,4 +1,5 @@ -ActiveRecord::Base.connection.begin_db_transaction +ActiveRecord::Base.connection.begin_transaction(joinable: false) + at_exit do - ActiveRecord::Base.connection.rollback_db_transaction + ActiveRecord::Base.connection.rollback_transaction end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index bc50802c4a..4d24654015 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -10,7 +10,7 @@ module ActiveRecord :extending] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, - :reverse_order, :uniq, :create_with] + :reverse_order, :distinct, :create_with] VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS @@ -188,8 +188,7 @@ module ActiveRecord # Please see further details in the # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain - _, queries = collecting_queries_for_explain { exec_queries } - exec_explain(queries) + exec_explain(collecting_queries_for_explain { exec_queries }) end # Converts relation objects to Array. @@ -236,8 +235,9 @@ module ActiveRecord # Scope all queries to the current scope. # # Comment.where(post_id: 1).scoping do - # Comment.first # SELECT * FROM comments WHERE post_id = 1 + # Comment.first # end + # # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1 # # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. @@ -506,6 +506,12 @@ module ActiveRecord includes_values & joins_values end + # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+ + # to maintain backwards compatibility. Use +distinct_value+ instead. + def uniq_value + distinct_value + end + # Compares two relations for equality. def ==(other) case other @@ -589,21 +595,25 @@ module ActiveRecord if (references_values - joined_tables).any? true - elsif (string_tables - joined_tables).any? + elsif !ActiveRecord::Base.disable_implicit_join_references && + (string_tables - joined_tables).any? ActiveSupport::Deprecation.warn( "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \ "that are referenced in a string SQL snippet. For example: \n" \ "\n" \ " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \ "\n" \ - "Currently, Active Record recognises the table in the string, and knows to JOIN the " \ + "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. " \ "From now on, you must explicitly tell Active Record when you are referencing a table " \ "from a string:\n" \ "\n" \ - " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n\n" + " 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 " \ + "by setting `config.active_record.disable_implicit_join_references = true`." ) true else diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 7f95181c67..be011b22af 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -11,7 +11,7 @@ module ActiveRecord # Person.count(:all) # # => performs a COUNT(*) (:all is an alias for '*') # - # Person.count(:age, distinct: true) + # Person.distinct.count(:age) # # => counts the number of different age values # # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column, @@ -198,8 +198,13 @@ module ActiveRecord def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase - # If #count is used in conjuction with #uniq it is considered distinct. (eg. relation.uniq.count) - distinct = options[:distinct] || self.uniq_value + # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) + distinct = self.distinct_value + if options.has_key?(:distinct) + ActiveSupport::Deprecation.warn "The :distinct option for `Relation#count` is deprecated. " \ + "Please use `Relation#distinct` instead. (eg. `relation.distinct.count`)" + distinct = options[:distinct] + end if operation == "count" column_name ||= (select_for_count || :all) diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 00a506c3a7..1b6239eb38 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -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 diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 14520381c9..e2685d0478 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>. diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index bd783a94cf..f44d46d15b 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -48,7 +48,7 @@ module ActiveRecord column = reflection.foreign_key end - queries << build(table[column.to_sym], value) + queries << build(table[column], value) queries end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 4b8c40592e..257221174b 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -5,7 +5,7 @@ module ActiveRecord extend ActiveSupport::Concern # WhereChain objects act as placeholder for queries in which #where does not have any parameter. - # In this case, #where must be chained with either #not, #like, or #not_like to return a new relation. + # In this case, #where must be chained with #not to return a new relation. class WhereChain def initialize(scope) @scope = scope @@ -317,6 +317,67 @@ module ActiveRecord self end + VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock, + :limit, :offset, :joins, :includes, :from, + :readonly, :having]) + + # Removes an unwanted relation that is already defined on a chain of relations. + # This is useful when passing around chains of relations and would like to + # modify the relations without reconstructing the entire chain. + # + # User.order('email DESC').unscope(:order) == User.all + # + # The method arguments are symbols which correspond to the names of the methods + # which should be unscoped. The valid arguments are given in VALID_UNSCOPING_VALUES. + # The method can also be called with multiple arguments. For example: + # + # User.order('email DESC').select('id').where(name: "John") + # .unscope(:order, :select, :where) == User.all + # + # One can additionally pass a hash as an argument to unscope specific :where values. + # This is done by passing a hash with a single key-value pair. The key should be + # :where and the value should be the where value to unscope. For example: + # + # User.where(name: "John", active: true).unscope(where: :name) + # == User.where(active: true) + # + # Note that this method is more generalized than ActiveRecord::SpawnMethods#except + # because #except will only affect a particular relation's values. It won't wipe + # the order, grouping, etc. when that relation is merged. For example: + # + # Post.comments.except(:order) + # + # will still have an order if it comes from the default_scope on Comment. + def unscope(*args) + check_if_method_has_arguments!("unscope", args) + spawn.unscope!(*args) + end + + def unscope!(*args) + args.flatten! + + args.each do |scope| + case scope + when Symbol + symbol_unscoping(scope) + when Hash + scope.each do |key, target_value| + if key != :where + raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key." + end + + Array(target_value).each do |val| + where_unscoping(val) + end + end + else + raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example." + end + end + + self + end + # Performs a joins on +args+: # # User.joins(:posts) @@ -553,7 +614,7 @@ module ActiveRecord # # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the # Null Object pattern. It is an object with defined null behavior and always returns an empty - # array of records without quering the database. + # array of records without querying the database. # # Any subsequent condition chained to the returned relation will continue # generating an empty relation and will not fire any query to the database. @@ -649,20 +710,22 @@ module ActiveRecord # User.select(:name) # # => Might return two records with the same name # - # User.select(:name).uniq - # # => Returns 1 record per unique name + # User.select(:name).distinct + # # => Returns 1 record per distinct name # - # User.select(:name).uniq.uniq(false) + # User.select(:name).distinct.distinct(false) # # => You can also remove the uniqueness - def uniq(value = true) - spawn.uniq!(value) + def distinct(value = true) + spawn.distinct!(value) end + alias uniq distinct - # Like #uniq, but modifies relation in place. - def uniq!(value = true) # :nodoc: - self.uniq_value = value + # Like #distinct, but modifies relation in place. + def distinct!(value = true) # :nodoc: + self.distinct_value = value self end + alias uniq! distinct! # Used to extend a scope with additional methods, either through # a module or through a block provided. @@ -753,7 +816,7 @@ module ActiveRecord build_select(arel, select_values.uniq) - arel.distinct(uniq_value) + arel.distinct(distinct_value) arel.from(build_from) if from_value arel.lock(lock_value) if lock_value @@ -762,6 +825,39 @@ module ActiveRecord private + def symbol_unscoping(scope) + if !VALID_UNSCOPING_VALUES.include?(scope) + raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." + end + + single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope) + unscope_code = :"#{scope}_value#{'s' unless single_val_method}=" + + case scope + when :order + self.send(:reverse_order_value=, false) + result = [] + else + result = [] unless single_val_method + end + + self.send(unscope_code, result) + end + + def where_unscoping(target_value) + target_value_sym = target_value.to_sym + + where_values.reject! do |rel| + case rel + when Arel::Nodes::In, Arel::Nodes::Equality + subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) + subrelation.name.to_sym == target_value_sym + else + raise "unscope(where: #{target_value.inspect}) failed: unscoping #{rel.class} is unimplemented." + end + end + end + def custom_join_ast(table, joins) joins = joins.reject { |join| join.blank? } diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index df090b972d..fa9de926c5 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 diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 9830abe7d8..6077144265 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -13,18 +13,21 @@ module ActiveRecord "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" end - def self.create_table + def self.create_table(limit=nil) unless connection.table_exists?(table_name) - connection.create_table(table_name, :id => false) do |t| - t.column :version, :string, :null => false + version_options = {null: false} + version_options[:limit] = limit if limit + + connection.create_table(table_name, id: false) do |t| + t.column :version, :string, version_options end - connection.add_index table_name, :version, :unique => true, :name => index_name + connection.add_index table_name, :version, unique: true, name: index_name end end def self.drop_table if connection.table_exists?(table_name) - connection.remove_index table_name, :name => index_name + connection.remove_index table_name, name: index_name connection.drop_table(table_name) end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 5bd481082e..cde4f29d08 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -27,14 +27,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 diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 01fbb96b8e..12317601b6 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -159,10 +159,19 @@ module ActiveRecord end singleton_class.send(:define_method, name) do |*args| - options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body - relation = all.merge(options) + if body.respond_to?(:call) + scope = extension ? body.call(*args).extending(extension) : body.call(*args) - extension ? relation.extending(extension) : relation + if scope + default_scoped = scope.default_scoped + scope = relation.merge(scope) + scope.default_scoped = default_scoped + end + else + scope = body + end + + scope || all end end end diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 6b55af4205..bd9079b596 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -5,7 +5,7 @@ module ActiveRecord #:nodoc: include ActiveModel::Serializers::JSON included do - self.include_root_in_json = true + self.include_root_in_json = false end def serializable_hash(options = nil) diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 17378969a5..50569d2462 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -26,7 +26,9 @@ module ActiveRecord $stdout.print error.error establish_connection root_configuration_without_database connection.create_database configuration['database'], creation_options - connection.execute grant_statement.gsub(/\s+/, ' ').strip + if configuration['username'] != 'root' + connection.execute grant_statement.gsub(/\s+/, ' ').strip + end establish_connection configuration else $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" @@ -57,7 +59,10 @@ module ActiveRecord args.concat(["--result-file", "#{filename}"]) args.concat(["--no-data"]) args.concat(["#{configuration['database']}"]) - Kernel.system(*args) + unless Kernel.system(*args) + $stderr.puts "Could not dump the database structure. "\ + "Make sure `mysqldump` is in your PATH and check the command output for warnings." + end end def structure_load(filename) diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 8ded6d4a86..ae99cff35e 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -98,6 +98,12 @@ module ActiveRecord timestamp_attributes_for_create + timestamp_attributes_for_update end + def max_updated_column_timestamp + if (timestamps = timestamp_attributes_for_update.map { |attr| self[attr] }.compact).present? + timestamps.map { |ts| ts.to_time }.max + end + end + def current_time_from_proper_timezone self.class.default_timezone == :utc ? Time.now.utc : Time.now end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 33718ef0e9..4dbd668fcf 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -160,7 +160,7 @@ module ActiveRecord # end # end # - # only "Kotori" is created. (This works on MySQL and PostgreSQL, but not on SQLite3.) + # only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it. # # Most databases don't support true nested transactions. At the time of # writing, the only database that we're aware of that supports true nested diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 3706885881..26dca415ff 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -74,8 +74,7 @@ module ActiveRecord protected def perform_validations(options={}) # :nodoc: - perform_validation = options[:validate] != false - perform_validation ? valid?(options[:context]) : true + options[:validate] == false || valid?(options[:context]) end end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 1427189851..a705d8c2c4 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -2,6 +2,10 @@ module ActiveRecord module Validations class UniquenessValidator < ActiveModel::EachValidator # :nodoc: def initialize(options) + if options[:conditions] && !options[:conditions].respond_to?(:call) + raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \ + "Pass a callable instead: `conditions: -> { where(approved: true) }`" + end super({ case_sensitive: true }.merge!(options)) end @@ -19,7 +23,7 @@ module ActiveRecord relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? relation = scope_relation(record, table, relation) relation = finder_class.unscoped.where(relation) - relation.merge!(options[:conditions]) if options[:conditions] + relation = relation.merge(options[:conditions]) if options[:conditions] if relation.exists? error_options = options.except(:case_sensitive, :scope, :conditions) @@ -116,7 +120,7 @@ module ActiveRecord # of the title attribute: # # class Article < ActiveRecord::Base - # validates_uniqueness_of :title, conditions: where('status != ?', 'archived') + # validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') } # end # # When the record is created, a check is performed to make sure that no @@ -132,7 +136,7 @@ module ActiveRecord # the uniqueness constraint. # * <tt>:conditions</tt> - Specify the conditions to be included as a # <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup - # (e.g. <tt>conditions: where('status = ?', 'active')</tt>). + # (e.g. <tt>conditions: -> { where(status: 'active') }</tt>). # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by # non-text columns (+true+ by default). # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index c0471bb506..f2b041ad97 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.0.0.beta1" + end - STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') + module VERSION #:nodoc: + MAJOR, MINOR, TINY, PRE = ActiveRecord.version.segments + STRING = ActiveRecord.version.to_s end end diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index 3a3cf86d73..fd94a2d038 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -2,8 +2,12 @@ class <%= migration_class_name %> < ActiveRecord::Migration def change create_table :<%= table_name %> do |t| <% attributes.each do |attribute| -%> +<% if attribute.password_digest? -%> + t.string :password_digest<%= attribute.inject_options %> +<% else -%> t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> <% end -%> +<% end -%> <% if options[:timestamps] %> t.timestamps <% end -%> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 056f55470c..808598699b 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -1,7 +1,10 @@ <% module_namespacing do -%> class <%= class_name %> < <%= parent_class_name.classify %> -<% attributes.select {|attr| attr.reference? }.each do |attribute| -%> +<% attributes.select(&:reference?).each do |attribute| -%> belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %> <% end -%> +<% if attributes.any?(&:password_digest?) -%> + has_secure_password +<% end -%> end <% end -%> diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 8812cf1b7d..9050ae3fe3 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -21,20 +21,39 @@ 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(btree hash).each do |type| + expected = "CREATE INDEX `index_people_on_last_name` USING #{type} ON `people` (`last_name`) " + assert_equal expected, add_index(:people, :last_name, :using => type) + 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 @@ -70,8 +89,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/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/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/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 5164acf77f..4cf4bc4c61 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -63,11 +63,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase assert_nothing_raised { @connection.rename_column(:group, :order, :values) } end - # dump structure of table with reserved word name - def test_structure_dump - assert_nothing_raised { @connection.structure_dump } - end - # introspect table with reserved word name def test_introspect assert_nothing_raised { @connection.columns(:group) } diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index d94bb629a7..e6e54bf20a 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -35,6 +35,25 @@ 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_post_title' + index_b_name = 'index_post_body' + + table = Post.table_name + + @connection.execute "CREATE INDEX `#{index_a_name}` ON `#{table}` (`title`);" + @connection.execute "CREATE INDEX `#{index_b_name}` USING btree ON `#{table}` (`body`(10));" + + indexes = @connection.indexes(table).sort_by {|i| i.name} + assert_equal 2,indexes.size + + assert_equal :btree, indexes.select{|i| i.name == index_a_name}[0].using + assert_equal :btree, indexes.select{|i| i.name == index_b_name}[0].using + + @connection.execute "DROP INDEX `#{index_a_name}` ON `#{table}`;" + @connection.execute "DROP INDEX `#{index_b_name}` ON `#{table}`;" + 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..48d63aeef5 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -21,20 +21,39 @@ 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(btree hash).each do |type| + expected = "CREATE INDEX `index_people_on_last_name` USING #{type} ON `people` (`last_name`) " + assert_equal expected, add_index(:people, :last_name, :using => type) + 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 +89,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/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 1265cb927e..fedd9f603c 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -70,12 +70,6 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end - def test_logs_name_structure_dump - @connection.structure_dump - assert_equal "SCHEMA", @connection.logged[0][1] - assert_equal "SCHEMA", @connection.logged[2][1] - end - def test_logs_name_show_variable @connection.show_variable 'foo' assert_equal "SCHEMA", @connection.logged[0][1] diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 1017b0758d..e76617b845 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -63,11 +63,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase assert_nothing_raised { @connection.rename_column(:group, :order, :values) } end - # dump structure of table with reserved word name - def test_structure_dump - assert_nothing_raised { @connection.structure_dump } - end - # introspect table with reserved word name def test_introspect assert_nothing_raised { @connection.columns(:group) } @@ -89,7 +84,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase assert_nothing_raised { x.save } assert_nothing_raised { Group.find_by_order('y') } assert_nothing_raised { Group.find(1) } - x = Group.find(1) end # has_one association with reserved-word table name diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb new file mode 100644 index 0000000000..9ecd901eac --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class Mysql2Adapter + class SchemaMigrationsTest < ActiveRecord::TestCase + def test_initializes_schema_migrations_for_encoding_utf8mb4 + conn = ActiveRecord::Base.connection + + smtn = ActiveRecord::Migrator.schema_migrations_table_name + conn.drop_table(smtn) if conn.table_exists?(smtn) + + config = conn.instance_variable_get(:@config) + original_encoding = config[:encoding] + + config[:encoding] = 'utf8mb4' + conn.initialize_schema_migrations_table + + assert conn.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4) + ensure + config[:encoding] = original_encoding + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 94429e772f..78f754d2ce 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -44,6 +44,24 @@ module ActiveRecord assert_match(/database 'foo-bar'/, e.inspect) end + def test_dump_indexes + index_a_name = 'index_post_title' + index_b_name = 'index_post_body' + + table = Post.table_name + + @connection.execute "CREATE INDEX `#{index_a_name}` ON `#{table}` (`title`);" + @connection.execute "CREATE INDEX `#{index_b_name}` USING btree ON `#{table}` (`body`(10));" + + indexes = @connection.indexes(table).sort_by {|i| i.name} + assert_equal 2,indexes.size + + assert_equal :btree, indexes.select{|i| i.name == index_a_name}[0].using + assert_equal :btree, indexes.select{|i| i.name == index_b_name}[0].using + + @connection.execute "DROP INDEX `#{index_a_name}` ON `#{table}`;" + @connection.execute "DROP INDEX `#{index_b_name}` ON `#{table}`;" + 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/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/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/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 685f0ea74f..b3429648ee 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -44,6 +44,14 @@ module ActiveRecord c = Column.new(nil, 1, 'float') assert_equal "'Infinity'", @conn.quote(infinity, c) end + + def test_quote_cast_numeric + fixnum = 666 + c = Column.new(nil, nil, 'string') + assert_equal "'666'", @conn.quote(fixnum, c) + c = Column.new(nil, nil, 'text') + assert_equal "'666'", @conn.quote(fixnum, c) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 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 new file mode 100644 index 0000000000..c0c0e8898c --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -0,0 +1,53 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlUUIDTest < ActiveRecord::TestCase + class UUID < ActiveRecord::Base + self.table_name = 'pg_uuids' + end + + def setup + @connection = ActiveRecord::Base.connection + + unless @connection.supports_extensions? + return skip "do not test on PG without uuid-ossp" + end + + unless @connection.extension_enabled?('uuid-ossp') + @connection.enable_extension 'uuid-ossp' + @connection.commit_db_transaction + end + + @connection.reconnect! + + @connection.transaction do + @connection.create_table('pg_uuids', id: :uuid) do |t| + t.string 'name' + t.uuid 'other_uuid', default: 'uuid_generate_v4()' + end + end + end + + def teardown + @connection.execute 'drop table if exists pg_uuids' + end + + def test_id_is_uuid + assert_equal :uuid, UUID.columns_hash['id'].type + assert UUID.primary_key + end + + def test_id_has_a_default + u = UUID.create + assert_not_nil u.id + end + + def test_auto_create_uuid + u = UUID.create + u.reload + assert_not_nil u.other_uuid + end +end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index d03d1dd94c..a5a22bc30b 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -1,7 +1,7 @@ require "cases/helper" class CopyTableTest < ActiveRecord::TestCase - fixtures :customers, :companies, :comments + fixtures :customers, :companies, :comments, :binaries def setup @connection = ActiveRecord::Base.connection @@ -72,6 +72,10 @@ class CopyTableTest < ActiveRecord::TestCase end end + def test_copy_table_with_binary_column + test_copy_table 'binaries', 'binaries2' + end + protected def copy_table(from, to, options = {}) @connection.copy_table(from, to, {:temporary => true}.merge(options)) @@ -86,7 +90,7 @@ protected end def table_indexes_without_name(table) - @connection.indexes('comments_with_index').delete(:name) + @connection.indexes(table).delete(:name) end def row_count(table) diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 003052bac4..dcbec24acf 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -25,6 +25,15 @@ 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 + + def test_invalid_column + assert @conn.valid_type?(:foobar) + end + def teardown @conn.intercepted = false @conn.logged = [] diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 3a6da0e59f..f5316952b8 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -21,9 +21,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase :posts, :tags, :taggings, :comments, :sponsors, :members def test_belongs_to - Client.find(3).firm.name - assert_equal companies(:first_firm).name, Client.find(3).firm.name - assert_not_nil Client.find(3).firm, "Microsoft should have a firm" + firm = Client.find(3).firm + assert_not_nil firm + assert_equal companies(:first_firm).name, firm.name end def test_belongs_to_with_primary_key @@ -232,15 +232,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_belongs_to_counter_with_assigning_nil - p = Post.find(1) - c = Comment.find(1) + post = Post.find(1) + comment = Comment.find(1) - assert_equal p.id, c.post_id - assert_equal 2, Post.find(p.id).comments.size + assert_equal post.id, comment.post_id + assert_equal 2, Post.find(post.id).comments.size - c.post = nil + comment.post = nil - assert_equal 1, Post.find(p.id).comments.size + assert_equal 1, Post.find(post.id).comments.size end def test_belongs_to_with_primary_key_counter @@ -263,56 +263,56 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_belongs_to_counter_with_reassigning - t1 = Topic.create("title" => "t1") - t2 = Topic.create("title" => "t2") - r1 = Reply.new("title" => "r1", "content" => "r1") - r1.topic = t1 + topic1 = Topic.create("title" => "t1") + topic2 = Topic.create("title" => "t2") + reply1 = Reply.new("title" => "r1", "content" => "r1") + reply1.topic = topic1 - assert r1.save - assert_equal 1, Topic.find(t1.id).replies.size - assert_equal 0, Topic.find(t2.id).replies.size + assert reply1.save + assert_equal 1, Topic.find(topic1.id).replies.size + assert_equal 0, Topic.find(topic2.id).replies.size - r1.topic = Topic.find(t2.id) + reply1.topic = Topic.find(topic2.id) assert_no_queries do - r1.topic = t2 + reply1.topic = topic2 end - assert r1.save - assert_equal 0, Topic.find(t1.id).replies.size - assert_equal 1, Topic.find(t2.id).replies.size + assert reply1.save + assert_equal 0, Topic.find(topic1.id).replies.size + assert_equal 1, Topic.find(topic2.id).replies.size - r1.topic = nil + reply1.topic = nil - assert_equal 0, Topic.find(t1.id).replies.size - assert_equal 0, Topic.find(t2.id).replies.size + assert_equal 0, Topic.find(topic1.id).replies.size + assert_equal 0, Topic.find(topic2.id).replies.size - r1.topic = t1 + reply1.topic = topic1 - assert_equal 1, Topic.find(t1.id).replies.size - assert_equal 0, Topic.find(t2.id).replies.size + assert_equal 1, Topic.find(topic1.id).replies.size + assert_equal 0, Topic.find(topic2.id).replies.size - r1.destroy + reply1.destroy - assert_equal 0, Topic.find(t1.id).replies.size - assert_equal 0, Topic.find(t2.id).replies.size + assert_equal 0, Topic.find(topic1.id).replies.size + assert_equal 0, Topic.find(topic2.id).replies.size end def test_belongs_to_reassign_with_namespaced_models_and_counters - t1 = Web::Topic.create("title" => "t1") - t2 = Web::Topic.create("title" => "t2") - r1 = Web::Reply.new("title" => "r1", "content" => "r1") - r1.topic = t1 + topic1 = Web::Topic.create("title" => "t1") + topic2 = Web::Topic.create("title" => "t2") + reply1 = Web::Reply.new("title" => "r1", "content" => "r1") + reply1.topic = topic1 - assert r1.save - assert_equal 1, Web::Topic.find(t1.id).replies.size - assert_equal 0, Web::Topic.find(t2.id).replies.size + assert reply1.save + assert_equal 1, Web::Topic.find(topic1.id).replies.size + assert_equal 0, Web::Topic.find(topic2.id).replies.size - r1.topic = Web::Topic.find(t2.id) + reply1.topic = Web::Topic.find(topic2.id) - assert r1.save - assert_equal 0, Web::Topic.find(t1.id).replies.size - assert_equal 1, Web::Topic.find(t2.id).replies.size + assert reply1.save + assert_equal 0, Web::Topic.find(topic1.id).replies.size + assert_equal 1, Web::Topic.find(topic2.id).replies.size end def test_belongs_to_counter_after_save @@ -367,9 +367,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_new_record_with_foreign_key_but_no_object - c = Client.new("firm_id" => 1) + client = Client.new("firm_id" => 1) # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - assert_equal Firm.all.merge!(:order => "id").first, c.firm_with_basic_id + assert_equal Firm.all.merge!(:order => "id").first, client.firm_with_basic_id end def test_setting_foreign_key_after_nil_target_loaded diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 80bca7f63e..e693d34f99 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -55,7 +55,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_nothing_raised do assert_equal 4, categories.count assert_equal 4, categories.to_a.count - assert_equal 3, categories.count(:distinct => true) + assert_equal 3, categories.distinct.count assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 1de7ee0846..d6850215b5 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -193,7 +193,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end end - def test_finding_with_includes_on_has_one_assocation_with_same_include_includes_only_once + def test_finding_with_includes_on_has_one_association_with_same_include_includes_only_once author = authors(:david) post = author.post_about_thinking_with_last_comment last_comment = post.last_comment @@ -467,7 +467,7 @@ class EagerAssociationTest < ActiveRecord::TestCase posts_with_comments = people(:michael).posts.merge(:includes => :comments, :order => 'posts.id').to_a posts_with_author = people(:michael).posts.merge(:includes => :author, :order => 'posts.id').to_a posts_with_comments_and_author = people(:michael).posts.merge(:includes => [ :comments, :author ], :order => 'posts.id').to_a - assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size } + assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum + post.comments.size } assert_equal authors(:david), assert_no_queries { posts_with_author.first.author } assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author } end @@ -523,7 +523,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit posts = Post.all.merge!(:order => 'posts.id asc', :includes => [ :author, :comments ], :limit => 2).to_a assert_equal 2, posts.size - assert_equal 3, posts.inject(0) { |sum, post| sum += post.comments.size } + assert_equal 3, posts.inject(0) { |sum, post| sum + post.comments.size } end def test_eager_with_has_many_and_limit_and_conditions @@ -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_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 1b1b479f1a..84bdca3a97 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -316,7 +316,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase dev.projects << projects(:active_record) assert_equal 3, dev.projects.size - assert_equal 1, dev.projects.uniq.size + assert_equal 1, dev.projects.distinct.size end def test_uniq_before_the_fact diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 1ddd380f23..781b87741d 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -789,6 +789,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + def test_calling_update_attributes_on_id_changes_the_counter_cache + topic = Topic.order("id ASC").first + original_count = topic.replies.to_a.size + assert_equal original_count, topic.replies_count + + first_reply = topic.replies.first + first_reply.update_attributes(:parent_id => nil) + assert_equal original_count - 1, topic.reload.replies_count + + first_reply.update_attributes(:parent_id => topic.id) + assert_equal original_count, topic.reload.replies_count + end + + def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache + topic1 = Topic.find(1) + topic2 = Topic.find(3) + original_count1 = topic1.replies.to_a.size + original_count2 = topic2.replies.to_a.size + + reply1 = topic1.replies.first + reply2 = topic2.replies.first + + reply1.update_attributes(:parent_id => topic2.id) + assert_equal original_count1 - 1, topic1.reload.replies_count + assert_equal original_count2 + 1, topic2.reload.replies_count + + reply2.update_attributes(:parent_id => topic1.id) + assert_equal original_count1, topic1.reload.replies_count + assert_equal original_count2, topic2.reload.replies_count + end + def test_deleting_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 4f246f575e..918783e8f1 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -82,7 +82,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions real_count = Author.all.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length - authors_with_welcoming_post_titles = Author.all.merge!(:joins => :posts, :where => "posts.title like 'Welcome%'").calculate(:count, 'authors.id', :distinct => true) + authors_with_welcoming_post_titles = Author.all.merge!(joins: :posts, where: "posts.title like 'Welcome%'").distinct.calculate(:count, 'authors.id') assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 8c9b4fb921..c8cad84013 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -278,6 +278,31 @@ class InverseHasManyTests < ActiveRecord::TestCase assert interests[1].man.equal? man end + def test_parent_instance_should_find_child_instance_using_child_instance_id + man = Man.create! + interest = Interest.create! + man.interests = [interest] + + assert interest.equal?(man.interests.first), "The inverse association should use the interest already created and held in memory" + assert interest.equal?(man.interests.find(interest.id)), "The inverse association should use the interest already created and held in memory" + assert man.equal?(man.interests.first.man), "Two inversion should lead back to the same object that was originally held" + assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held" + end + + def test_parent_instance_should_find_child_instance_using_child_instance_id_when_created + man = Man.create! + interest = Interest.create!(man: man) + + assert man.equal?(man.interests.first.man), "Two inverses should lead back to the same object that was originally held" + assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held" + + assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed" + man.name = "Ben Bitdiddle" + assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the parent name is changed" + man.interests.find(interest.id).man.name = "Alyssa P. Hacker" + assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the child name is changed" + end + def test_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/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 10ec33be75..aabeea025f 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -397,14 +397,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_many - assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.uniq.sort_by { |t| t.id } + assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by { |t| t.id } end def test_include_has_many_through_polymorphic_has_many author = Author.includes(:taggings).find authors(:david).id expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do - assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } + assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } end end @@ -464,7 +464,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert saved_post.reload.tags(true).include?(new_tag) - new_post = Post.new(:title => "Association replacmenet works!", :body => "You best believe it.") + new_post = Post.new(:title => "Association replacement works!", :body => "You best believe it.") saved_tag = tags(:general) new_post.tags << saved_tag diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index e355ed3495..e75d43bda8 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -410,7 +410,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # Mary and Bob both have posts in misc, but they are the only ones. authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id) - assert_equal [authors(:mary), authors(:bob)], authors.uniq.sort_by(&:id) + assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id) # Check the polymorphism of taggings is being observed correctly (in both joins) authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel') diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 201fa5d5a9..a06bacafca 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -95,7 +95,7 @@ class AssociationsTest < ActiveRecord::TestCase def test_force_reload firm = Firm.new("name" => "A New Firm, Inc") firm.save - firm.clients.each {|c|} # forcing to load all clients + firm.clients.each {} # forcing to load all clients assert firm.clients.empty?, "New firm shouldn't have client objects" assert_equal 0, firm.clients.size, "New firm should have 0 clients" @@ -237,6 +237,11 @@ class AssociationProxyTest < ActiveRecord::TestCase assert david.projects.scope.is_a?(ActiveRecord::Relation) assert_equal david.projects, david.projects.scope end + + test "proxy object is cached" do + david = developers(:david) + assert david.projects.equal?(david.projects) + end end class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index c503c21e27..648596b828 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -159,8 +159,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_read_attributes_before_type_cast - category = Category.new({:name=>"Test categoty", :type => nil}) - category_attrs = {"name"=>"Test categoty", "id" => nil, "type" => nil, "categorizations_count" => nil} + category = Category.new({:name=>"Test category", :type => nil}) + category_attrs = {"name"=>"Test category", "id" => nil, "type" => nil, "categorizations_count" => nil} assert_equal category_attrs , category.attributes_before_type_cast end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index af1845c937..69eef500f5 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -141,13 +141,13 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_limit_should_sanitize_sql_injection_for_limit_without_comas + def test_limit_should_sanitize_sql_injection_for_limit_without_commas assert_raises(ArgumentError) do Topic.limit("1 select * from schema").to_a end end - def test_limit_should_sanitize_sql_injection_for_limit_with_comas + def test_limit_should_sanitize_sql_injection_for_limit_with_commas assert_raises(ArgumentError) do Topic.limit("1, 7 procedure help()").to_a end @@ -840,7 +840,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 +869,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 +880,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 +1047,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 @@ -1091,7 +1114,7 @@ class BasicsTest < ActiveRecord::TestCase res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" res7 = nil assert_nothing_raised do - res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count(distinct: true) + res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").distinct.count end assert_equal res6, res7 end @@ -1142,8 +1165,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_keeps_multiple_group_values - combined = Developer.all.merge!(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at').to_a - assert_equal combined, Developer.all.merge!(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at']).to_a + combined = Developer.all.merge!(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at, developers.created_on, developers.updated_on').to_a + assert_equal combined, Developer.all.merge!(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at', 'developers.created_on', 'developers.updated_on']).to_a end def test_find_symbol_ordered_last @@ -1344,9 +1367,9 @@ class BasicsTest < ActiveRecord::TestCase def test_clear_cache! # preheat cache - c1 = Post.connection.schema_cache.columns['posts'] + c1 = Post.connection.schema_cache.columns('posts') ActiveRecord::Base.clear_cache! - c2 = Post.connection.schema_cache.columns['posts'] + c2 = Post.connection.schema_cache.columns('posts') assert_not_equal c1, c2 end @@ -1441,12 +1464,30 @@ class BasicsTest < ActiveRecord::TestCase assert_not_equal key, car.cache_key end - def test_cache_key_format_for_existing_record_with_nil_updated_at + def test_cache_key_format_for_existing_record_with_nil_updated_timestamps dev = Developer.first - dev.update_columns(updated_at: nil) + dev.update_columns(updated_at: nil, updated_on: nil) assert_match(/\/#{dev.id}$/, dev.cache_key) end + def test_cache_key_for_updated_on + dev = Developer.first + dev.updated_at = nil + assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key + end + + def test_cache_key_for_newer_updated_at + dev = Developer.first + dev.updated_at += 3600 + assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key + end + + def test_cache_key_for_newer_updated_on + dev = Developer.first + dev.updated_on += 3600 + assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key + end + def test_touch_should_raise_error_on_a_new_object company = Company.new(:rating => 1, :name => "37signals", :firm_name => "37signals") assert_raises(ActiveRecord::ActiveRecordError) do @@ -1467,6 +1508,12 @@ class BasicsTest < ActiveRecord::TestCase assert_equal scope, Bird.uniq end + def test_distinct_delegates_to_scoped + scope = stub + Bird.stubs(:all).returns(mock(:distinct => scope)) + assert_equal scope, Bird.distinct + end + def test_table_name_with_2_abstract_subclasses assert_equal "photos", Photo.table_name end @@ -1531,4 +1578,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 overriden" 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 acb8b5f562..87559925e7 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -11,7 +11,7 @@ class EachTest < ActiveRecord::TestCase Post.count('id') # preheat arel's table cache end - def test_each_should_excecute_one_query_per_batch + def test_each_should_execute_one_query_per_batch assert_queries(Post.count + 1) do Post.find_each(:batch_size => 1) do |post| assert_kind_of Post, post diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index be49e948fc..c645523905 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -305,8 +305,8 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_count_selected_field_with_include - assert_equal 6, Account.includes(:firm).count(:distinct => true) - assert_equal 4, Account.includes(:firm).select(:credit_limit).count(:distinct => true) + assert_equal 6, Account.includes(:firm).distinct.count + assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count end def test_should_not_perform_joined_include_by_default @@ -341,7 +341,18 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 5, Account.count(:firm_id) end - def test_count_with_uniq + def test_count_distinct_option_is_deprecated + assert_deprecated do + assert_equal 4, Account.select(:credit_limit).count(distinct: true) + end + + assert_deprecated do + assert_equal 6, Account.select(:credit_limit).count(distinct: false) + end + end + + def test_count_with_distinct + assert_equal 4, Account.select(:credit_limit).distinct.count assert_equal 4, Account.select(:credit_limit).uniq.count end @@ -351,7 +362,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_count_field_in_joined_table assert_equal 5, Account.joins(:firm).count('companies.id') - assert_equal 4, Account.joins(:firm).count('companies.id', :distinct => true) + assert_equal 4, Account.joins(:firm).distinct.count('companies.id') end def test_should_count_field_in_joined_table_with_group_by @@ -455,7 +466,7 @@ class CalculationsTest < ActiveRecord::TestCase approved_topics_count = Topic.group(:approved).count(:author_name)[true] assert_equal approved_topics_count, 3 # Count the number of distinct authors for approved Topics - distinct_authors_for_approved_count = Topic.group(:approved).count(:author_name, :distinct => true)[true] + distinct_authors_for_approved_count = Topic.group(:approved).distinct.count(:author_name)[true] assert_equal distinct_authors_for_approved_count, 2 end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 7457bafd4e..187cad9599 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -520,7 +520,7 @@ class CallbacksTest < ActiveRecord::TestCase ], david.history end - def test_inheritence_of_callbacks + def test_inheritance_of_callbacks parent = ParentDeveloper.new assert !parent.after_save_called parent.save diff --git a/activerecord/test/cases/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/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 541e983758..ecad7c942f 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -9,49 +9,46 @@ module ActiveRecord end def test_primary_key - assert_equal 'id', @cache.primary_keys['posts'] + assert_equal 'id', @cache.primary_keys('posts') end def test_primary_key_for_non_existent_table - assert_nil @cache.primary_keys['omgponies'] + assert_nil @cache.primary_keys('omgponies') end def test_caches_columns - columns = @cache.columns['posts'] - assert_equal columns, @cache.columns['posts'] + columns = @cache.columns('posts') + assert_equal columns, @cache.columns('posts') end def test_caches_columns_hash - columns_hash = @cache.columns_hash['posts'] - assert_equal columns_hash, @cache.columns_hash['posts'] + columns_hash = @cache.columns_hash('posts') + assert_equal columns_hash, @cache.columns_hash('posts') end def test_clearing - @cache.columns['posts'] - @cache.columns_hash['posts'] - @cache.tables['posts'] - @cache.primary_keys['posts'] + @cache.columns('posts') + @cache.columns_hash('posts') + @cache.tables('posts') + @cache.primary_keys('posts') @cache.clear! - assert_equal 0, @cache.columns.size - assert_equal 0, @cache.columns_hash.size - assert_equal 0, @cache.tables.size - assert_equal 0, @cache.primary_keys.size + assert_equal 0, @cache.size end def test_dump_and_load - @cache.columns['posts'] - @cache.columns_hash['posts'] - @cache.tables['posts'] - @cache.primary_keys['posts'] + @cache.columns('posts') + @cache.columns_hash('posts') + @cache.tables('posts') + @cache.primary_keys('posts') @cache = Marshal.load(Marshal.dump(@cache)) - assert_equal 12, @cache.columns['posts'].size - assert_equal 12, @cache.columns_hash['posts'].size - assert @cache.tables['posts'] - assert_equal 'id', @cache.primary_keys['posts'] + assert_equal 12, @cache.columns('posts').size + assert_equal 12, @cache.columns_hash('posts').size + assert @cache.tables('posts') + assert_equal 'id', @cache.primary_keys('posts') end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 23e64bee7e..e6af29282c 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -185,7 +185,7 @@ module ActiveRecord assert_not_nil connection threads = [] 4.times do |i| - threads << Thread.new(i) do |pool_count| + threads << Thread.new(i) do connection = pool.connection assert_not_nil connection connection.close diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index fc46a249c8..ac093251a5 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -115,10 +115,19 @@ class CounterCacheTest < ActiveRecord::TestCase end end + test "update other counters on parent destroy" do + david, joanna = dog_lovers(:david, :joanna) + joanna = joanna # squelch a warning + + assert_difference 'joanna.reload.dogs_count', -1 do + david.destroy + end + end + test "reset the right counter if two have the same foreign key" do michael = people(:michael) assert_nothing_raised(ActiveRecord::StatementInvalid) do - Person.reset_counters(michael.id, :followers) + Person.reset_counters(michael.id, :friends_too) end end @@ -131,4 +140,11 @@ class CounterCacheTest < ActiveRecord::TestCase Subscriber.reset_counters(subscriber.id, 'books') end end + + test "the passed symbol needs to be an association name" do + e = assert_raises(ArgumentError) do + Topic.reset_counters(@topic.id, :replies_count) + end + assert_equal "'Topic' has no association called 'replies_count'", e.message + end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index c7d2ba6073..7b2034dadf 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -243,6 +243,21 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.changed? end + def test_float_zero_to_string_zero_not_marked_as_changed + data = NumericData.new :temperature => 0.0 + data.save! + + assert_not data.changed? + + data.temperature = '0' + assert_empty data.changes + + data.temperature = '0.0' + assert_empty data.changes + + data.temperature = '0.00' + assert_empty data.changes + end def test_zero_to_blank_marked_as_changed pirate = Pirate.new diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index fe105b9d22..dbf6005a67 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -110,7 +110,7 @@ module ActiveRecord def test_dup_validity_is_independent repair_validations(Topic) do Topic.validates_presence_of :title - topic = Topic.new("title" => "Litterature") + topic = Topic.new("title" => "Literature") topic.valid? duped = topic.dup diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index b1d276f9eb..6dac5db111 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -20,7 +20,7 @@ if ActiveRecord::Base.connection.supports_explain? end def test_collecting_queries_for_explain - result, queries = ActiveRecord::Base.collecting_queries_for_explain do + queries = ActiveRecord::Base.collecting_queries_for_explain do Car.where(:name => 'honda').to_a end @@ -28,7 +28,6 @@ if ActiveRecord::Base.connection.supports_explain? assert_match "SELECT", sql assert_match "honda", sql assert_equal [], binds - assert_equal [cars(:honda)], result end def test_exec_explain_with_no_binds diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index a9fa107749..e505fe9f18 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -82,7 +82,7 @@ class FinderTest < ActiveRecord::TestCase # ensures +exists?+ runs valid SQL by excluding order value def test_exists_with_order - assert Topic.order(:id).uniq.exists? + assert Topic.order(:id).distinct.exists? end def test_exists_with_includes_limit_and_empty_result diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index b0b29f5f42..f6cfee0cb8 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -477,9 +477,8 @@ class CustomConnectionFixturesTest < ActiveRecord::TestCase fixtures :courses self.use_transactional_fixtures = false - def test_connection - assert_kind_of Course, courses(:ruby) - assert_equal Course.connection, courses(:ruby).connection + def test_connection_instance_method_deprecation + assert_deprecated { courses(:ruby).connection } end def test_leaky_destroy @@ -577,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 189066eb41..99d54e7526 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -68,6 +68,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_company_descends_from_active_record + assert !ActiveRecord::Base.descends_from_active_record? assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base' assert Company.descends_from_active_record?, 'Company should descend from ActiveRecord::Base' assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base' @@ -171,6 +172,20 @@ class InheritanceTest < ActiveRecord::TestCase assert_equal Firm, firm.class end + def test_new_with_abstract_class + e = assert_raises(NotImplementedError) do + AbstractCompany.new + end + assert_equal("AbstractCompany is an abstract class and can not be instantiated.", e.message) + end + + def test_new_with_ar_base + e = assert_raises(NotImplementedError) do + ActiveRecord::Base.new + end + assert_equal("ActiveRecord::Base is an abstract class and can not be instantiated.", e.message) + end + def test_new_with_invalid_type assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'InvalidType') } end @@ -179,6 +194,17 @@ class InheritanceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'Account') } end + def test_new_with_autoload_paths + path = File.expand_path('../../models/autoloadable', __FILE__) + ActiveSupport::Dependencies.autoload_paths << path + + firm = Company.new(:type => 'ExtraFirm') + assert_equal ExtraFirm, firm.class + ensure + ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path } + ActiveSupport::Dependencies.clear + end + def test_inheritance_condition assert_equal 10, Company.count assert_equal 2, Firm.count diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index a86b165c78..a222675918 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -6,7 +6,21 @@ require 'models/tagging' require 'models/tag' require 'models/comment' +module JsonSerializationHelpers + private + + def set_include_root_in_json(value) + original_root_in_json = ActiveRecord::Base.include_root_in_json + ActiveRecord::Base.include_root_in_json = value + yield + ensure + ActiveRecord::Base.include_root_in_json = original_root_in_json + end +end + class JsonSerializationTest < ActiveRecord::TestCase + include JsonSerializationHelpers + class NamespacedContact < Contact column :name, :string end @@ -23,20 +37,24 @@ class JsonSerializationTest < ActiveRecord::TestCase end def test_should_demodulize_root_in_json - @contact = NamespacedContact.new :name => 'whatever' - json = @contact.to_json - assert_match %r{^\{"namespaced_contact":\{}, json + set_include_root_in_json(true) do + @contact = NamespacedContact.new name: 'whatever' + json = @contact.to_json + assert_match %r{^\{"namespaced_contact":\{}, json + end end def test_should_include_root_in_json - json = @contact.to_json - - assert_match %r{^\{"contact":\{}, json - assert_match %r{"name":"Konata Izumi"}, json - assert_match %r{"age":16}, json - assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) - assert_match %r{"awesome":true}, json - assert_match %r{"preferences":\{"shows":"anime"\}}, json + set_include_root_in_json(true) do + json = @contact.to_json + + assert_match %r{^\{"contact":\{}, json + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"age":16}, json + assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) + assert_match %r{"awesome":true}, json + assert_match %r{"preferences":\{"shows":"anime"\}}, json + end end def test_should_encode_all_encodable_attributes @@ -141,6 +159,8 @@ end class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase fixtures :authors, :posts, :comments, :tags, :taggings + include JsonSerializationHelpers + def setup @david = authors(:david) @mary = authors(:mary) @@ -227,23 +247,21 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase end def test_should_allow_only_option_for_list_of_authors - ActiveRecord::Base.include_root_in_json = false - authors = [@david, @mary] - assert_equal %([{"name":"David"},{"name":"Mary"}]), ActiveSupport::JSON.encode(authors, :only => :name) - ensure - ActiveRecord::Base.include_root_in_json = true + set_include_root_in_json(false) do + authors = [@david, @mary] + assert_equal %([{"name":"David"},{"name":"Mary"}]), ActiveSupport::JSON.encode(authors, only: :name) + end end def test_should_allow_except_option_for_list_of_authors - ActiveRecord::Base.include_root_in_json = false - authors = [@david, @mary] - encoded = ActiveSupport::JSON.encode(authors, :except => [ - :name, :author_address_id, :author_address_extra_id, - :organization_id, :owned_essay_id - ]) - assert_equal %([{"id":1},{"id":2}]), encoded - ensure - ActiveRecord::Base.include_root_in_json = true + set_include_root_in_json(false) do + authors = [@david, @mary] + encoded = ActiveSupport::JSON.encode(authors, except: [ + :name, :author_address_id, :author_address_extra_id, + :organization_id, :owned_essay_id + ]) + assert_equal %([{"id":1},{"id":2}]), encoded + end end def test_should_allow_includes_for_list_of_authors @@ -262,17 +280,21 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase end def test_should_allow_options_for_hash_of_authors - authors_hash = { - 1 => @david, - 2 => @mary - } - assert_equal %({"1":{"author":{"name":"David"}}}), ActiveSupport::JSON.encode(authors_hash, :only => [1, :name]) + set_include_root_in_json(true) do + authors_hash = { + 1 => @david, + 2 => @mary + } + assert_equal %({"1":{"author":{"name":"David"}}}), ActiveSupport::JSON.encode(authors_hash, only: [1, :name]) + end end def test_should_be_able_to_encode_relation - authors_relation = Author.where(:id => [@david.id, @mary.id]) + set_include_root_in_json(true) do + authors_relation = Author.where(id: [@david.id, @mary.id]) - json = ActiveSupport::JSON.encode authors_relation, :only => :name - assert_equal '[{"author":{"name":"David"}},{"author":{"name":"Mary"}}]', json + json = ActiveSupport::JSON.encode authors_relation, only: :name + assert_equal '[{"author":{"name":"David"}},{"author":{"name":"Mary"}}]', json + end end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index a0a3e6cb0d..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 @@ -341,9 +349,6 @@ end # is so cumbersome. Will deadlock Ruby threads if the underlying db.execute # blocks, so separate script called by Kernel#system is needed. # (See exec vs. async_exec in the PostgreSQL adapter.) - -# TODO: The Sybase, and OpenBase adapters currently have no support for pessimistic locking - unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? class PessimisticLockingTest < ActiveRecord::TestCase self.use_transactional_fixtures = false diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index e52809f0f8..52906c8a01 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -62,6 +62,13 @@ module ActiveRecord 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/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb index ee0c20747e..97efb94b66 100644 --- a/activerecord/test/cases/migration/logger_test.rb +++ b/activerecord/test/cases/migration/logger_test.rb @@ -7,6 +7,7 @@ module ActiveRecord self.use_transactional_fixtures = false Migration = Struct.new(:name, :version) do + def disable_ddl_transaction; false end def migrate direction # do nothing end @@ -34,4 +35,3 @@ module ActiveRecord end end end - diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index fa8dec0e15..f8afb7c591 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -239,9 +239,13 @@ class MigrationTest < ActiveRecord::TestCase assert_not Person.column_methods_hash.include?(:last_name) - migration = Struct.new(:name, :version) { - def migrate(x); raise 'Something broke'; end - }.new('zomg', 100) + 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) @@ -250,7 +254,39 @@ class MigrationTest < ActiveRecord::TestCase assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message Person.reset_column_information + assert_not Person.column_methods_hash.include?(:last_name), + "On error, the Migrator should revert schema changes but it did not." + end + + def test_migration_without_transaction + unless ActiveRecord::Base.connection.supports_ddl_transactions? + skip "not supported on #{ActiveRecord::Base.connection.class}" + end + assert_not Person.column_methods_hash.include?(:last_name) + + migration = Class.new(ActiveRecord::Migration) { + self.disable_ddl_transaction! + + def version; 101 end + def migrate(x) + add_column "people", "last_name", :string + raise 'Something broke' + end + }.new + + migrator = ActiveRecord::Migrator.new(:up, [migration], 101) + e = assert_raise(StandardError) { migrator.migrate } + assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message + + Person.reset_column_information + assert Person.column_methods_hash.include?(:last_name), + "without ddl transactions, the Migrator should not rollback on error but it did." + ensure + Person.reset_column_information + if Person.column_methods_hash.include?(:last_name) + Person.connection.remove_column('people', 'last_name') + end end def test_schema_migrations_table_name @@ -426,6 +462,22 @@ class ReservedWordsMigrationTest < ActiveRecord::TestCase end end +class ExplicitlyNamedIndexMigrationTest < ActiveRecord::TestCase + def test_drop_index_by_name + connection = Person.connection + connection.create_table :values, force: true do |t| + t.integer :value + end + + assert_nothing_raised ArgumentError do + connection.add_index :values, :value, name: 'a_different_name' + connection.remove_index :values, column: :value, name: 'a_different_name' + end + + connection.drop_table :values rescue nil + end +end + if ActiveRecord::Base.connection.supports_bulk_alter? class BulkAlterTableMigrationsTest < ActiveRecord::TestCase def setup @@ -686,6 +738,26 @@ class CopyMigrationsTest < ActiveRecord::TestCase clear end + def test_copying_migrations_preserving_magic_comments + ActiveRecord::Base.timestamped_migrations = false + @migrations_path = MIGRATIONS_ROOT + "/valid" + @existing_migrations = Dir[@migrations_path + "/*.rb"] + + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"}) + assert File.exists?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb") + assert_equal [@migrations_path + "/4_currencies_have_symbols.bukkits.rb"], copied.map(&:filename) + + expected = "# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)" + assert_equal expected, IO.readlines(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")[0..1].join.chomp + + files_count = Dir[@migrations_path + "/*.rb"].length + copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"}) + assert_equal files_count, Dir[@migrations_path + "/*.rb"].length + assert copied.empty? + ensure + clear + end + def test_skipping_migrations @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" @existing_migrations = Dir[@migrations_path + "/*.rb"] diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index bd121126e7..1eb3bc4177 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -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 @@ -309,7 +322,7 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size end - def test_chaining_should_use_latest_conditions_when_creating + def test_chaining_applies_last_conditions_when_creating post = Topic.rejected.new assert !post.approved? @@ -323,13 +336,13 @@ class NamedScopeTest < ActiveRecord::TestCase assert post.approved? end - def test_chaining_should_use_latest_conditions_when_searching + def test_chaining_combines_conditions_when_searching # Normal hash conditions - assert_equal Topic.where(:approved => true).to_a, Topic.rejected.approved.to_a - assert_equal Topic.where(:approved => false).to_a, Topic.approved.rejected.to_a + assert_equal Topic.where(approved: false).where(approved: true).to_a, Topic.rejected.approved.to_a + assert_equal Topic.where(approved: true).where(approved: false).to_a, Topic.approved.rejected.to_a # Nested hash conditions with same keys - assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.to_a + assert_equal [], Post.with_special_comments.with_very_special_comments.to_a # Nested hash conditions with different keys assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 94837341fc..b6e140b912 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -167,7 +167,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record Man.accepts_nested_attributes_for(:interests) man = Man.create(:name => "John") - interest = man.interests.create :topic => 'gardning' + interest = man.interests.create :topic => 'gardening' man = Man.find man.id man.interests_attributes = [{:id => interest.id, :topic => 'gardening'}] assert_equal man.interests.first.topic, man.interests[0].topic @@ -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 8156f99037..a29189df05 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -399,14 +399,6 @@ class PersistencesTest < ActiveRecord::TestCase assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') } end - def test_string_ids - # FIXME: Fix this failing test - skip "Failing test. We need this fixed before 4.0.0" - mv = Minivan.where(:minivan_id => 1234).first_or_initialize - assert mv.new_record? - assert_equal '1234', mv.minivan_id - end - def test_update_attribute_with_one_updated t = Topic.first t.update_attribute(:title, 'super_title') @@ -669,6 +661,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/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index 8ce44636b4..92d1e013e8 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -6,26 +6,31 @@ module ActiveRecord class WhereChainTest < ActiveRecord::TestCase fixtures :posts + def setup + super + @name = 'title' + end + def test_not_eq - expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'hello') + expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello') relation = Post.where.not(title: 'hello') assert_equal([expected], relation.where_values) end def test_not_null - expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], nil) + expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], nil) relation = Post.where.not(title: nil) assert_equal([expected], relation.where_values) end def test_not_in - expected = Arel::Nodes::NotIn.new(Post.arel_table[:title], %w[hello goodbye]) + expected = Arel::Nodes::NotIn.new(Post.arel_table[@name], %w[hello goodbye]) relation = Post.where.not(title: %w[hello goodbye]) assert_equal([expected], relation.where_values) end def test_association_not_eq - expected = Arel::Nodes::NotEqual.new(Comment.arel_table[:title], 'hello') + expected = Arel::Nodes::NotEqual.new(Comment.arel_table[@name], 'hello') relation = Post.joins(:comments).where.not(comments: {title: 'hello'}) assert_equal(expected.to_sql, relation.where_values.first.to_sql) end @@ -33,20 +38,20 @@ module ActiveRecord def test_not_eq_with_preceding_where relation = Post.where(title: 'hello').where.not(title: 'world') - expected = Arel::Nodes::Equality.new(Post.arel_table[:title], 'hello') + expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'hello') assert_equal(expected, relation.where_values.first) - expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'world') + expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'world') assert_equal(expected, relation.where_values.last) end def test_not_eq_with_succeeding_where relation = Post.where.not(title: 'hello').where(title: 'world') - expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'hello') + expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'hello') assert_equal(expected, relation.where_values.first) - expected = Arel::Nodes::Equality.new(Post.arel_table[:title], 'world') + expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'world') assert_equal(expected, relation.where_values.last) end @@ -65,10 +70,10 @@ module ActiveRecord def test_chaining_multiple relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails') - expected = Arel::Nodes::NotIn.new(Post.arel_table[:author_id], [1, 2]) + expected = Arel::Nodes::NotIn.new(Post.arel_table['author_id'], [1, 2]) assert_equal(expected, relation.where_values[0]) - expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'ruby on rails') + expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'ruby on rails') assert_equal(expected, relation.where_values[1]) end end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 7388324a0d..6cd89b6227 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -390,20 +390,20 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_default_scope_with_inheritance wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash - assert_equal "Jamis", wheres[:name] - assert_equal 50000, wheres[:salary] + assert_equal "Jamis", wheres['name'] + assert_equal 50000, wheres['salary'] end def test_default_scope_with_module_includes wheres = ModuleIncludedPoorDeveloperCalledJamis.all.where_values_hash - assert_equal "Jamis", wheres[:name] - assert_equal 50000, wheres[:salary] + assert_equal "Jamis", wheres['name'] + assert_equal 50000, wheres['salary'] end def test_default_scope_with_multiple_calls wheres = MultiplePoorDeveloperCalledJamis.all.where_values_hash - assert_equal "Jamis", wheres[:name] - assert_equal 50000, wheres[:salary] + assert_equal "Jamis", wheres['name'] + assert_equal 50000, wheres['salary'] end def test_scope_overwrites_default @@ -424,6 +424,150 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end + def test_unscope_overrides_default_scope + expected = Developer.all.collect { |dev| [dev.name, dev.id] } + received = Developer.order('name ASC, id DESC').unscope(:order).collect { |dev| [dev.name, dev.id] } + assert_equal expected, received + end + + def test_unscope_after_reordering_and_combining + expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] } + received = DeveloperOrderedBySalary.reorder('name DESC').unscope(:order).order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] } + assert_equal expected, received + + expected_2 = Developer.all.collect { |dev| [dev.name, dev.id] } + received_2 = Developer.order('id DESC, name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] } + assert_equal expected_2, received_2 + + expected_3 = Developer.all.collect { |dev| [dev.name, dev.id] } + received_3 = Developer.reorder('name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] } + assert_equal expected_3, received_3 + end + + def test_unscope_with_where_attributes + expected = Developer.order('salary DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect { |dev| dev.name } + assert_equal expected, received + + expected_2 = Developer.order('salary DESC').collect { |dev| dev.name } + received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect { |dev| dev.name } + assert_equal expected_2, received_2 + + expected_3 = Developer.order('salary DESC').collect { |dev| dev.name } + received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect { |dev| dev.name } + assert_equal expected_3, received_3 + end + + def test_unscope_multiple_where_clauses + expected = Developer.order('salary DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_unscope_with_grouping_attributes + expected = Developer.order('salary DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect { |dev| dev.name } + assert_equal expected, received + + expected_2 = Developer.order('salary DESC').collect { |dev| dev.name } + received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect { |dev| dev.name } + assert_equal expected_2, received_2 + end + + def test_unscope_with_limit_in_query + expected = Developer.order('salary DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_order_to_unscope_reordering + expected = DeveloperOrderedBySalary.all.collect { |dev| [dev.name, dev.id] } + received = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order).collect { |dev| [dev.name, dev.id] } + assert_equal expected, received + end + + def test_unscope_reverse_order + expected = Developer.all.collect { |dev| dev.name } + received = Developer.order('salary DESC').reverse_order.unscope(:order).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_unscope_select + expected = Developer.order('salary ASC').collect { |dev| dev.name } + received = Developer.order('salary DESC').reverse_order.select(:name => "Jamis").unscope(:select).collect { |dev| dev.name } + assert_equal expected, received + + expected_2 = Developer.all.collect { |dev| dev.id } + received_2 = Developer.select(:name).unscope(:select).collect { |dev| dev.id } + assert_equal expected_2, received_2 + end + + def test_unscope_offset + expected = Developer.all.collect { |dev| dev.name } + received = Developer.offset(5).unscope(:offset).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_unscope_joins_and_select_on_developers_projects + expected = Developer.all.collect { |dev| dev.name } + received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_unscope_includes + expected = Developer.all.collect { |dev| dev.name } + received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_unscope_having + expected = DeveloperOrderedBySalary.all.collect { |dev| dev.name } + received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect { |dev| dev.name } + 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) + end + + assert_raises(ArgumentError) do + Developer.all.unscope(:includes, :select, :some_broken_value) + end + + assert_raises(ArgumentError) do + Developer.order('name DESC').reverse_order.unscope(:reverse_order) + end + + assert_raises(ArgumentError) do + Developer.order('name DESC').where(name: "Jamis").unscope() + end + end + + def test_unscope_errors_with_non_where_hash_keys + assert_raises(ArgumentError) do + Developer.where(name: "Jamis").limit(4).unscope(limit: 4) + end + + assert_raises(ArgumentError) do + Developer.where(name: "Jamis").unscope("where" => :name) + end + end + + def test_unscope_errors_with_non_symbol_or_hash_arguments + assert_raises(ArgumentError) do + Developer.where(name: "Jamis").limit(3).unscope("limit") + end + + assert_raises(ArgumentError) do + Developer.select("id").unscope("select") + end + + assert_raises(ArgumentError) do + Developer.select("id").unscope(5) + end + end + def test_order_in_default_scope_should_not_prevail expected = Developer.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary } received = DeveloperOrderedBySalary.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary } @@ -490,7 +634,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/relation_test.rb b/activerecord/test/cases/relation_test.rb index fd0b05cb77..9ca980fdf6 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -278,5 +278,17 @@ module ActiveRecord assert_equal [NullRelation], relation.extending_values assert relation.is_a?(NullRelation) end + + test "distinct!" do + relation.distinct! :foo + assert_equal :foo, relation.distinct_value + assert_equal :foo, relation.uniq_value # deprecated access + end + + test "uniq! was replaced by distinct!" do + relation.uniq! :foo + assert_equal :foo, relation.distinct_value + assert_equal :foo, relation.uniq_value # deprecated access + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 8298d7534c..9008c2785e 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -492,6 +492,7 @@ class RelationTest < ActiveRecord::TestCase expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do + assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } end @@ -714,6 +715,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal [developers(:poor_jamis)], dev_with_count.to_a end + def test_relation_to_sql + sql = Post.connection.unprepared_statement do + Post.first.comments.to_sql + end + assert_no_match(/\?/, sql) + end + def test_relation_merging_with_arel_equalities_keeps_last_equality devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge( Developer.where(Developer.arel_table[:salary].eq(9000)) @@ -782,11 +790,11 @@ class RelationTest < ActiveRecord::TestCase def test_count_with_distinct posts = Post.all - assert_equal 3, posts.count(:comments_count, :distinct => true) - assert_equal 11, posts.count(:comments_count, :distinct => false) + assert_equal 3, posts.distinct(true).count(:comments_count) + assert_equal 11, posts.distinct(false).count(:comments_count) - assert_equal 3, posts.select(:comments_count).count(:distinct => true) - assert_equal 11, posts.select(:comments_count).count(:distinct => false) + assert_equal 3, posts.distinct(true).select(:comments_count).count + assert_equal 11, posts.distinct(false).select(:comments_count).count end def test_count_explicit_columns @@ -1216,6 +1224,16 @@ class RelationTest < ActiveRecord::TestCase end end + def test_turn_off_eager_loading_with_conditions_on_joins + original_value = ActiveRecord::Base.disable_implicit_join_references + ActiveRecord::Base.disable_implicit_join_references = true + + scope = Topic.where(author_email_address: 'my.example@gmail.com').includes(:replies) + assert_not scope.eager_loading? + ensure + ActiveRecord::Base.disable_implicit_join_references = original_value + end + def test_ordering_with_extra_spaces assert_equal authors(:david), Author.order('id DESC , name DESC').last end @@ -1262,7 +1280,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal posts(:welcome), comments(:greetings).post end - def test_uniq + def test_distinct tag1 = Tag.create(:name => 'Foo') tag2 = Tag.create(:name => 'Foo') @@ -1270,11 +1288,14 @@ class RelationTest < ActiveRecord::TestCase assert_equal ['Foo', 'Foo'], query.map(&:name) assert_sql(/DISTINCT/) do + assert_equal ['Foo'], query.distinct.map(&:name) assert_equal ['Foo'], query.uniq.map(&:name) end assert_sql(/DISTINCT/) do + assert_equal ['Foo'], query.distinct(true).map(&:name) assert_equal ['Foo'], query.uniq(true).map(&:name) end + assert_equal ['Foo', 'Foo'], query.distinct(true).distinct(false).map(&:name) assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index eb9cb91e32..c46060a646 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -18,6 +18,10 @@ class SerializationTest < ActiveRecord::TestCase } end + def test_include_root_in_json_is_false_by_default + assert_equal false, ActiveRecord::Base.include_root_in_json, "include_root_in_json should be false by default but was not" + end + def test_serialize_should_be_reversible FORMATS.each do |format| @serialized = Contact.new.send("to_#{format}") diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 295c7e13fa..726338db14 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'models/topic' require 'models/person' +require 'models/traffic_light' require 'bcrypt' class SerializedAttributeTest < ActiveRecord::TestCase @@ -234,4 +235,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase person = person.reload assert_equal(insures, person.insures) end + + def test_regression_serialized_default_on_text_column_with_null_false + light = TrafficLight.new + assert_equal [], light.state + assert_equal [], light.long_state + end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 38b9dd02f0..816bd62751 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -71,7 +71,7 @@ module ActiveRecord return skip("only tested on mysql") end - @connection = stub(:create_database => true, :execute => true) + @connection = stub("Connection", create_database: true) @error = Mysql::Error.new "Invalid permissions" @configuration = { 'adapter' => 'mysql', @@ -90,6 +90,7 @@ module ActiveRecord end def test_root_password_is_requested + assert_permissions_granted_for "pat" skip "only if mysql is available" unless defined?(::Mysql) $stdin.expects(:gets).returns("secret\n") @@ -97,6 +98,7 @@ module ActiveRecord end def test_connection_established_as_root + assert_permissions_granted_for "pat" ActiveRecord::Base.expects(:establish_connection).with( 'adapter' => 'mysql', 'database' => nil, @@ -108,6 +110,7 @@ module ActiveRecord end def test_database_created_by_root + assert_permissions_granted_for "pat" @connection.expects(:create_database). with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') @@ -115,12 +118,18 @@ module ActiveRecord end def test_grant_privileges_for_normal_user - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON my-app-db.* TO 'pat'@'localhost' IDENTIFIED BY 'wossname' WITH GRANT OPTION;") + assert_permissions_granted_for "pat" + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + def test_do_not_grant_privileges_for_root_user + @configuration['username'] = 'root' + @configuration['password'] = '' ActiveRecord::Tasks::DatabaseTasks.create @configuration end def test_connection_established_as_normal_user + assert_permissions_granted_for "pat" ActiveRecord::Base.expects(:establish_connection).returns do ActiveRecord::Base.expects(:establish_connection).with( 'adapter' => 'mysql', @@ -142,6 +151,13 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.create @configuration end + + private + def assert_permissions_granted_for(db_user) + db_name = @configuration['database'] + db_password = @configuration['password'] + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") + end end class MySQLDBDropTest < ActiveRecord::TestCase @@ -249,10 +265,21 @@ module ActiveRecord def test_structure_dump filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db") + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end + + def test_warn_when_external_structure_dump_fails + filename = "awesome-file.sql" + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(false) + + warnings = capture(:stderr) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + + assert_match(/Could not dump the database structure/, warnings) + end end class MySQLStructureLoadTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index eb4ffd4498..766a5c0c90 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -182,9 +182,9 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_call_after_rollback_when_commit_fails - @first.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction) + @first.class.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction) begin - @first.connection.class.class_eval do + @first.class.connection.class.class_eval do def commit_db_transaction; raise "boom!"; end end @@ -194,8 +194,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert !@first.save rescue nil assert_equal [:after_rollback], @first.history ensure - @first.connection.class.send(:remove_method, :commit_db_transaction) - @first.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction) + @first.class.connection.class.send(:remove_method, :commit_db_transaction) + @first.class.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction) end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 46e767af1a..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 @@ -348,7 +348,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase end def test_validate_uniqueness_with_conditions - Topic.validates_uniqueness_of(:title, :conditions => Topic.where('approved = ?', true)) + Topic.validates_uniqueness_of :title, conditions: -> { where(approved: true) } Topic.create("title" => "I'm a topic", "approved" => true) Topic.create("title" => "I'm an unapproved topic", "approved" => false) @@ -359,6 +359,12 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t4.valid?, "t4 should be valid" end + def test_validate_uniqueness_with_non_callable_conditions_is_not_supported + assert_raises(ArgumentError) { + Topic.validates_uniqueness_of :title, conditions: Topic.where(approved: true) + } + end + def test_validate_uniqueness_with_array_column return skip "Uniqueness on arrays has only been tested in PostgreSQL so far." if !current_adapter? :PostgreSQLAdapter diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb index 11912ca1cc..c02b3241cd 100644 --- a/activerecord/test/cases/validations_repair_helper.rb +++ b/activerecord/test/cases/validations_repair_helper.rb @@ -6,7 +6,7 @@ module ActiveRecord def repair_validations(*model_classes) teardown do model_classes.each do |k| - k.reset_callbacks(:validate) + k.clear_validators! end end end @@ -16,7 +16,7 @@ module ActiveRecord yield ensure model_classes.each do |k| - k.reset_callbacks(:validate) + k.clear_validators! end end end diff --git a/activerecord/test/fixtures/dog_lovers.yml b/activerecord/test/fixtures/dog_lovers.yml index d3e5e4a1aa..3f4c6c9e4c 100644 --- a/activerecord/test/fixtures/dog_lovers.yml +++ b/activerecord/test/fixtures/dog_lovers.yml @@ -2,3 +2,6 @@ david: id: 1 bred_dogs_count: 0 trained_dogs_count: 1 +joanna: + id: 2 + dogs_count: 1 diff --git a/activerecord/test/fixtures/dogs.yml b/activerecord/test/fixtures/dogs.yml index 16d19be2c5..b5eb2c7b74 100644 --- a/activerecord/test/fixtures/dogs.yml +++ b/activerecord/test/fixtures/dogs.yml @@ -1,3 +1,4 @@ sophie: id: 1 trainer_id: 1 + dog_lover_id: 2 diff --git a/activerecord/test/fixtures/friendships.yml b/activerecord/test/fixtures/friendships.yml index 1ee09175bf..ae0abe0162 100644 --- a/activerecord/test/fixtures/friendships.yml +++ b/activerecord/test/fixtures/friendships.yml @@ -1,4 +1,4 @@ Connection 1: id: 1 - person_id: 1 - friend_id: 2
\ No newline at end of file + friend_id: 1 + follower_id: 2 diff --git a/activerecord/test/fixtures/people.yml b/activerecord/test/fixtures/people.yml index e640a38f1f..0ec05e8d56 100644 --- a/activerecord/test/fixtures/people.yml +++ b/activerecord/test/fixtures/people.yml @@ -5,6 +5,7 @@ michael: number1_fan_id: 3 gender: M followers_count: 1 + friends_too_count: 1 david: id: 2 first_name: David @@ -12,6 +13,7 @@ david: number1_fan_id: 1 gender: M followers_count: 1 + friends_too_count: 1 susan: id: 3 first_name: Susan @@ -19,3 +21,4 @@ susan: number1_fan_id: 1 gender: F followers_count: 1 + friends_too_count: 1 diff --git a/activerecord/test/fixtures/traffic_lights.yml b/activerecord/test/fixtures/traffic_lights.yml index 6dabd53474..81b4e47959 100644 --- a/activerecord/test/fixtures/traffic_lights.yml +++ b/activerecord/test/fixtures/traffic_lights.yml @@ -4,3 +4,7 @@ uk: - Green - Red - Orange + long_state: + - "Green, go ahead" + - "Red, wait" + - "Orange, caution light is about to switch"
\ No newline at end of file diff --git a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb new file mode 100644 index 0000000000..c066c068c2 --- /dev/null +++ b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb @@ -0,0 +1,12 @@ +# coding: ISO-8859-15 + +class CurrenciesHaveSymbols < ActiveRecord::Migration + def self.up + # We use ¤ for default currency symbol + add_column "currencies", "symbol", :string, :default => "¤" + end + + def self.down + remove_column "currencies", "symbol" + end +end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 8423411474..a96899ae10 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -30,8 +30,8 @@ class Author < ActiveRecord::Base has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments has_many :limited_comments, -> { limit(1) }, :through => :posts, :source => :comments has_many :funky_comments, :through => :posts, :source => :comments - has_many :ordered_uniq_comments, -> { uniq.order('comments.id') }, :through => :posts, :source => :comments - has_many :ordered_uniq_comments_desc, -> { uniq.order('comments.id DESC') }, :through => :posts, :source => :comments + has_many :ordered_uniq_comments, -> { distinct.order('comments.id') }, :through => :posts, :source => :comments + has_many :ordered_uniq_comments_desc, -> { distinct.order('comments.id DESC') }, :through => :posts, :source => :comments has_many :readonly_comments, -> { readonly }, :through => :posts, :source => :comments has_many :special_posts @@ -78,7 +78,7 @@ class Author < ActiveRecord::Base has_many :categories_like_general, -> { where(:name => 'General') }, :through => :categorizations, :source => :category, :class_name => 'Category' has_many :categorized_posts, :through => :categorizations, :source => :post - has_many :unique_categorized_posts, -> { uniq }, :through => :categorizations, :source => :post + has_many :unique_categorized_posts, -> { distinct }, :through => :categorizations, :source => :post has_many :nothings, :through => :kateggorisatons, :class_name => 'Category' @@ -91,7 +91,7 @@ class Author < ActiveRecord::Base has_many :post_categories, :through => :posts, :source => :categories has_many :tagging_tags, :through => :taggings, :source => :tag - has_many :similar_posts, -> { uniq }, :through => :tags, :source => :tagged_posts + has_many :similar_posts, -> { distinct }, :through => :tags, :source => :tagged_posts has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, :through => :posts, :source => :tags has_many :tags_with_primary_key, :through => :posts diff --git a/activerecord/test/models/autoloadable/extra_firm.rb b/activerecord/test/models/autoloadable/extra_firm.rb new file mode 100644 index 0000000000..5578ba0d9b --- /dev/null +++ b/activerecord/test/models/autoloadable/extra_firm.rb @@ -0,0 +1,2 @@ +class ExtraFirm < Company +end diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index ce81a37966..5458a28cc9 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -2,7 +2,7 @@ class Book < ActiveRecord::Base has_many :authors has_many :citations, :foreign_key => 'book1_id' - has_many :references, -> { uniq }, :through => :citations, :source => :reference_of + has_many :references, -> { distinct }, :through => :citations, :source => :reference_of has_many :subscriptions has_many :subscribers, :through => :subscriptions diff --git a/activerecord/test/models/dog.rb b/activerecord/test/models/dog.rb index 72b7d33a86..b02b8447b8 100644 --- a/activerecord/test/models/dog.rb +++ b/activerecord/test/models/dog.rb @@ -1,4 +1,5 @@ class Dog < ActiveRecord::Base - belongs_to :breeder, :class_name => "DogLover", :counter_cache => :bred_dogs_count - belongs_to :trainer, :class_name => "DogLover", :counter_cache => :trained_dogs_count + belongs_to :breeder, class_name: "DogLover", counter_cache: :bred_dogs_count + belongs_to :trainer, class_name: "DogLover", counter_cache: :trained_dogs_count + belongs_to :doglover, foreign_key: :dog_lover_id, class_name: "DogLover", counter_cache: true end diff --git a/activerecord/test/models/dog_lover.rb b/activerecord/test/models/dog_lover.rb index a33dc575c5..2c5be94aea 100644 --- a/activerecord/test/models/dog_lover.rb +++ b/activerecord/test/models/dog_lover.rb @@ -1,4 +1,5 @@ class DogLover < ActiveRecord::Base - has_many :trained_dogs, :class_name => "Dog", :foreign_key => :trainer_id - has_many :bred_dogs, :class_name => "Dog", :foreign_key => :breeder_id + has_many :trained_dogs, class_name: "Dog", foreign_key: :trainer_id, dependent: :destroy + has_many :bred_dogs, class_name: "Dog", foreign_key: :breeder_id + has_many :dogs end diff --git a/activerecord/test/models/friendship.rb b/activerecord/test/models/friendship.rb index 6b4f7acc38..4b411ca8e0 100644 --- a/activerecord/test/models/friendship.rb +++ b/activerecord/test/models/friendship.rb @@ -1,4 +1,6 @@ class Friendship < ActiveRecord::Base belongs_to :friend, class_name: 'Person' - belongs_to :follower, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :followers_count + # friend_too exists to test a bug, and probably shouldn't be used elsewhere + belongs_to :friend_too, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :friends_too_count + belongs_to :follower, class_name: 'Person' end diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb index 6cfd443e75..69d4d7df1a 100644 --- a/activerecord/test/models/liquid.rb +++ b/activerecord/test/models/liquid.rb @@ -1,5 +1,4 @@ class Liquid < ActiveRecord::Base self.table_name = :liquid - has_many :molecules, -> { uniq } + has_many :molecules, -> { distinct } end - diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index fa717ef8d6..2985160d28 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -8,7 +8,10 @@ class Person < ActiveRecord::Base has_many :posts_with_no_comments, -> { includes(:comments).where('comments.id is null').references(:comments) }, :through => :readers, :source => :post - has_many :followers, foreign_key: 'friend_id', class_name: 'Friendship' + has_many :friendships, foreign_key: 'friend_id' + # friends_too exists to test a bug, and probably shouldn't be used elsewhere + has_many :friends_too, foreign_key: 'friend_id', class_name: 'Friendship' + has_many :followers, through: :friendships has_many :references has_many :bad_references diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 90273adafc..f893754b9f 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -1,11 +1,11 @@ class Project < ActiveRecord::Base - has_and_belongs_to_many :developers, -> { uniq.order 'developers.name desc, developers.id desc' } + has_and_belongs_to_many :developers, -> { distinct.order 'developers.name desc, developers.id desc' } has_and_belongs_to_many :readonly_developers, -> { readonly }, :class_name => "Developer" - has_and_belongs_to_many :selected_developers, -> { uniq.select "developers.*" }, :class_name => "Developer" + has_and_belongs_to_many :selected_developers, -> { distinct.select "developers.*" }, :class_name => "Developer" has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer' has_and_belongs_to_many :limited_developers, -> { limit 1 }, :class_name => "Developer" - has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").uniq }, :class_name => "Developer" - has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').uniq }, :class_name => "Developer" + has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").distinct }, :class_name => "Developer" + has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').distinct }, :class_name => "Developer" has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, :class_name => "Developer" ActiveSupport::Deprecation.silence do diff --git a/activerecord/test/models/traffic_light.rb b/activerecord/test/models/traffic_light.rb index 228f3f7bd4..a6b7edb882 100644 --- a/activerecord/test/models/traffic_light.rb +++ b/activerecord/test/models/traffic_light.rb @@ -1,3 +1,4 @@ class TrafficLight < ActiveRecord::Base serialize :state, Array + serialize :long_state, Array end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index cd9835259a..8fd4898ad6 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -219,6 +219,8 @@ ActiveRecord::Schema.define do t.integer :salary, :default => 70000 t.datetime :created_at t.datetime :updated_at + t.datetime :created_on + t.datetime :updated_on end create_table :developers_projects, :force => true, :id => false do |t| @@ -228,14 +230,16 @@ ActiveRecord::Schema.define do t.integer :access_level, :default => 1 end - create_table :dog_lovers, :force => true do |t| - t.integer :trained_dogs_count, :default => 0 - t.integer :bred_dogs_count, :default => 0 + create_table :dog_lovers, force: true do |t| + t.integer :trained_dogs_count, default: 0 + t.integer :bred_dogs_count, default: 0 + t.integer :dogs_count, default: 0 end create_table :dogs, :force => true do |t| t.integer :trainer_id t.integer :breeder_id + t.integer :dog_lover_id end create_table :edges, :force => true, :id => false do |t| @@ -278,7 +282,7 @@ ActiveRecord::Schema.define do create_table :friendships, :force => true do |t| t.integer :friend_id - t.integer :person_id + t.integer :follower_id end create_table :goofy_string_id, :force => true, :id => false do |t| @@ -492,6 +496,7 @@ ActiveRecord::Schema.define do t.integer :lock_version, :null => false, :default => 0 t.string :comments t.integer :followers_count, :default => 0 + t.integer :friends_too_count, :default => 0 t.references :best_friend t.references :best_friend_of t.integer :insures, null: false, default: 0 @@ -685,6 +690,7 @@ ActiveRecord::Schema.define do create_table :traffic_lights, :force => true do |t| t.string :location t.string :state + t.text :long_state, :null => false t.datetime :created_at t.datetime :updated_at end |