diff options
Diffstat (limited to 'activerecord')
211 files changed, 6636 insertions, 5980 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 92e0b47469..8f1f315e42 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,199 @@ ## Rails 4.0.0 (unreleased) ## +* Deprecated most of the 'dynamic finder' methods. All dynamic methods + except for `find_by_...` and `find_by_...!` are deprecated. Here's + how you can rewrite the code: + + * `find_all_by_...` can be rewritten using `where(...)` + * `find_last_by_...` can be rewritten using `where(...).last` + * `scoped_by_...` can be rewritten using `where(...)` + * `find_or_initialize_by_...` can be rewritten using + `where(...).first_or_initialize` + * `find_or_create_by_...` can be rewritten using + `where(...).first_or_create` + * `find_or_create_by_...!` can be rewritten using + `where(...).first_or_create!` + + The implementation of the deprecated dynamic finders has been moved + to the `active_record_deprecated_finders` gem. See below for details. + + *Jon Leighton* + +* Deprecated the old-style hash based finder API. This means that + methods which previously accepted "finder options" no longer do. For + example this: + + Post.find(:all, :conditions => { :comments_count => 10 }, :limit => 5) + + Should be rewritten in the new style which has existed since Rails 3: + + Post.where(comments_count: 10).limit(5) + + Note that as an interim step, it is possible to rewrite the above as: + + Post.scoped(:where => { :comments_count => 10 }, :limit => 5) + + This could save you a lot of work if there is a lot of old-style + finder usage in your application. + + Calling `Post.scoped(options)` is a shortcut for + `Post.scoped.merge(options)`. `Relation#merge` now accepts a hash of + options, but they must be identical to the names of the equivalent + finder method. These are mostly identical to the old-style finder + option names, except in the following cases: + + * `:conditions` becomes `:where` + * `:include` becomes `:includes` + * `:extend` becomes `:extending` + + The code to implement the deprecated features has been moved out to + the `active_record_deprecated_finders` gem. This gem is a dependency + of Active Record in Rails 4.0. It will no longer be a dependency + from Rails 4.1, but if your app relies on the deprecated features + then you can add it to your own Gemfile. It will be maintained by + the Rails core team until Rails 5.0 is released. + + *Jon Leighton* + +* It's not possible anymore to destroy a model marked as read only. + + *Johannes Barre* + +* Added ability to ActiveRecord::Relation#from to accept other ActiveRecord::Relation objects + + Record.from(subquery) + Record.from(subquery, :a) + + *Radoslav Stankov* + +* Added custom coders support for ActiveRecord::Store. Now you can set + your custom coder like this: + + store :settings, accessors: [ :color, :homepage ], coder: JSON + + *Andrey Voronkov* + +* `mysql` and `mysql2` connections will set `SQL_MODE=STRICT_ALL_TABLES` by + default to avoid silent data loss. This can be disabled by specifying + `strict: false` in your `database.yml`. + + *Michael Pearson* + +* Added default order to `first` to assure consistent results among + diferent database engines. Introduced `take` as a replacement to + the old behavior of `first`. + + *Marcelo Silveira* + +* Added an :index option to automatically create indexes for references + and belongs_to statements in migrations. + + The `references` and `belongs_to` methods now support an `index` + option that receives either a boolean value or an options hash + that is identical to options available to the add_index method: + + create_table :messages do |t| + t.references :person, :index => true + end + + Is the same as: + + create_table :messages do |t| + t.references :person + end + add_index :messages, :person_id + + Generators have also been updated to use the new syntax. + + [Joshua Wood] + +* Added bang methods for mutating `ActiveRecord::Relation` objects. + For example, while `foo.where(:bar)` will return a new object + leaving `foo` unchanged, `foo.where!(:bar)` will mutate the foo + object + + *Jon Leighton* + +* Added `#find_by` and `#find_by!` to mirror the functionality + provided by dynamic finders in a way that allows dynamic input more + easily: + + Post.find_by name: 'Spartacus', rating: 4 + Post.find_by "published_at < ?", 2.weeks.ago + Post.find_by! name: 'Spartacus' + + *Jon Leighton* + +* Added ActiveRecord::Base#slice to return a hash of the given methods with + their names as keys and returned values as values. + + *Guillermo Iguaran* + +* Deprecate eager-evaluated scopes. + + Don't use this: + + scope :red, where(color: 'red') + default_scope where(color: 'red') + + Use this: + + scope :red, -> { where(color: 'red') } + default_scope { where(color: 'red') } + + The former has numerous issues. It is a common newbie gotcha to do + the following: + + scope :recent, where(published_at: Time.now - 2.weeks) + + Or a more subtle variant: + + scope :recent, -> { where(published_at: Time.now - 2.weeks) } + scope :recent_red, recent.where(color: 'red') + + Eager scopes are also very complex to implement within Active + Record, and there are still bugs. For example, the following does + not do what you expect: + + scope :remove_conditions, except(:where) + where(...).remove_conditions # => still has conditions + + *Jon Leighton* + +* Remove IdentityMap + + IdentityMap has never graduated to be an "enabled-by-default" feature, due + to some inconsistencies with associations, as described in this commit: + + https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6 + + Hence the removal from the codebase, until such issues are fixed. + + *Carlos Antonio da Silva* + +* Added the schema cache dump feature. + + `Schema cache dump` feature was implemetend. This feature can dump/load internal state of `SchemaCache` instance + because we want to boot rails more quickly when we have many models. + + Usage notes: + + 1) execute rake task. + RAILS_ENV=production bundle exec rake db:schema:cache:dump + => generate db/schema_cache.dump + + 2) add config.use_schema_cache_dump = true in config/production.rb. BTW, true is default. + + 3) boot rails. + RAILS_ENV=production bundle exec rails server + => use db/schema_cache.dump + + 4) If you remove clear dumped cache, execute rake task. + RAILS_ENV=production bundle exec rake db:schema:cache:clear + => remove db/schema_cache.dump + + *kennyj* + * Added support for partial indices to PostgreSQL adapter The `add_index` method now supports a `where` option that receives a @@ -130,6 +324,28 @@ * PostgreSQL hstore types are automatically deserialized from the database. +## Rails 3.2.3 (March 30, 2012) ## + +* Added find_or_create_by_{attribute}! dynamic method. *Andrew White* + +* Whitelist all attribute assignment by default. Change the default for newly generated applications to whitelist all attribute assignment. Also update the generated model classes so users are reminded of the importance of attr_accessible. *NZKoz* + +* Update ActiveRecord::AttributeMethods#attribute_present? to return false for empty strings. *Jacobkg* + +* Fix associations when using per class databases. *larskanis* + +* Revert setting NOT NULL constraints in add_timestamps *fxn* + +* Fix mysql to use proper text types. Fixes #3931. *kennyj* + +* Fix #5069 - Protect foreign key from mass assignment through association builder. *byroot* + + +## Rails 3.2.2 (March 1, 2012) ## + +* No changes. + + ## Rails 3.2.1 (January 26, 2012) ## * The threshold for auto EXPLAIN is ignored if there's no logger. *fxn* @@ -320,6 +536,7 @@ *Aaron Christy* + ## Rails 3.1.4 (March 1, 2012) ## * Fix a custom primary key regression *GH 3987* @@ -347,6 +564,7 @@ *Julius de Bruijn* + ### Rails 3.1.3 (November 20, 2011) ## * Perf fix: If we're deleting all records in an association, don't add a IN(..) clause @@ -360,6 +578,7 @@ *Christos Zisopoulos and Kenny J* + ### Rails 3.1.2 (November 18, 2011) ## * Fix bug with PostgreSQLAdapter#indexes. When the search path has multiple schemas, spaces @@ -407,6 +626,7 @@ *Kenny J* + ## Rails 3.1.1 (October 7, 2011) ## * Add deprecation for the preload_associations method. Fixes #3022. @@ -445,19 +665,6 @@ a URI that specifies the connection configuration. For example: ActiveRecord::Base.establish_connection 'postgres://localhost/foo' -* Active Record's dynamic finder will now raise the error if you passing in less number of arguments than what you call in method signature. - - So if you were doing this and expecting the second argument to be nil: - - User.find_by_username_and_group("sikachu") - - You'll now get `ArgumentError: wrong number of arguments (1 for 2).` You'll then have to do this: - - User.find_by_username_and_group("sikachu", nil) - - *Prem Sichanugrist* - - ## Rails 3.1.0 (August 30, 2011) ## * Add a proxy_association method to association proxies, which can be called by association @@ -492,7 +699,7 @@ has_one :account end - user.build_account{ |a| a.credit_limit => 100.0 } + user.build_account{ |a| a.credit_limit = 100.0 } The block is called after the instance has been initialized. *Andrew White* @@ -3865,7 +4072,7 @@ * Fixed that adding a record to a has_and_belongs_to collection would always save it -- now it only saves if its a new record #1203 *Alisdair McDiarmid* -* Fixed saving of in-memory association structures to happen as a after_create/after_update callback instead of after_save -- that way you can add new associations in after_create/after_update callbacks without getting them saved twice +* Fixed saving of in-memory association structures to happen as an after_create/after_update callback instead of after_save -- that way you can add new associations in after_create/after_update callbacks without getting them saved twice * Allow any Enumerable, not just Array, to work as bind variables #1344 *Jeremy Kemper* @@ -5378,7 +5585,7 @@ * Fixed that adding a record to a has_and_belongs_to collection would always save it -- now it only saves if its a new record #1203 *Alisdair McDiarmid* -* Fixed saving of in-memory association structures to happen as a after_create/after_update callback instead of after_save -- that way you can add new associations in after_create/after_update callbacks without getting them saved twice +* Fixed saving of in-memory association structures to happen as an after_create/after_update callback instead of after_save -- that way you can add new associations in after_create/after_update callbacks without getting them saved twice * Allow any Enumerable, not just Array, to work as bind variables #1344 *Jeremy Kemper* @@ -6300,7 +6507,7 @@ post.categories.push_with_attributes(category, :added_on => Date.today) post.categories.first.added_on # => Date.today - NOTE: The categories table doesn't have a added_on column, it's the categories_post join table that does! + NOTE: The categories table doesn't have an added_on column, it's the categories_post join table that does! * Fixed that :exclusively_dependent and :dependent can't be activated at the same time on has_many associations *Jeremy Kemper* diff --git a/activerecord/RUNNING_UNIT_TESTS b/activerecord/RUNNING_UNIT_TESTS index a1ed6712c5..2c310e7ac3 100644 --- a/activerecord/RUNNING_UNIT_TESTS +++ b/activerecord/RUNNING_UNIT_TESTS @@ -26,13 +26,6 @@ You can run all the tests for a given database via rake: The 'rake test' task will run all the tests for mysql, mysql2, sqlite3 and postgresql. -== Identity Map - -By default the tests run with the Identity Map turned off. But all tests should pass whether or -not the identity map is on or off. You can turn it on using the IM env variable: - - $ IM=true ruby -Itest test/case/base_test.rb - == Config file By default, the config file is expected to be at the path test/config.yml. You can specify a diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 4090293b56..7feb0b75a0 100755..100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rubygems/package_task' diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 26eb74df0f..dca7f13fd2 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -8,6 +8,7 @@ Gem::Specification.new do |s| s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.' s.required_ruby_version = '>= 1.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' @@ -22,4 +23,6 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) s.add_dependency('arel', '~> 3.0.2') + + s.add_dependency('active_record_deprecated_finders', '0.0.1') end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 73c8a06ab7..210820062b 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -22,9 +22,9 @@ #++ require 'active_support' -require 'active_support/i18n' require 'active_model' require 'arel' +require 'active_record_deprecated_finders' require 'active_record/version' @@ -62,10 +62,7 @@ module ActiveRecord autoload :CounterCache autoload :ConnectionHandling autoload :DynamicMatchers - autoload :DynamicFinderMatch - autoload :DynamicScopeMatch autoload :Explain - autoload :IdentityMap autoload :Inheritance autoload :Integration autoload :Migration @@ -147,4 +144,6 @@ ActiveSupport.on_load(:active_record) do Arel::Table.engine = self end -I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml' +ActiveSupport.on_load(:i18n) do + I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml' +end diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index c39284539c..3ae7030caa 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -10,9 +10,9 @@ module ActiveRecord # Active Record implements aggregation through a macro-like class method called +composed_of+ # for representing attributes as value objects. It expresses relationships like "Account [is] # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call - # to the macro adds a description of how the value objects are created from the attributes of - # the entity object (when the entity is initialized either as a new object or from finding an - # existing object) and how it can be turned back into attributes (when the entity is saved to + # to the macro adds a description of how the value objects are created from the attributes of + # the entity object (when the entity is initialized either as a new object or from finding an + # existing object) and how it can be turned back into attributes (when the entity is saved to # the database). # # class Customer < ActiveRecord::Base @@ -71,7 +71,7 @@ module ActiveRecord # Now it's possible to access attributes from the database through the value objects instead. If # you choose to name the composition the same as the attribute's name, it will be the only way to # access that attribute. That's the case with our +balance+ attribute. You interact with the value - # objects just like you would any other attribute, though: + # objects just like you would with any other attribute: # # customer.balance = Money.new(20) # sets the Money value object and the attribute # customer.balance # => Money value object @@ -86,6 +86,12 @@ module ActiveRecord # customer.address_street = "Hyancintvej" # customer.address_city = "Copenhagen" # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # + # customer.address_street = "Vesterbrogade" + # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # customer.clear_aggregation_cache + # customer.address # => Address.new("Vesterbrogade", "Copenhagen") + # # customer.address = Address.new("May Street", "Chicago") # customer.address_street # => "May Street" # customer.address_city # => "Chicago" @@ -101,8 +107,8 @@ module ActiveRecord # ActiveRecord::Base classes are entity objects. # # It's also important to treat the value objects as immutable. Don't allow the Money object to have - # its amount changed after creation. Create a new Money object with the new value instead. This - # is exemplified by the Money#exchange_to method that returns a new value object instead of changing + # its amount changed after creation. Create a new Money object with the new value instead. The + # Money#exchange_to method is an example of this. It returns a new value object instead of changing # its own values. Active Record won't persist value objects that have been changed through means # other than the writer method. # @@ -119,7 +125,7 @@ module ActiveRecord # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows # a custom constructor to be specified. # - # When a new value is assigned to the value object the default assumption is that the new value + # When a new value is assigned to the value object, the default assumption is that the new value # is an instance of the value class. Specifying a custom converter allows the new value to be automatically # converted to an instance of value class if necessary. # @@ -187,7 +193,8 @@ module ActiveRecord # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> # or a Proc that is called when a new value is assigned to the value object. The converter is # passed the single value that is used in the assignment and is only called if the new value is - # not an instance of <tt>:class_name</tt>. + # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter + # can return nil to skip the assignment. # # Option examples: # composed_of :temperature, :mapping => %w(reading celsius) @@ -235,16 +242,15 @@ module ActiveRecord def writer_method(name, class_name, mapping, allow_nil, converter) define_method("#{name}=") do |part| + klass = class_name.constantize + unless part.is_a?(klass) || converter.nil? || part.nil? + part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part) + end + if part.nil? && allow_nil mapping.each { |pair| self[pair.first] = nil } @aggregation_cache[name] = nil else - unless part.is_a?(class_name.constantize) || converter.nil? - part = converter.respond_to?(:call) ? - converter.call(part) : - class_name.constantize.send(converter, part) - end - mapping.each { |pair| self[pair.first] = part.send(pair.last) } @aggregation_cache[name] = part.freeze end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 958821add6..68f8bbeb1c 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -543,7 +543,7 @@ module ActiveRecord # end # # @group = Group.first - # @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group + # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group # @group.avatars # selects all avatars by going through the User join model. # # An important caveat with going through +has_one+ or +has_many+ associations on the @@ -1129,7 +1129,7 @@ module ActiveRecord # it would skip the first 4 rows. # [:select] # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if - # you, for example, want to do a join but not include the joined columns. Do not forget + # you want to do a join but not include the joined columns, for example. Do not forget # to include the primary and foreign keys, otherwise it will raise an error. # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). @@ -1264,8 +1264,8 @@ module ActiveRecord # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, - # you want to do a join but not include the joined columns. Do not forget to include the + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if + # you want to do a join but not include the joined columns, for example. Do not forget to include the # primary and foreign keys, otherwise it will raise an error. # [:through] # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>, @@ -1355,7 +1355,7 @@ module ActiveRecord # SQL fragment, such as <tt>authorized = 1</tt>. # [:select] # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed - # if, for example, you want to do a join but not include the joined columns. Do not + # if you want to do a join but not include the joined columns, for example. Do not # forget to include the primary and foreign keys, otherwise it will raise an error. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name @@ -1382,7 +1382,9 @@ module ActiveRecord # and +decrement_counter+. The counter cache is incremented when an object of this # class is created and decremented when it's destroyed. This requires that a column # named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class) - # is used on the associate class (such as a Post class). You can also specify a custom counter + # is used on the associate class (such as a Post class) - that is the migration for + # <tt>#{table_name}_count</tt> is created on the associate class (such that Post.comments_count will + # return the count cached, see note below). You can also specify a custom counter # cache column by providing a column name instead of a +true+/+false+ value to this # option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.) # Note: Specifying a counter cache will add it to that model's list of readonly attributes @@ -1430,7 +1432,7 @@ module ActiveRecord # Specifies a many-to-many relationship with another class. This associates two classes via an # intermediate join table. Unless the join table is explicitly specified as an option, it is # guessed using the lexical order of the class names. So a join between Developer and Project - # will give the default join table name of "developers_projects" because "D" outranks "P". + # will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically. # Note that this precedence is calculated using the <tt><</tt> operator for String. This # means that if the strings are of different lengths, and the strings are equal when compared # up to the shortest length, then the longer string is considered of higher @@ -1512,8 +1514,8 @@ module ActiveRecord # * <tt>Developer#projects.size</tt> # * <tt>Developer#projects.find(id)</tt> # * <tt>Developer#projects.exists?(...)</tt> - # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>) - # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>) + # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>) + # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>) # The declaration may include an options hash to specialize the behavior of the association. # # === Options @@ -1574,8 +1576,8 @@ module ActiveRecord # An integer determining the offset from where the rows should be fetched. So at 5, # it would skip the first 4 rows. # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, - # you want to do a join but not include the joined columns. Do not forget to include the primary + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if + # you want to do a join but exclude the joined columns, for example. Do not forget to include the primary # and foreign keys, otherwise it will raise an error. # [:readonly] # If true, all the associated objects are readonly through the association. @@ -1594,7 +1596,7 @@ module ActiveRecord # has_and_belongs_to_many :categories, :join_table => "prods_cats" # has_and_belongs_to_many :categories, :readonly => true # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => - # "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" + # proc { |record| "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" } def has_and_belongs_to_many(name, options = {}, &extension) Builder::HasAndBelongsToMany.build(self, name, options, &extension) end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 7887d59aad..e75003f261 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -25,9 +25,7 @@ module ActiveRecord def initialize(owner, reflection) reflection.check_validity! - @target = nil @owner, @reflection = owner, reflection - @updated = false reset reset_scope @@ -38,14 +36,14 @@ module ActiveRecord # post.comments.aliased_table_name # => "comments" # def aliased_table_name - reflection.klass.table_name + klass.table_name end # Resets the \loaded flag to +false+ and sets the \target to +nil+. def reset @loaded = false - IdentityMap.remove(target) if IdentityMap.enabled? && target @target = nil + @stale_state = nil end # Reloads the \target and returns +self+ on success. @@ -134,17 +132,8 @@ module ActiveRecord # ActiveRecord::RecordNotFound is rescued within the method, and it is # not reraised. The proxy is \reset and +nil+ is the return value. def load_target - if find_target? - begin - if IdentityMap.enabled? && association_class && association_class.respond_to?(:base_class) - @target = IdentityMap.get(association_class, owner[reflection.foreign_key]) - end - rescue NameError - nil - ensure - @target ||= find_target - end - end + @target = find_target if (@stale_state && stale_target?) || find_target? + loaded! unless loaded? target rescue ActiveRecord::RecordNotFound @@ -225,13 +214,10 @@ module ActiveRecord def stale_state end - def association_class - @reflection.klass - end - def build_record(attributes, options) reflection.build_association(attributes, options) do |record| - record.assign_attributes(create_scope.except(*record.changed), :without_protection => true) + attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) + record.assign_attributes(attributes, :without_protection => true) end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 2972b7e13e..5a44d3a156 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -15,19 +15,20 @@ module ActiveRecord def scope scope = klass.unscoped - scope = scope.extending(*Array(options[:extend])) + + scope.extending!(*Array(options[:extend])) # It's okay to just apply all these like this. The options will only be present if the # association supports that option; this is enforced by the association builder. - scope = scope.apply_finder_options(options.slice( - :readonly, :include, :references, :order, :limit, :joins, :group, :having, :offset, :select)) + scope.merge!(options.slice( + :readonly, :references, :order, :limit, :joins, :group, :having, :offset, :select, :uniq)) - if options[:through] && !options[:include] - scope = scope.includes(source_options[:include]) + if options[:include] + scope.includes! options[:include] + elsif options[:through] + scope.includes! source_options[:include] end - scope = scope.uniq if options[:uniq] - add_constraints(scope) end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 97f531d064..ddfc6f6c05 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -14,6 +14,11 @@ module ActiveRecord self.target = record end + def reset + super + @updated = false + end + def updated? @updated end @@ -72,7 +77,7 @@ module ActiveRecord end def stale_state - owner[reflection.foreign_key].to_s + owner[reflection.foreign_key] && owner[reflection.foreign_key].to_s end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 2ee5dbbd70..88ce03a3cd 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -27,7 +27,8 @@ module ActiveRecord end def stale_state - [super, owner[reflection.foreign_type].to_s] + foreign_key = super + foreign_key && [foreign_key.to_s, owner[reflection.foreign_type].to_s] end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 2059d8acdf..9a6896dd55 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -69,12 +69,12 @@ module ActiveRecord::Associations::Builder def define_restrict_dependency_method name = self.name mixin.redefine_method(dependency_method_name) do - # has_many or has_one associations - if send(name).respond_to?(:exists?) ? send(name).exists? : !send(name).nil? + has_one_macro = association(name).reflection.macro == :has_one + if has_one_macro ? !send(name).nil? : send(name).exists? if dependent_restrict_raises? raise ActiveRecord::DeleteRestrictionError.new(name) else - key = association(name).reflection.macro == :has_one ? "one" : "many" + key = has_one_macro ? "one" : "many" errors.add(:base, :"restrict_dependent_destroy.#{key}", :record => self.class.human_attribute_name(name).downcase) return false diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 0b634ab944..30fc44b4c2 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -18,7 +18,7 @@ module ActiveRecord::Associations::Builder model.send(:include, Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 def destroy_associations - association(#{name.to_sym.inspect}).delete_all_on_destroy + association(#{name.to_sym.inspect}).delete_all super end RUBY diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 9ddfd433e4..d37d4e9d33 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -42,7 +42,7 @@ module ActiveRecord::Associations::Builder def define_delete_all_dependency_method name = self.name mixin.redefine_method(dependency_method_name) do - association(name).delete_all_on_destroy + association(name).delete_all end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index b2136605e1..4ec176e641 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -6,6 +6,15 @@ module ActiveRecord # ease the implementation of association proxies that represent # collections. See the class hierarchy in AssociationProxy. # + # CollectionAssociation: + # HasAndBelongsToManyAssociation => has_and_belongs_to_many + # HasManyAssociation => has_many + # HasManyThroughAssociation + ThroughAssociation => has_many :through + # + # CollectionAssociation class provides common methods to the collections + # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with + # +:through association+ option. + # # You need to be careful with assumptions regarding the target: The proxy # does not fetch records from the database until it needs them, but new # ones created with +build+ are added to the target. So, the target may be @@ -16,12 +25,6 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: - attr_reader :proxy - - def initialize(owner, reflection) - super - @proxy = CollectionProxy.new(self) - end # Implements the reader method, e.g. foo.items for Foo.has_many :items def reader(force_reload = false) @@ -31,7 +34,7 @@ module ActiveRecord reload end - proxy + CollectionProxy.new(self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -71,7 +74,7 @@ module ActiveRecord end def reset - @loaded = false + super @target = [] end @@ -121,8 +124,9 @@ module ActiveRecord create_record(attributes, options, true, &block) end - # Add +records+ to this association. Returns +self+ so method calls may be chained. - # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. + # Add +records+ to this association. Returns +self+ so method calls may + # be chained. Since << flattens its argument list and inserts each record, + # +push+ and +concat+ behave identically. def concat(*records) load_target if owner.new_record? @@ -148,23 +152,16 @@ module ActiveRecord end end - # Remove all records from this association + # Remove all records from this association. # # See delete for more info. def delete_all - delete(load_target).tap do + delete(:all).tap do reset loaded! end end - # Called when the association is declared as :dependent => :delete_all. This is - # an optimised version which avoids loading the records into memory. Not really - # for public consumption. - def delete_all_on_destroy - scoped.delete_all - end - # Destroy all the records from this association. # # See destroy for more info. @@ -175,7 +172,7 @@ module ActiveRecord end end - # Calculate sum using SQL, not Enumerable + # Calculate sum using SQL, not Enumerable. def sum(*args) if block_given? scoped.sum(*args) { |*block_args| yield(*block_args) } @@ -224,7 +221,17 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) - delete_or_destroy(records, options[:dependent]) + dependent = options[:dependent] + + if records.first == :all + if loaded? || dependent == :destroy + delete_or_destroy(load_target, dependent) + else + delete_records(:all, dependent) + end + else + delete_or_destroy(records, dependent) + end end # Destroy +records+ and remove them from this association calling @@ -248,8 +255,12 @@ module ActiveRecord # This method is abstract in the sense that it relies on # +count_records+, which is a method descendants have to provide. def size - if !find_target? || (loaded? && !options[:uniq]) - target.size + if !find_target? || loaded? + if options[:uniq] + target.uniq.size + else + target.size + end elsif !loaded? && options[:group] load_target.size elsif !loaded? && !options[:uniq] && target.is_a?(Array) @@ -269,13 +280,16 @@ module ActiveRecord load_target.size end - # Equivalent to <tt>collection.size.zero?</tt>. If the collection has - # not been already loaded and you are going to fetch the records anyway - # it is better to check <tt>collection.length.zero?</tt>. + # Returns true if the collection is empty. Equivalent to + # <tt>collection.size.zero?</tt>. If the collection has not been already + # loaded and you are going to fetch the records anyway it is better to + # check <tt>collection.length.zero?</tt>. def empty? size.zero? end + # Returns true if the collections is not empty. + # Equivalent to +!collection.empty?+. def any? if block_given? load_target.any? { |*block_args| yield(*block_args) } @@ -284,7 +298,8 @@ module ActiveRecord end end - # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1. + # Returns true if the collection has more than 1 record. + # Equivalent to +collection.size > 1+. def many? if block_given? load_target.many? { |*block_args| yield(*block_args) } @@ -300,8 +315,8 @@ module ActiveRecord end end - # Replace this collection with +other_array+ - # This will perform a diff and delete/add only records that have changed. + # Replace this collection with +other_array+. This will perform a diff + # and delete/add only records that have changed. def replace(other_array) other_array.each { |val| raise_on_type_mismatch(val) } original_target = load_target.dup @@ -474,6 +489,8 @@ module ActiveRecord raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ "new records could not be saved." end + + target end def concat_records(records) @@ -544,7 +561,7 @@ module ActiveRecord # If using a custom finder_sql, #find scans the entire collection. def find_by_scan(*args) expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.uniq.map { |arg| arg.to_i } + ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq if ids.size == 1 id = ids.first diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 5eda0387c4..100fb38dec 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -33,16 +33,665 @@ module ActiveRecord # # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. - class CollectionProxy # :nodoc: - alias :proxy_extend :extend + class CollectionProxy < Relation + delegate :target, :load_target, :loaded?, :to => :@association - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } + ## + # :method: select + # + # :call-seq: + # select(select = nil) + # select(&block) + # + # Works in two ways. + # + # *First:* Specify a subset of fields to be selected from the result set. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.select(:name) + # # => [ + # # #<Pet id: nil, name: "Fancy-Fancy">, + # # #<Pet id: nil, name: "Spook">, + # # #<Pet id: nil, name: "Choo-Choo"> + # # ] + # + # person.pets.select([:id, :name]) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy">, + # # #<Pet id: 2, name: "Spook">, + # # #<Pet id: 3, name: "Choo-Choo"> + # # ] + # + # Be careful because this also means you’re initializing a model + # object with only the fields that you’ve selected. If you attempt + # to access a field that is not in the initialized record you’ll + # receive: + # + # person.pets.select(:name).first.person_id + # # => ActiveModel::MissingAttributeError: missing attribute: person_id + # + # *Second:* You can pass a block so it can be used just like Array#select. + # This build an array of objects from the database for the scope, + # converting them into an array and iterating through them using + # Array#select. + # + # person.pets.select { |pet| pet.name =~ /oo/ } + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.select(:name) { |pet| pet.name =~ /oo/ } + # # => [ + # # #<Pet id: 2, name: "Spook">, + # # #<Pet id: 3, name: "Choo-Choo"> + # # ] - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, - :lock, :readonly, :having, :pluck, :to => :scoped + ## + # :method: find + # + # :call-seq: + # find(*args, &block) + # + # Finds an object in the collection responding to the +id+. Uses the same + # rules as +ActiveRecord::Base.find+. Returns +ActiveRecord::RecordNotFound++ + # error if the object can not be found. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4 + # + # person.pets.find(2) { |pet| pet.name.downcase! } + # # => #<Pet id: 2, name: "fancy-fancy", person_id: 1> + # + # person.pets.find(2, 3) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] - delegate :target, :load_target, :loaded?, :to => :@association + ## + # :method: first + # + # :call-seq: + # first(limit = nil) + # + # Returns the first record, or the first +n+ records, from the collection. + # If the collection is empty, the first form returns +nil+, and the second + # form returns an empty array. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.first # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.first(2) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.first # => nil + # another_person_without.pets.first(3) # => [] + + ## + # :method: last + # + # :call-seq: + # last(limit = nil) + # + # Returns the last record, or the last +n+ records, from the collection. + # If the collection is empty, the first form returns +nil+, and the second + # form returns an empty array. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.last # => #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # + # person.pets.last(2) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.last # => nil + # another_person_without.pets.last(3) # => [] + + ## + # :method: build + # + # :call-seq: + # build(attributes = {}, options = {}, &block) + # + # Returns a new object of the collection type that has been instantiated + # with +attributes+ and linked to this object, but have not yet been saved. + # You can pass an array of attributes hashes, this will return an array + # with the new objects. + # + # class Person + # has_many :pets + # end + # + # person.pets.build + # # => #<Pet id: nil, name: nil, person_id: 1> + # + # person.pets.build(name: 'Fancy-Fancy') + # # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}]) + # # => [ + # # #<Pet id: nil, name: "Spook", person_id: 1>, + # # #<Pet id: nil, name: "Choo-Choo", person_id: 1>, + # # #<Pet id: nil, name: "Brain", person_id: 1> + # # ] + # + # person.pets.size # => 5 # size of the collection + # person.pets.count # => 0 # count from database + + ## + # :method: create + # + # :call-seq: + # create(attributes = {}, options = {}, &block) + # + # Returns a new object of the collection type that has been instantiated with + # attributes, linked to this object and that has already been saved (if it + # passes the validations). + # + # class Person + # has_many :pets + # end + # + # person.pets.create(name: 'Fancy-Fancy') + # # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.create([{name: 'Spook'}, {name: 'Choo-Choo'}]) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 3 + # person.pets.count # => 3 + # + # person.pets.find(1, 2, 3) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + + ## + # :method: create! + # + # :call-seq: + # create!(attributes = {}, options = {}, &block) + # + # Like +create+, except that if the record is invalid, raises an exception. + # + # class Person + # has_many :pets + # end + # + # class Pet + # attr_accessible :name + # validates :name, presence: true + # end + # + # person.pets.create!(name: nil) + # # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank + + ## + # :method: concat + # + # :call-seq: + # concat(*records) + # + # Add one or more records to the collection by setting their foreign keys + # to the association's primary key. Since << flattens its argument list and + # inserts each record, +push+ and +concat+ behave identically. Returns +self+ + # so method calls may be chained. + # + # class Person < ActiveRecord::Base + # pets :has_many + # end + # + # person.pets.size # => 0 + # person.pets.concat(Pet.new(name: 'Fancy-Fancy')) + # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')) + # person.pets.size # => 3 + # + # person.id # => 1 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')]) + # person.pets.size # => 5 + + ## + # :method: replace + # + # :call-seq: + # replace(other_array) + # + # Replace this collection with +other_array+. This will perform a diff + # and delete/add only records that have changed. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>] + # + # other_pets = [Pet.new(name: 'Puff', group: 'celebrities'] + # + # person.pets.replace(other_pets) + # + # person.pets + # # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>] + # + # If the supplied array has an incorrect association type, it raises + # an <tt>ActiveRecord::AssociationTypeMismatch</tt> error: + # + # person.pets.replace(["doo", "ggie", "gaga"]) + # # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String + + ## + # :method: delete_all + # + # Deletes all the records from the collection. For +has_many+ asssociations, + # the deletion is done according to the strategy specified by the <tt>:dependent</tt> + # option. Returns an array with the deleted records. + # + # If no <tt>:dependent</tt> option is given, then it will follow the + # default strategy. The default strategy is <tt>:nullify</tt>. This + # sets the foreign keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, + # the default strategy is +delete_all+. + # + # class Person < ActiveRecord::Base + # has_many :pets # dependent: :nullify option by default + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1, 2, 3) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: nil>, + # # #<Pet id: 2, name: "Spook", person_id: nil>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: nil> + # # ] + # + # If it is set to <tt>:destroy</tt> all the objects from the collection + # are removed by calling their +destroy+ method. See +destroy+ for more + # information. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :destroy + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1, 2, 3) + # # => ActiveRecord::RecordNotFound + # + # If it is set to <tt>:delete_all</tt>, all the objects are deleted + # *without* calling their +destroy+ method. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :delete_all + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.delete_all + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # Pet.find(1, 2, 3) + # # => ActiveRecord::RecordNotFound + + ## + # :method: destroy_all + # + # Deletes the records of the collection directly from the database. + # This will _always_ remove the records ignoring the +:dependent+ + # option. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy_all + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1) # => Couldn't find Pet with id=1 + + ## + # :method: destroy + # + # :call-seq: + # destroy(*records) + # + # Destroy the +records+ supplied and remove them from the collection. + # This method will _always_ remove record from the database ignoring + # the +:dependent+ option. Returns an array with the removed records. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy(Pet.find(1)) + # # => [#<Pet id: 1, name: "Fancy-Fancy", person_id: 1>] + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.destroy(Pet.find(2), Pet.find(3)) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3) + # + # You can pass +Fixnum+ or +String+ values, it finds the records + # responding to the +id+ and then deletes them from the database. + # + # person.pets.size # => 3 + # person.pets + # # => [ + # # #<Pet id: 4, name: "Benny", person_id: 1>, + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.destroy("4") + # # => #<Pet id: 4, name: "Benny", person_id: 1> + # + # person.pets.size # => 2 + # person.pets + # # => [ + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.destroy(5, 6) + # # => [ + # # #<Pet id: 5, name: "Brain", person_id: 1>, + # # #<Pet id: 6, name: "Boss", person_id: 1> + # # ] + # + # person.pets.size # => 0 + # person.pets # => [] + # + # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6) + ## + # :method: size + # + # Returns the size of the collection. If the collection hasn't been loaded, + # it executes a <tt>SELECT COUNT(*)</tt> query. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1 + # + # person.pets # This will execute a SELECT * FROM query + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.size # => 3 + # # Because the collection is already loaded, this will behave like + # # collection.size and no SQL count query is executed. + + ## + # :method: length + # + # Returns the size of the collection calling +size+ on the target. + # If the collection has been already loaded, +length+ and +size+ are + # equivalent. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.length # => 3 + # # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1 + # + # # Because the collection is loaded, you can + # # call the collection with no additional queries: + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + + ## + # :method: empty? + # + # Returns +true+ if the collection is empty. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count # => 1 + # person.pets.empty? # => false + # + # person.pets.delete_all + # + # person.pets.count # => 0 + # person.pets.empty? # => true + + ## + # :method: any? + # + # :call-seq: + # any? + # any?{|item| block} + # + # Returns +true+ if the collection is not empty. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count # => 0 + # person.pets.any? # => false + # + # person.pets << Pet.new(name: 'Snoop') + # person.pets.count # => 0 + # person.pets.any? # => true + # + # You can also pass a block to define criteria. The behaviour + # is the same, it returns true if the collection based on the + # criteria is not empty. + # + # person.pets + # # => [#<Pet name: "Snoop", group: "dogs">] + # + # person.pets.any? do |pet| + # pet.group == 'cats' + # end + # # => false + # + # person.pets.any? do |pet| + # pet.group == 'dogs' + # end + # # => true + + ## + # :method: many? + # + # :call-seq: + # many? + # many?{|item| block} + # + # Returns true if the collection has more than one record. + # Equivalent to <tt>collection.size > 1</tt>. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count #=> 1 + # person.pets.many? #=> false + # + # person.pets << Pet.new(name: 'Snoopy') + # person.pets.count #=> 2 + # person.pets.many? #=> true + # + # You can also pass a block to define criteria. The + # behaviour is the same, it returns true if the collection + # based on the criteria has more than one record. + # + # person.pets + # # => [ + # # #<Pet name: "Gorby", group: "cats">, + # # #<Pet name: "Puff", group: "cats">, + # # #<Pet name: "Snoop", group: "dogs"> + # # ] + # + # person.pets.many? do |pet| + # pet.group == 'dogs' + # end + # # => false + # + # person.pets.many? do |pet| + # pet.group == 'cats' + # end + # # => true + + ## + # :method: include? + # + # :call-seq: + # include?(record) + # + # Returns +true+ if the given object is present in the collection. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets # => [#<Pet id: 20, name: "Snoop">] + # + # person.pets.include?(Pet.find(20)) # => true + # person.pets.include?(Pet.find(21)) # => false delegate :select, :find, :first, :last, :build, :create, :create!, :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq, @@ -52,7 +701,8 @@ module ActiveRecord def initialize(association) @association = association - Array(association.options[:extend]).each { |ext| proxy_extend(ext) } + super association.klass, association.klass.arel_table + merge! association.scoped end alias_method :new, :build @@ -61,50 +711,28 @@ module ActiveRecord @association end - def scoped - association = @association - association.scoped.extending do - define_method(:proxy_association) { association } - end + # We don't want this object to be put on the scoping stack, because + # that could create an infinite loop where we call an @association + # method, which gets the current scope, which is this object, which + # delegates to @association, and so on. + def scoping + @association.scoped.scoping { yield } end - def respond_to?(name, include_private = false) - super || - (load_target && target.respond_to?(name, include_private)) || - proxy_association.klass.respond_to?(name, include_private) + def spawn + scoped end - def method_missing(method, *args, &block) - match = DynamicFinderMatch.match(method) - if match && match.instantiator? - send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r| - proxy_association.send :set_owner_attributes, r - proxy_association.send :add_to_target, r - yield(r) if block_given? - end - - elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method)) - if load_target - if target.respond_to?(method) - target.send(method, *args, &block) - else - begin - super - rescue NoMethodError => e - raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}") - end - end - end - - else - scoped.readonly(nil).send(method, *args, &block) + def scoped(options = nil) + association = @association + + super.extending! do + define_method(:proxy_association) { association } end end - # Forwards <tt>===</tt> explicitly to the \target because the instance method - # removal above doesn't catch it. Loads the \target if needed. - def ===(other) - other === load_target + def ==(other) + load_target == other end def to_ary @@ -112,16 +740,57 @@ module ActiveRecord end alias_method :to_a, :to_ary + # Adds one or more +records+ to the collection by setting their foreign keys + # to the association‘s primary key. Returns +self+, so several appends may be + # chained together. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 0 + # person.pets << Pet.new(name: 'Fancy-Fancy') + # person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')] + # person.pets.size # => 3 + # + # person.id # => 1 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] def <<(*records) proxy_association.concat(records) && self end alias_method :push, :<< + # Equivalent to +delete_all+. The difference is that returns +self+, instead + # of an array with the deleted objects, so methods can be chained. See + # +delete_all+ for more information. def clear delete_all self end + # Reloads the collection from the database. Returns +self+. + # Equivalent to <tt>collection(true)</tt>. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets # uses the pets cache + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets.reload # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets(true) # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload proxy_association.reload self diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index a4cea99372..58d041ec1d 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 @@ -32,10 +32,6 @@ module ActiveRecord record end - # ActiveRecord::Relation#delete_all needs to support joins before we can use a - # SQL-only implementation. - alias delete_all_on_destroy delete_all - private def count_records @@ -44,13 +40,20 @@ module ActiveRecord def delete_records(records, method) if sql = options[:delete_sql] + records = load_target if records == :all records.each { |record| owner.connection.delete(interpolate(sql, record)) } else - relation = join_table - stmt = relation.where(relation[reflection.foreign_key].eq(owner.id). - and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) - ).compile_delete - owner.connection.delete stmt + relation = join_table + condition = relation[reflection.foreign_key].eq(owner.id) + + unless records == :all + condition = condition.and( + relation[reflection.association_foreign_key] + .in(records.map { |x| x.id }.compact) + ) + end + + owner.connection.delete(relation.where(condition).compile_delete) end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 059e6c77bc..e631579087 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -89,8 +89,12 @@ module ActiveRecord records.each { |r| r.destroy } update_counter(-records.length) unless inverse_updates_counter_cache? else - keys = records.map { |r| r[reflection.association_primary_key] } - scope = scoped.where(reflection.association_primary_key => keys) + if records == :all + scope = scoped + else + keys = records.map { |r| r[reflection.association_primary_key] } + scope = scoped.where(reflection.association_primary_key => keys) + end if method == :delete_all update_counter(-scope.delete_all) diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 9657cb081d..2683aaf5da 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -54,10 +54,6 @@ module ActiveRecord record end - # ActiveRecord::Relation#delete_all needs to support joins before we can use a - # SQL-only implementation. - alias delete_all_on_destroy delete_all - private def through_association @@ -73,7 +69,9 @@ module ActiveRecord # association def build_through_record(record) @through_records[record.object_id] ||= begin - through_record = through_association.build(construct_join_attributes(record)) + ensure_mutable + + through_record = through_association.build through_record.send("#{source_reflection.name}=", record) through_record end @@ -124,7 +122,12 @@ module ActiveRecord def delete_records(records, method) ensure_not_nested - scope = through_association.scoped.where(construct_join_attributes(*records)) + # This is unoptimised; it will load all the target records + # even when we just want to delete everything. + records = load_target if records == :all + + scope = through_association.scoped + scope.where! construct_join_attributes(*records) case method when :destroy diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index fafed94ff2..54705e4950 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -12,7 +12,7 @@ module ActiveRecord # and all of its books via a single query: # # SELECT * FROM authors - # LEFT OUTER JOIN books ON authors.id = books.id + # LEFT OUTER JOIN books ON authors.id = books.author_id # WHERE authors.name = 'Ken Akamatsu' # # However, this could result in many rows that contain redundant data. After diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 253998fb23..b4c3908b10 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -77,7 +77,7 @@ module ActiveRecord # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) # Make several smaller queries if necessary or make one query if the adapter supports it sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size) - records = sliced.map { |slice| records_for(slice) }.flatten + records = sliced.map { |slice| records_for(slice).to_a }.flatten end # Each record may have multiple owners, and vice-versa @@ -93,7 +93,8 @@ module ActiveRecord end def build_scope - scope = klass.scoped + scope = klass.unscoped + scope.default_scoped = true scope = scope.where(interpolate(options[:conditions])) scope = scope.where(interpolate(preload_options[:conditions])) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f95e5337c2..be890e5767 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -37,9 +37,7 @@ module ActiveRecord # situation it is more natural for the user to just create or modify their join records # directly as required. def construct_join_attributes(*records) - if source_reflection.macro != :belongs_to - raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) - end + ensure_mutable join_attributes = { source_reflection.foreign_key => @@ -64,7 +62,7 @@ module ActiveRecord # properly support stale-checking for nested associations. def stale_state if through_reflection.macro == :belongs_to - owner[through_reflection.foreign_key].to_s + owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s end end @@ -73,6 +71,12 @@ module ActiveRecord !owner[through_reflection.foreign_key].nil? end + def ensure_mutable + if source_reflection.macro != :belongs_to + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + end + end + def ensure_not_nested if reflection.nested? raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 3e27e85f02..39ea885246 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -49,14 +49,6 @@ module ActiveRecord @attribute_methods_generated ||= false end - # We will define the methods as instance methods, but will call them as singleton - # methods. This allows us to use method_defined? to check if the method exists, - # which is fast and won't give any false positives from the ancestors (because - # there are no ancestors). - def generated_external_attribute_methods - @generated_external_attribute_methods ||= Module.new { extend self } - end - def undefine_attribute_methods super if attribute_methods_generated? @attribute_methods_generated = false @@ -189,7 +181,7 @@ module ActiveRecord # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). def attribute_present?(attribute) value = read_attribute(attribute) - !value.nil? || (value.respond_to?(:empty?) && !value.empty?) + !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end # Returns the column object for the named attribute. @@ -214,37 +206,64 @@ module ActiveRecord value end - # Returns a copy of the attributes hash where all the values have been safely quoted for use in - # an Arel insert/update method. - def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) - attrs = {} - klass = self.class - arel_table = klass.arel_table + def arel_attributes_with_values_for_create(pk_attribute_allowed) + arel_attributes_with_values(attributes_for_create(pk_attribute_allowed)) + end - attribute_names.each do |name| - if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) + def arel_attributes_with_values_for_update(attribute_names) + arel_attributes_with_values(attributes_for_update(attribute_names)) + end + + def attribute_method?(attr_name) + defined?(@attributes) && @attributes.include?(attr_name) + end - if include_readonly_attributes || !self.class.readonly_attributes.include?(name) + private - value = if klass.serialized_attributes.include?(name) - @attributes[name].serialized_value - else - # FIXME: we need @attributes to be used consistently. - # If the values stored in @attributes were already type - # casted, this code could be simplified - read_attribute(name) - end + # Returns a Hash of the Arel::Attributes and attribute values that have been + # type casted for use in an Arel insert/update method. + def arel_attributes_with_values(attribute_names) + attrs = {} + arel_table = self.class.arel_table - attrs[arel_table[name]] = value - end - end + attribute_names.each do |name| + attrs[arel_table[name]] = typecasted_attribute_value(name) end - attrs end - def attribute_method?(attr_name) - defined?(@attributes) && @attributes.include?(attr_name) + # Filters the primary keys and readonly attributes from the attribute names. + def attributes_for_update(attribute_names) + attribute_names.select do |name| + column_for_attribute(name) && !pk_attribute?(name) && !readonly_attribute?(name) + end + end + + # Filters out the primary keys, from the attribute names, when the primary + # key is to be generated (e.g. the id attribute has no value). + def attributes_for_create(pk_attribute_allowed) + @attributes.keys.select do |name| + column_for_attribute(name) && (pk_attribute_allowed || !pk_attribute?(name)) + end + end + + def readonly_attribute?(name) + self.class.readonly_attributes.include?(name) + end + + def pk_attribute?(name) + column_for_attribute(name).primary + end + + def typecasted_attribute_value(name) + if self.class.serialized_attributes.include?(name) + @attributes[name].serialized_value + else + # FIXME: we need @attributes to be used consistently. + # If the values stored in @attributes were already typecasted, this code + # could be simplified + read_attribute(name) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 3a737e5b35..11c63591e3 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -22,8 +22,6 @@ module ActiveRecord if status = super @previously_changed = changes @changed_attributes.clear - elsif IdentityMap.enabled? - IdentityMap.remove(self) end status end @@ -34,9 +32,6 @@ module ActiveRecord @previously_changed = changes @changed_attributes.clear end - rescue - IdentityMap.remove(self) if IdentityMap.enabled? - raise end # <tt>reload</tt> the record and clears changed attributes. diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 8e0b3e1402..7b7811a706 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -43,12 +43,6 @@ module ActiveRecord if attr_name == primary_key && attr_name != 'id' generated_attribute_methods.send(:alias_method, :id, primary_key) - generated_external_attribute_methods.module_eval <<-CODE, __FILE__, __LINE__ - def id(v, attributes, attributes_cache, attr_name) - attr_name = '#{primary_key}' - send(attr_name, attributes[attr_name], attributes, attributes_cache, attr_name) - end - CODE end end @@ -111,9 +105,8 @@ module ActiveRecord # end # Project.primary_key # => "foo_id" def primary_key=(value) - @original_primary_key = @primary_key if defined?(@primary_key) - @primary_key = value && value.to_s - @quoted_primary_key = nil + @primary_key = value && value.to_s + @quoted_primary_key = nil end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 846ac03d82..dcc3d79de9 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -29,35 +29,8 @@ module ActiveRecord cached_attributes.include?(attr_name) end - def undefine_attribute_methods - generated_external_attribute_methods.module_eval do - instance_methods.each { |m| undef_method(m) } - end - - super - end - - def type_cast_attribute(attr_name, attributes, cache = {}) #:nodoc: - return unless attr_name - attr_name = attr_name.to_s - - if generated_external_attribute_methods.method_defined?(attr_name) - if attributes.has_key?(attr_name) || attr_name == 'id' - generated_external_attribute_methods.send(attr_name, attributes[attr_name], attributes, cache, attr_name) - end - elsif !attribute_methods_generated? - # If we haven't generated the caster methods yet, do that and - # then try again - define_attribute_methods - type_cast_attribute(attr_name, attributes, cache) - else - # If we get here, the attribute has no associated DB column, so - # just return it verbatim. - attributes[attr_name] - end - end - protected + # We want to generate the methods via module_eval rather than define_method, # because define_method is slower on dispatch and uses more memory (because it # creates a closure). @@ -67,19 +40,9 @@ module ActiveRecord # we first define with the __temp__ identifier, and then use alias method to # rename it to what we want. def define_method_attribute(attr_name) - cast_code = attribute_cast_code(attr_name) - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def __temp__ - #{internal_attribute_access_code(attr_name, cast_code)} - end - alias_method '#{attr_name}', :__temp__ - undef_method :__temp__ - STR - - generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__(v, attributes, attributes_cache, attr_name) - #{external_attribute_access_code(attr_name, cast_code)} + read_attribute('#{attr_name}') { |n| missing_attribute(n, caller) } end alias_method '#{attr_name}', :__temp__ undef_method :__temp__ @@ -87,6 +50,7 @@ module ActiveRecord end private + def cacheable_column?(column) if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT ! serialized_attributes.include? column.name @@ -94,33 +58,19 @@ module ActiveRecord attribute_types_cached_by_default.include?(column.type) end end - - def internal_attribute_access_code(attr_name, cast_code) - "read_attribute('#{attr_name}') { |n| missing_attribute(n, caller) }" - end - - def external_attribute_access_code(attr_name, cast_code) - access_code = "v && #{cast_code}" - - if cache_attribute?(attr_name) - access_code = "attributes_cache[attr_name] ||= (#{access_code})" - end - - access_code - end - - def attribute_cast_code(attr_name) - columns_hash[attr_name].type_cast_code('v') - end end # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) # If it's cached, just return it - @attributes_cache.fetch(attr_name) { |name| + @attributes_cache.fetch(attr_name.to_s) { |name| column = @columns_hash.fetch(name) { - return self.class.type_cast_attribute(name, @attributes, @attributes_cache) + return @attributes.fetch(name) { + if name == 'id' && self.class.primary_key != name + read_attribute(self.class.primary_key) + end + } } value = @attributes.fetch(name) { diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 4bafadc666..d545e7799d 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -28,7 +28,7 @@ module ActiveRecord # Association with autosave option defines several callbacks on your # model (before_save, after_create, after_update). Please note that # callbacks are executed in the order they were defined in - # model. You should avoid modyfing the association content, before + # model. You should avoid modifying the association content, before # autosave callbacks are executed. Placing your callbacks after # associations is usually a good practice. # @@ -191,23 +191,21 @@ module ActiveRecord # Doesn't use after_save as that would save associations added in after_create/after_update twice after_create save_method after_update save_method + elsif reflection.macro == :has_one + define_method(save_method) { save_has_one_association(reflection) } + # Configures two callbacks instead of a single after_save so that + # the model may rely on their execution order relative to its + # own callbacks. + # + # For example, given that after_creates run before after_saves, if + # we configured instead an after_save there would be no way to fire + # a custom after_create callback after the child association gets + # created. + after_create save_method + after_update save_method else - if reflection.macro == :has_one - define_method(save_method) { save_has_one_association(reflection) } - # Configures two callbacks instead of a single after_save so that - # the model may rely on their execution order relative to its - # own callbacks. - # - # For example, given that after_creates run before after_saves, if - # we configured instead an after_save there would be no way to fire - # a custom after_create callback after the child association gets - # created. - after_create save_method - after_update save_method - else - define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) } - before_save save_method - end + define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) } + before_save save_method end end @@ -330,14 +328,14 @@ module ActiveRecord autosave = reflection.options[:autosave] if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) - begin + records_to_destroy = [] records.each do |record| next if record.destroyed? saved = true if autosave && record.marked_for_destruction? - association.proxy.destroy(record) + records_to_destroy << record elsif autosave != false && (@new_record_before_save || record.new_record?) if autosave saved = association.insert_record(record, false) @@ -350,11 +348,10 @@ module ActiveRecord raise ActiveRecord::Rollback unless saved end - rescue - records.each {|x| IdentityMap.remove(x) } if IdentityMap.enabled? - raise - end + records_to_destroy.each do |record| + association.destroy(record) + end end # reconstruct the scope now that we know the owner's id diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index d4d0220fb7..189985b671 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -201,6 +201,9 @@ module ActiveRecord #:nodoc: # # Now 'Bob' exist and is an 'admin' # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true } # + # Adding an exclamation point (!) on to the end of <tt>find_or_create_by_</tt> will + # raise an <tt>ActiveRecord::RecordInvalid</tt> error if the new record is invalid. + # # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without # saving it first. Protected attributes won't be set unless they are given in a block. # @@ -301,21 +304,22 @@ module ActiveRecord #:nodoc: # (or a bad spelling of an existing one). # * AssociationTypeMismatch - The object assigned to the association wasn't of the type # specified in the association definition. - # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. - # * ConnectionNotEstablished+ - No connection has been established. Use <tt>establish_connection</tt> + # * AttributeAssignmentError - An error occurred while doing a mass assignment through the + # <tt>attributes=</tt> method. + # You can inspect the +attribute+ property of the exception object to determine which attribute + # triggered the error. + # * ConnectionNotEstablished - No connection has been established. Use <tt>establish_connection</tt> # before querying. - # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist - # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal - # nothing was found, please check its documentation for further details. - # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * AttributeAssignmentError - An error occurred while doing a mass assignment through the - # <tt>attributes=</tt> method. - # You can inspect the +attribute+ property of the exception object to determine which attribute - # triggered the error. + # * RecordInvalid - raised by save! and create! when the record is invalid. + # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist + # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal + # nothing was found, please check its documentation for further details. + # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. + # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. # # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level). # So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 77af540c3e..66a0c83c41 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -15,7 +15,13 @@ module ActiveRecord end def dump(obj) - YAML.dump(obj) unless obj.nil? + return if obj.nil? + + unless obj.is_a?(object_class) + raise SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" + end + YAML.dump obj end def load(yaml) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 37a9d216df..c259e46073 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -2,6 +2,7 @@ require 'thread' require 'monitor' require 'set' require 'active_support/core_ext/module/deprecation' +require 'timeout' module ActiveRecord # Raised when a connection could not be obtained within the connection @@ -11,9 +12,6 @@ module ActiveRecord # Raised when a connection pool is full and another connection is requested class PoolFullError < ConnectionNotEstablished - def initialize size, timeout - super("Connection pool of size #{size} and timeout #{timeout}s is full") - end end module ConnectionAdapters @@ -57,12 +55,20 @@ module ActiveRecord # # == Options # - # There are two connection-pooling-related options that you can add to + # There are several connection-pooling-related options that you can add to # your database connection configuration: # # * +pool+: number indicating size of connection pool (default 5) - # * +wait_timeout+: number of seconds to block and wait for a connection + # * +checkout_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). + # * +reaping_frequency+: frequency in seconds to periodically run the + # Reaper, which attempts to find and close dead connections, which can + # occur if a programmer forgets to close a connection at the end of a + # thread or a thread dies unexpectedly. (Default nil, which means don't + # run the Reaper). + # * +dead_connection_timeout+: number of seconds from last checkout + # after which the Reaper will consider a connection reapable. (default + # 5 seconds). class ConnectionPool # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. # A reaper instantiated with a nil frequency will never reap the @@ -91,9 +97,24 @@ module ActiveRecord include MonitorMixin - attr_accessor :automatic_reconnect, :timeout + attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout attr_reader :spec, :connections, :size, :reaper + class Latch # :nodoc: + def initialize + @mutex = Mutex.new + @cond = ConditionVariable.new + end + + def release + @mutex.synchronize { @cond.broadcast } + end + + def await + @mutex.synchronize { @cond.wait @mutex } + end + end + # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, # host name, username, password, etc), as well as the maximum size for @@ -108,13 +129,15 @@ module ActiveRecord # The cache of reserved connections mapped to threads @reserved_connections = {} - @timeout = spec.config[:wait_timeout] || 5 + @checkout_timeout = spec.config[:checkout_timeout] || 5 + @dead_connection_timeout = spec.config[:dead_connection_timeout] @reaper = Reaper.new self, spec.config[:reaping_frequency] @reaper.run # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 + @latch = Latch.new @connections = [] @automatic_reconnect = true end @@ -125,21 +148,28 @@ module ActiveRecord # #connection can be called any number of times; the connection is # held in a hash keyed by the thread id. def connection - @reserved_connections[current_connection_id] ||= checkout + synchronize do + @reserved_connections[current_connection_id] ||= checkout + end end - # Check to see if there is an active connection in this connection - # pool. + # Is there an open connection that is being used for the current thread? def active_connection? - active_connections.any? + synchronize do + @reserved_connections.fetch(current_connection_id) { + return false + }.in_use? + end end # Signal that the thread is finished with the current connection. # #release_connection releases the connection-thread association # and returns the connection to the pool. def release_connection(with_id = current_connection_id) - conn = @reserved_connections.delete(with_id) - checkin conn if conn + synchronize do + conn = @reserved_connections.delete(with_id) + checkin conn if conn + end end # If a connection already exists yield it to the block. If no connection @@ -185,8 +215,9 @@ module ActiveRecord end def clear_stale_cached_connections! # :nodoc: + reap end - deprecate :clear_stale_cached_connections! + deprecate :clear_stale_cached_connections! => "Please use #reap instead" # Check-out a database connection from the pool, indicating that you want # to use it. You should call #checkin when you no longer need this. @@ -203,23 +234,23 @@ module ActiveRecord # Raises: # - PoolFullError: no connection can be obtained from the pool. def checkout - # Checkout an available connection - synchronize do - # Try to find a connection that hasn't been leased, and lease it - conn = connections.find { |c| c.lease } - - # If all connections were leased, and we have room to expand, - # create a new connection and lease it. - if !conn && connections.size < size - conn = checkout_new_connection - conn.lease - end + loop do + # Checkout an available connection + synchronize do + # Try to find a connection that hasn't been leased, and lease it + conn = connections.find { |c| c.lease } + + # If all connections were leased, and we have room to expand, + # create a new connection and lease it. + if !conn && connections.size < size + conn = checkout_new_connection + conn.lease + end - if conn - checkout_and_verify conn - else - raise PoolFullError.new(size, timeout) + return checkout_and_verify(conn) if conn end + + Timeout.timeout(@checkout_timeout, PoolFullError) { @latch.await } end end @@ -233,7 +264,10 @@ module ActiveRecord conn.run_callbacks :checkin do conn.expire end + + release conn end + @latch.release end # Remove a connection from the connection pool. The connection will @@ -244,11 +278,9 @@ module ActiveRecord # FIXME: we might want to store the key on the connection so that removing # from the reserved hash will be a little easier. - thread_id = @reserved_connections.keys.find { |k| - @reserved_connections[k] == conn - } - @reserved_connections.delete thread_id if thread_id + release conn end + @latch.release end # Removes dead connections from the pool. A dead connection can occur @@ -256,15 +288,28 @@ module ActiveRecord # or a thread dies unexpectedly. def reap synchronize do - stale = Time.now - @timeout + stale = Time.now - @dead_connection_timeout connections.dup.each do |conn| remove conn if conn.in_use? && stale > conn.last_use && !conn.active? end end + @latch.release end private + def release(conn) + thread_id = if @reserved_connections[current_connection_id] == conn + current_connection_id + else + @reserved_connections.keys.find { |k| + @reserved_connections[k] == conn + } + end + + @reserved_connections.delete thread_id if thread_id + end + def new_connection ActiveRecord::Base.send(spec.adapter_method, spec.config) end @@ -288,10 +333,6 @@ module ActiveRecord end c end - - def active_connections - @connections.find_all { |c| c.in_use? } - end end # ConnectionHandler is a collection of ConnectionPool objects. It is used 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 174450eb00..7b2961a04a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -57,21 +57,21 @@ module ActiveRecord end # Executes insert +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_insert(sql, name, binds) + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) exec_query(sql, name, binds) end # Executes delete +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_delete(sql, name, binds) exec_query(sql, name, binds) end # Executes update +sql+ statement in the context of this connection using - # +binds+ as the bind substitutes. +name+ is the logged along with + # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_update(sql, name, binds) exec_query(sql, name, binds) @@ -87,7 +87,7 @@ module ActiveRecord # passed in as +id_value+. def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) - value = exec_insert(sql, name, binds) + value = exec_insert(sql, name, binds, pk, sequence_name) id_value || last_inserted_id(value) end @@ -312,13 +312,27 @@ module ActiveRecord # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in # an UPDATE statement, so in the mysql adapters we redefine this to do that. def join_to_update(update, select) #:nodoc: - subselect = select.clone - subselect.projections = [update.key] + key = update.key + subselect = subquery_for(key, select) - update.where update.key.in(subselect) + update.where key.in(subselect) + end + + def join_to_delete(delete, select, key) #:nodoc: + subselect = subquery_for(key, select) + + delete.where key.in(subselect) end protected + + # Return a subquery for the given key using the join information. + def subquery_for(key, select) + subselect = select.clone + subselect.projections = [key] + subselect + end + # Returns an array of record hashes with the column names as keys and # column values as values. def select(sql, name = nil, binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 44ac37c498..6f9f0399db 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -34,6 +34,7 @@ module ActiveRecord when Numeric then value.to_s when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" + when Class then "'#{value.to_s}'" else "'#{quote_string(YAML.dump(value))}'" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index ad2e8634eb..df78ba6c5a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,6 +6,9 @@ require 'bigdecimal/util' module ActiveRecord module ConnectionAdapters #:nodoc: + # Abstract representation of an index definition on a table. Instances of + # this type are typically created and returned by methods in database + # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where) #:nodoc: end @@ -20,7 +23,7 @@ module ActiveRecord end def sql_type - base.type_to_sql(type.to_sym, limit, precision, scale) rescue type + base.type_to_sql(type.to_sym, limit, precision, scale) end def to_sql @@ -62,11 +65,12 @@ module ActiveRecord class TableDefinition # An array of ColumnDefinition objects, representing the column changes # that have been defined. - attr_accessor :columns + attr_accessor :columns, :indexes def initialize(base) @columns = [] @columns_hash = {} + @indexes = {} @base = base end @@ -209,19 +213,22 @@ module ActiveRecord # # TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type # column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of - # options, these will be used when creating the <tt>_type</tt> column. So what can be written like this: + # options, these will be used when creating the <tt>_type</tt> column. The <tt>:index</tt> option + # will also create an index, similar to calling <tt>add_index</tt>. So what can be written like this: # # create_table :taggings do |t| # t.integer :tag_id, :tagger_id, :taggable_id # t.string :tagger_type # t.string :taggable_type, :default => 'Photo' # end + # add_index :taggings, :tag_id, :name => 'index_taggings_on_tag_id' + # add_index :taggings, [:tagger_id, :tagger_type] # # Can also be written as follows using references: # # create_table :taggings do |t| - # t.references :tag - # t.references :tagger, :polymorphic => true + # t.references :tag, :index => { :name => 'index_taggings_on_tag_id' } + # t.references :tagger, :polymorphic => true, :index => true # t.references :taggable, :polymorphic => { :default => 'Photo' } # end def column(name, type, options = {}) @@ -252,6 +259,14 @@ module ActiveRecord end # end EOV end + + # Adds index options to the indexes hash, keyed by column name + # This is primarily used to track indexes that need to be created after the table + # + # index(:account_id, :name => 'index_projects_on_account_id') + def index(column_name, options = {}) + indexes[column_name] = options + end # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and # <tt>:updated_at</tt> to the table. @@ -264,9 +279,11 @@ module ActiveRecord def references(*args) options = args.extract_options! polymorphic = options.delete(:polymorphic) + index_options = options.delete(:index) args.each do |col| column("#{col}_id", :integer, options) column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil? + index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options end end alias :belongs_to :references @@ -331,7 +348,7 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. - # ===== Example + # # ====== Creating a simple column # t.column(:name, :string) def column(column_name, type, options = {}) @@ -346,7 +363,6 @@ module ActiveRecord # Adds a new index to the table. +column_name+ can be a single Symbol, or # an Array of Symbols. See SchemaStatements#add_index # - # ===== Examples # ====== Creating a simple index # t.index(:name) # ====== Creating a unique index @@ -363,7 +379,7 @@ module ActiveRecord end # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps - # ===== Example + # # t.timestamps def timestamps @base.add_timestamps(@table_name) @@ -371,7 +387,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # t.change(:name, :string, :limit => 80) # t.change(:description, :text) def change(column_name, type, options = {}) @@ -379,7 +395,7 @@ module ActiveRecord end # Sets a new default value for a column. See SchemaStatements#change_column_default - # ===== Examples + # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) def change_default(column_name, default) @@ -387,16 +403,15 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # t.remove(:qualification) # t.remove(:qualification, :experience) def remove(*column_names) - @base.remove_column(@table_name, column_names) + @base.remove_column(@table_name, *column_names) end # Removes the given index from the table. # - # ===== Examples # ====== Remove the index_table_name_on_column in the table_name table # t.remove_index :column # ====== Remove the index named index_table_name_on_branch_id in the table_name table @@ -410,14 +425,14 @@ module ActiveRecord end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. - # ===== Example + # # t.remove_timestamps def remove_timestamps @base.remove_timestamps(@table_name) end # Renames a column. - # ===== Example + # # t.rename(:description, :name) def rename(column_name, new_column_name) @base.rename_column(@table_name, column_name, new_column_name) @@ -425,23 +440,25 @@ module ActiveRecord # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. - # ===== Examples + # # t.references(:goat) # t.references(:goat, :polymorphic => true) # t.belongs_to(:goat) def references(*args) options = args.extract_options! polymorphic = options.delete(:polymorphic) + index_options = options.delete(:index) args.each do |col| @base.add_column(@table_name, "#{col}_id", :integer, options) @base.add_column(@table_name, "#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil? + @base.add_index(@table_name, polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options end end alias :belongs_to :references # Removes a reference. Optionally removes a +type+ column. # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. - # ===== Examples + # # t.remove_references(:goat) # t.remove_references(:goat, :polymorphic => true) # t.remove_belongs_to(:goat) @@ -456,7 +473,7 @@ module ActiveRecord alias :remove_belongs_to :remove_references # Adds a column or columns of a specified type - # ===== Examples + # # t.string(:goat) # t.string(:goat, :sheep) %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 2c210e5ba2..62b0f51bb2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -16,12 +16,11 @@ module ActiveRecord # Truncates a table alias according to the limits of the current adapter. def table_alias_for(table_name) - table_name[0...table_alias_length].gsub(/\./, '_') + table_name[0...table_alias_length].tr('.', '_') end # Checks to see if the table +table_name+ exists on the database. # - # === Example # table_exists?(:developers) def table_exists?(table_name) tables.include?(table_name.to_s) @@ -32,7 +31,6 @@ module ActiveRecord # Checks to see if an index exists on a table for a given index definition. # - # === Examples # # Check an index exists # index_exists?(:suppliers, :company_id) # @@ -126,7 +124,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -171,10 +168,11 @@ module ActiveRecord create_sql << td.to_sql create_sql << ") #{options[:options]}" execute create_sql + td.indexes.each_pair { |c,o| add_index table_name, c, o } end # Creates a new join table with the name created using the lexical order of the first two - # arguments. These arguments can be be a String or a Symbol. + # arguments. These arguments can be a String or a Symbol. # # # Creates a table called 'assemblies_parts' with no id. # create_join_table(:assemblies, :parts) @@ -192,7 +190,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -214,7 +211,6 @@ module ActiveRecord # A block for changing columns in +table+. # - # === Example # # change_table() yields a Table instance # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -228,7 +224,6 @@ module ActiveRecord # # Defaults to false. # - # ===== Examples # ====== Add a column # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -287,7 +282,7 @@ module ActiveRecord end # Renames a table. - # ===== Example + # # rename_table('octopuses', 'octopi') def rename_table(table_name, new_name) raise NotImplementedError, "rename_table is not implemented" @@ -307,7 +302,7 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # remove_column(:suppliers, :qualification) # remove_columns(:suppliers, :qualification, :experience) def remove_column(table_name, *column_names) @@ -317,7 +312,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # change_column(:suppliers, :name, :string, :limit => 80) # change_column(:accounts, :description, :text) def change_column(table_name, column_name, type, options = {}) @@ -325,7 +320,7 @@ module ActiveRecord end # Sets a new default value for a column. - # ===== Examples + # # change_column_default(:suppliers, :qualification, 'new') # change_column_default(:accounts, :authorized, 1) # change_column_default(:users, :email, nil) @@ -334,7 +329,7 @@ module ActiveRecord end # Renames a column. - # ===== Example + # # rename_column(:suppliers, :description, :name) def rename_column(table_name, column_name, new_column_name) raise NotImplementedError, "rename_column is not implemented" @@ -346,8 +341,6 @@ module ActiveRecord # The index will be named after the table and the column name(s), unless # you pass <tt>:name</tt> as an option. # - # ===== Examples - # # ====== Creating a simple index # add_index(:suppliers, :name) # generates @@ -375,7 +368,7 @@ module ActiveRecord # Note: SQLite doesn't support index length # # ====== Creating an index with a sort order (desc or asc, asc is the default) - # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :part_id => :asc}) + # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :party_id => :asc}) # generates # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # @@ -536,7 +529,7 @@ module ActiveRecord end # Adds timestamps (created_at and updated_at) columns to the named table. - # ===== Examples + # # add_timestamps(:suppliers) def add_timestamps(table_name) add_column table_name, :created_at, :datetime @@ -544,7 +537,7 @@ module ActiveRecord end # Removes the timestamp columns (created_at and updated_at) from the table definition. - # ===== Examples + # # remove_timestamps(:suppliers) def remove_timestamps(table_name) remove_column table_name, :updated_at @@ -617,8 +610,6 @@ module ActiveRecord end def columns_for_remove(table_name, *column_names) - column_names = column_names.flatten - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank? column_names.map {|column_name| quote_column_name(column_name) } end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index c6af682d28..c6faae77cc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -57,7 +57,7 @@ module ActiveRecord define_callbacks :checkout, :checkin attr_accessor :visitor, :pool - attr_reader :schema_cache, :last_use, :in_use + attr_reader :schema_cache, :last_use, :in_use, :logger alias :in_use? :in_use def initialize(connection, logger = nil, pool = nil) #:nodoc: @@ -86,6 +86,11 @@ module ActiveRecord end end + def schema_cache=(cache) + cache.connection = self + @schema_cache = cache + end + def expire @in_use = false end @@ -158,11 +163,6 @@ module ActiveRecord # QUOTING ================================================== - # Override to return the quoted table name. Defaults to column quoting. - def quote_table_name(name) - quote_column_name(name) - end - # Returns a bind substitution value given a +column+ and list of current # +binds+ def substitute_at(column, index) @@ -279,26 +279,25 @@ module ActiveRecord protected - def log(sql, name = "SQL", binds = []) - @instrumenter.instrument( - "sql.active_record", - :sql => sql, - :name => name, - :connection_id => object_id, - :binds => binds) { yield } - rescue Exception => e - message = "#{e.class.name}: #{e.message}: #{sql}" - @logger.debug message if @logger - exception = translate_exception(e, message) - exception.set_backtrace e.backtrace - raise exception - end - - def translate_exception(e, message) - # override in derived class - ActiveRecord::StatementInvalid.new(message) - end - + def log(sql, name = "SQL", binds = []) + @instrumenter.instrument( + "sql.active_record", + :sql => sql, + :name => name, + :connection_id => object_id, + :binds => binds) { yield } + rescue Exception => e + message = "#{e.class.name}: #{e.message}: #{sql}" + @logger.error message if @logger + exception = translate_exception(e, message) + exception.set_backtrace e.backtrace + raise exception + end + + def translate_exception(exception, message) + # override in derived class + ActiveRecord::StatementInvalid.new(message) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index e33903622b..9794c5663e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -251,7 +251,7 @@ module ActiveRecord end # MysqlAdapter has to free a result after using it, so we use this method to write - # stuff in a abstract way without concerning ourselves about whether it needs to be + # stuff in an abstract way without concerning ourselves about whether it needs to be # explicitly freed or not. def execute_and_free(sql, name = nil) #:nodoc: yield execute(sql, name) @@ -294,19 +294,10 @@ module ActiveRecord # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support - # these, we must use a subquery. However, MySQL is too stupid to create a - # temporary table for this automatically, so we have to give it some prompting - # in the form of a subsubquery. Ugh! + # these, we must use a subquery. def join_to_update(update, select) #:nodoc: if select.limit || select.offset || select.orders.any? - subsubselect = select.clone - subsubselect.projections = [update.key] - - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(update.key.name) - subselect.from subsubselect.as('__active_record_temp') - - update.where update.key.in(subselect) + super else update.table select.source update.wheres = select.constraints @@ -375,7 +366,7 @@ module ActiveRecord def tables(name = nil, database = nil, like = nil) #:nodoc: sql = "SHOW TABLES " - sql << "IN #{database} " if database + sql << "IN #{quote_table_name(database)} " if database sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| @@ -525,8 +516,8 @@ module ActiveRecord def pk_and_sequence_for(table) execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result| create_table = each_hash(result).first[:"Create Table"] - if create_table.to_s =~ /PRIMARY KEY\s+\((.+)\)/ - keys = $1.split(",").map { |key| key.gsub(/[`"]/, "") } + if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/ + keys = $1.split(",").map { |key| key.delete('`"') } keys.length == 1 ? [keys.first, nil] : nil else nil @@ -558,6 +549,17 @@ module ActiveRecord protected + # MySQL is too stupid to create a temporary table for use subquery, so we have + # to give it some prompting in the form of a subsubquery. Ugh! + def subquery_for(key, select) + subsubselect = select.clone + subsubselect.projections = [key] + + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(key.name) + subselect.from subsubselect.as('__active_record_temp') + end + def add_index_length(option_strings, column_names, options = {}) if options.is_a?(Hash) && length = options[:length] case length diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 78e54c4c9b..01bd3ae26c 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -1,4 +1,5 @@ require 'set' +require 'active_support/deprecation' module ActiveRecord # :stopdoc: @@ -94,7 +95,7 @@ module ActiveRecord case type when :string, :text then value - when :integer then value.to_i rescue value ? 1 : 0 + when :integer then value.to_i when :float then value.to_f when :decimal then klass.value_to_decimal(value) when :datetime, :timestamp then klass.string_to_time(value) @@ -107,6 +108,9 @@ module ActiveRecord end def type_cast_code(var_name) + ActiveSupport::Deprecation.warn("Column#type_cast_code is deprecated in favor of" \ + "using Column#type_cast only, and it is going to be removed in future Rails versions.") + klass = self.class.name case type @@ -120,6 +124,7 @@ module ActiveRecord when :binary then "#{klass}.binary_to_string(#{var_name})" when :boolean then "#{klass}.value_to_boolean(#{var_name})" when :hstore then "#{klass}.string_to_hstore(#{var_name})" + when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})" else var_name end end @@ -154,7 +159,7 @@ module ActiveRecord def value_to_date(value) if value.is_a?(String) - return nil if value.empty? + return nil if value.blank? fast_string_to_date(value) || fallback_string_to_date(value) elsif value.respond_to?(:to_date) value.to_date @@ -165,14 +170,14 @@ module ActiveRecord def string_to_time(string) return string unless string.is_a?(String) - return nil if string.empty? + return nil if string.blank? fast_string_to_time(string) || fallback_string_to_time(string) end def string_to_dummy_time(string) return string unless string.is_a?(String) - return nil if string.empty? + return nil if string.blank? string_to_time "2000-01-01 #{string}" end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index b343898854..8fc172f6e8 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -226,7 +226,7 @@ module ActiveRecord end alias :create :insert_sql - def exec_insert(sql, name, binds) + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) execute to_sql(sql, binds), name end @@ -253,6 +253,14 @@ module ActiveRecord # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 variable_assignments = ['SQL_AUTO_IS_NULL=0'] + + # Make MySQL reject illegal values rather than truncating or + # blanking them. See + # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables + if @config.fetch(:strict, true) + variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'" + end + encoding = @config[:encoding] # make sure we set the encoding diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 724dbff1f0..0b6734b010 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -53,6 +53,7 @@ module ActiveRecord # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). + # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html) # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. @@ -404,6 +405,13 @@ module ActiveRecord # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) + + # Make MySQL reject illegal values rather than truncating or + # blanking them. See + # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables + if @config.fetch(:strict, true) + execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging) + end end def select(sql, name = nil, binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index c82afc232c..df3d5e4657 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -137,6 +137,14 @@ module ActiveRecord end end + class Cidr < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_cidr value + end + end + class TypeMap def initialize @mapping = {} @@ -212,11 +220,9 @@ module ActiveRecord # FIXME: why are we keeping these types as strings? alias_type 'tsvector', 'text' alias_type 'interval', 'text' - alias_type 'cidr', 'text' - alias_type 'inet', 'text' - alias_type 'macaddr', 'text' alias_type 'bit', 'text' alias_type 'varbit', 'text' + alias_type 'macaddr', 'text' # FIXME: I don't think this is correct. We should probably be returning a parsed date, # but the tests pass with a string returned. @@ -237,6 +243,9 @@ module ActiveRecord register_type 'polygon', OID::Identity.new register_type 'circle', OID::Identity.new register_type 'hstore', OID::Hstore.new + + register_type 'cidr', OID::Cidr.new + alias_type 'inet', 'cidr' end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index d2126a3e19..d62cf529a4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -8,6 +8,8 @@ require 'arel/visitors/bind_visitor' gem 'pg', '~> 0.11' require 'pg' +require 'ipaddr' + module ActiveRecord module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects @@ -16,8 +18,8 @@ module ActiveRecord # Forward any unused config params to PGconn.connect. [:statement_limit, :encoding, :min_messages, :schema_search_path, - :schema_order, :adapter, :pool, :wait_timeout, :template, - :reaping_frequency].each do |key| + :schema_order, :adapter, :pool, :checkout_timeout, :template, + :reaping_frequency, :insert_returning].each do |key| conn_params.delete key end conn_params.delete_if { |k,v| v.nil? } @@ -79,6 +81,25 @@ module ActiveRecord end end + def string_to_cidr(string) + if string.nil? + nil + elsif String === string + IPAddr.new(string) + else + string + end + + end + + def cidr_to_string(object) + if IPAddr === object + "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + else + object + end + end + private HstorePair = begin quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ @@ -88,9 +109,8 @@ module ActiveRecord def escape_hstore(value) value.nil? ? 'NULL' - : value =~ /[=\s,>]/ ? '"%s"' % value.gsub(/(["\\])/, '\\\\\1') : value == "" ? '""' - : value.to_s.gsub(/(["\\])/, '\\\\\1') + : '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') end end # :startdoc: @@ -198,6 +218,13 @@ module ActiveRecord :decimal when 'hstore' :hstore + # Network address types + when 'inet' + :inet + when 'cidr' + :cidr + when 'macaddr' + :macaddr # Character types when /^(?:character varying|bpchar)(?:\(\d+\))?$/ :string @@ -212,9 +239,6 @@ module ActiveRecord # Geometric types when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ :string - # Network address types - when /^(?:cidr|inet|macaddr)$/ - :string # Bit strings when /^bit(?: varying)?(?:\(\d+\))?$/ :string @@ -259,6 +283,8 @@ module ActiveRecord # <encoding></tt> call on the connection. # * <tt>:min_messages</tt> - An optional client min messages that is used in a # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. + # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT<tt> statements + # defaults to true. # # Any further options are used as connection parameters to libpq. See # http://www.postgresql.org/docs/9.1/static/libpq-connect.html for the @@ -281,6 +307,18 @@ module ActiveRecord def hstore(name, options = {}) column(name, 'hstore', options) end + + def inet(name, options = {}) + column(name, 'inet', options) + end + + def cidr(name, options = {}) + column(name, 'cidr', options) + end + + def macaddr(name, options = {}) + column(name, 'macaddr', options) + end end ADAPTER_NAME = 'PostgreSQL' @@ -300,7 +338,10 @@ module ActiveRecord :boolean => { :name => "boolean" }, :xml => { :name => "xml" }, :tsvector => { :name => "tsvector" }, - :hstore => { :name => "hstore" } + :hstore => { :name => "hstore" }, + :inet => { :name => "inet" }, + :cidr => { :name => "cidr" }, + :macaddr => { :name => "macaddr" } } # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -406,6 +447,7 @@ module ActiveRecord initialize_type_map @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] + @use_insert_returning = @config.key?(:insert_returning) ? @config[:insert_returning] : true end # Clears the prepared statements cache. @@ -508,9 +550,19 @@ module ActiveRecord when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) else super end + when IPAddr + case column.sql_type + when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) + else super + end when Float - return super unless value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" + if value.infinite? && column.type == :datetime + "'#{value.to_s.downcase}'" + elsif value.infinite? || value.nan? + "'#{value.to_s}'" + else + super + end when Numeric return super unless column.sql_type == 'money' # Not truly string input, so doesn't require (or allow) escape string syntax. @@ -542,6 +594,9 @@ module ActiveRecord when Hash return super unless 'hstore' == column.sql_type PostgreSQLColumn.hstore_to_string(value) + when IPAddr + return super unless ['inet','cidr'].includes? column.sql_type + PostgreSQLColumn.cidr_to_string(value) else super end @@ -667,8 +722,11 @@ module ActiveRecord pk = primary_key(table_ref) if table_ref end - if pk + if pk && use_insert_returning? select_value("#{sql} RETURNING #{quote_column_name(pk)}") + elsif pk + super + last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) else super end @@ -783,11 +841,27 @@ module ActiveRecord pk = primary_key(table_ref) if table_ref end - sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk + if pk && use_insert_returning? + sql = "#{sql} RETURNING #{quote_column_name(pk)}" + end [sql, binds] end + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + val = exec_query(sql, name, binds) + if !use_insert_returning? && pk + unless sequence_name + table_ref = extract_table_ref_from_insert_sql(sql) + sequence_name = default_sequence_name(table_ref, pk) + return val unless sequence_name + end + last_insert_id_result(sequence_name) + else + val + end + end + # Executes an UPDATE query and returns the number of affected tuples. def update_sql(sql, name = nil) super.cmd_tuples @@ -978,6 +1052,27 @@ module ActiveRecord end_sql end + # Returns an array of schema names. + def schema_names + query(<<-SQL).flatten + SELECT nspname + FROM pg_namespace + WHERE nspname !~ '^pg_.*' + AND nspname NOT IN ('information_schema') + ORDER by nspname; + SQL + end + + # Creates a schema for the given schema name. + def create_schema schema_name + execute "CREATE SCHEMA #{schema_name}" + end + + # Drops the schema for the given schema name. + def drop_schema schema_name + execute "DROP SCHEMA #{schema_name} CASCADE" + end + # Sets the schema search path to a string of comma-separated schema names. # Names beginning with $ have to be quoted (e.g. $user => '$user'). # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html @@ -985,7 +1080,7 @@ module ActiveRecord # This should be not be called manually but set in database.yml. def schema_search_path=(schema_csv) if schema_csv - execute "SET search_path TO #{schema_csv}" + execute("SET search_path TO #{schema_csv}", 'SCHEMA') @schema_search_path = schema_csv end end @@ -1007,7 +1102,9 @@ module ActiveRecord # Returns the sequence name for a table's primary key or some other specified key. def default_sequence_name(table_name, pk = nil) #:nodoc: - serial_sequence(table_name, pk || 'id').split('.').last + result = serial_sequence(table_name, pk || 'id') + return nil unless result + result.split('.').last rescue ActiveRecord::StatementInvalid "#{table_name}_#{pk || 'id'}_seq" end @@ -1167,14 +1264,25 @@ module ActiveRecord # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) - return super unless type.to_s == 'integer' - return 'integer' unless limit - - case limit - when 1, 2; 'smallint' - when 3, 4; 'integer' - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + case type.to_s + when 'binary' + # PostgreSQL doesn't support limits on binary (bytea) columns. + # The hard limit is 1Gb, because of a 32-bit size field, and TOAST. + case limit + when nil, 0..0x3fffffff; super(type) + else raise(ActiveRecordError, "No binary type has byte size #{limit}.") + end + when 'integer' + return 'integer' unless limit + + case limit + when 1, 2; 'smallint' + when 3, 4; 'integer' + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + end + else + super end end @@ -1189,7 +1297,10 @@ module ActiveRecord # Construct a clean list of column names from the ORDER BY clause, removing # any ASC/DESC modifiers - order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') } + order_columns = orders.collect do |s| + s = s.to_sql unless s.is_a?(String) + s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + end order_columns.delete_if { |c| c.blank? } order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } @@ -1215,17 +1326,25 @@ module ActiveRecord end end + def use_insert_returning? + @use_insert_returning + end + protected # Returns the version of the connected PostgreSQL server. def postgresql_version @connection.server_version end + # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html + FOREIGN_KEY_VIOLATION = "23503" + UNIQUE_VIOLATION = "23505" + def translate_exception(exception, message) - case exception.message - when /duplicate key value violates unique constraint/ + case exception.result.error_field(PGresult::PG_DIAG_SQLSTATE) + when UNIQUE_VIOLATION RecordNotUnique.new(message, exception) - when /violates foreign key constraint/ + when FOREIGN_KEY_VIOLATION InvalidForeignKey.new(message, exception) else super @@ -1344,8 +1463,15 @@ module ActiveRecord # Returns the current ID of a table's sequence. def last_insert_id(sequence_name) #:nodoc: - r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) - Integer(r.rows.first.first) + Integer(last_insert_id_value(sequence_name)) + end + + def last_insert_id_value(sequence_name) + last_insert_id_result(sequence_name).rows.first.first + end + + def last_insert_id_result(sequence_name) #:nodoc: + exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) end # Executes a SELECT query and returns the results, performing any data type diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 962718da56..aad1f9a7ef 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,26 +1,17 @@ module ActiveRecord module ConnectionAdapters class SchemaCache - attr_reader :columns, :columns_hash, :primary_keys, :tables - attr_reader :connection + attr_reader :columns, :columns_hash, :primary_keys, :tables, :version + attr_accessor :connection def initialize(conn) @connection = conn - @tables = {} - @columns = Hash.new do |h, table_name| - h[table_name] = conn.columns(table_name) - end - - @columns_hash = Hash.new do |h, table_name| - h[table_name] = Hash[columns[table_name].map { |col| - [col.name, col] - }] - end - - @primary_keys = Hash.new do |h, table_name| - h[table_name] = table_exists?(table_name) ? conn.primary_key(table_name) : nil - end + @columns = {} + @columns_hash = {} + @primary_keys = {} + @tables = {} + prepare_default_proc end # A cached lookup for table existence. @@ -30,12 +21,22 @@ module ActiveRecord @tables[name] = connection.table_exists?(name) end + # Add internal cache for table with +table_name+. + def add(table_name) + if table_exists?(table_name) + @primary_keys[table_name] + @columns[table_name] + @columns_hash[table_name] + end + end + # Clears out internal caches def clear! @columns.clear @columns_hash.clear @primary_keys.clear @tables.clear + @version = nil end # Clear out internal caches for table with +table_name+. @@ -45,6 +46,37 @@ module ActiveRecord @primary_keys.delete table_name @tables.delete table_name end + + def marshal_dump + # if we get current version during initialization, it happens stack over flow. + @version = ActiveRecord::Migrator.current_version + [@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val| + self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h } + end + end + + def marshal_load(array) + @version, @columns, @columns_hash, @primary_keys, @tables = array + prepare_default_proc + end + + private + + def prepare_default_proc + @columns.default_proc = Proc.new do |h, table_name| + h[table_name] = connection.columns(table_name) + end + + @columns_hash.default_proc = Proc.new do |h, table_name| + h[table_name] = Hash[columns[table_name].map { |col| + [col.name, col] + }] + end + + @primary_keys.default_proc = Proc.new do |h, table_name| + h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index ee5d10859c..d4ffa82b17 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,6 +1,8 @@ -require 'active_record/connection_adapters/sqlite_adapter' +require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/statement_pool' +require 'arel/visitors/bind_visitor' -gem 'sqlite3', '~> 1.3.5' +gem 'sqlite3', '~> 1.3.6' require 'sqlite3' module ActiveRecord @@ -19,10 +21,6 @@ module ActiveRecord config[:database] = File.expand_path(config[:database], Rails.root) end - unless 'sqlite3' == config[:adapter] - raise ArgumentError, 'adapter name should be "sqlite3"' - end - db = SQLite3::Database.new( config[:database], :results_as_hash => true @@ -35,7 +33,184 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Adapter < SQLiteAdapter # :nodoc: + class SQLite3Column < Column #:nodoc: + class << self + def binary_to_string(value) + if value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) + end + value + end + end + end + + # The SQLite3 adapter works SQLite 3.6.16 or newer + # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). + # + # Options: + # + # * <tt>:database</tt> - Path to the database file. + class SQLite3Adapter < AbstractAdapter + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split('.').map { |v| v.to_i } + end + + def <=>(version_string) + @version <=> version_string.split('.').map { |v| v.to_i } + end + end + + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max) + super + @cache = Hash.new { |h,pid| h[pid] = {} } + end + + def each(&block); cache.each(&block); end + def key?(key); cache.key?(key); end + def [](key); cache[key]; end + def length; cache.length; end + + def []=(sql, key) + while @max <= cache.size + dealloc(cache.shift.last[:stmt]) + end + cache[sql] = key + end + + def clear + cache.values.each do |hash| + dealloc hash[:stmt] + end + cache.clear + end + + private + def cache + @cache[$$] + end + + def dealloc(stmt) + stmt.close unless stmt.closed? + end + end + + class BindSubstitution < Arel::Visitors::SQLite # :nodoc: + include Arel::Visitors::BindVisitor + end + + def initialize(connection, logger, config) + super(connection, logger) + @statements = StatementPool.new(@connection, + config.fetch(:statement_limit) { 1000 }) + @config = config + + if config.fetch(:prepared_statements) { true } + @visitor = Arel::Visitors::SQLite.new self + else + @visitor = BindSubstitution.new self + end + end + + def adapter_name #:nodoc: + 'SQLite' + end + + # Returns true + def supports_ddl_transactions? + true + end + + # Returns true if SQLite version is '3.6.8' or greater, false otherwise. + def supports_savepoints? + sqlite_version >= '3.6.8' + end + + # Returns true, since this connection adapter supports prepared statement + # caching. + def supports_statement_cache? + true + end + + # Returns true, since this connection adapter supports migrations. + def supports_migrations? #:nodoc: + true + end + + # Returns true. + def supports_primary_key? #:nodoc: + true + end + + def requires_reloading? + true + end + + # Returns true + def supports_add_column? + true + end + + # Disconnects from the database if already connected. Otherwise, this + # method does nothing. + def disconnect! + super + clear_cache! + @connection.close rescue nil + end + + # Clears the prepared statements cache. + def clear_cache! + @statements.clear + end + + # Returns true + def supports_count_distinct? #:nodoc: + true + end + + # Returns true + def supports_autoincrement? #:nodoc: + true + end + + def supports_index_sort_order? + true + end + + def native_database_types #:nodoc: + { + :primary_key => default_primary_key_type, + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "integer" }, + :float => { :name => "float" }, + :decimal => { :name => "decimal" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "datetime" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "boolean" } + } + end + + # Returns the current database encoding format as a string, eg: 'UTF-8' + def encoding + @connection.encoding.to_s + end + + # Returns true. + def supports_explain? + true + end + + + # QUOTING ================================================== + def quote(value, column = nil) if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) s = column.class.string_to_binary(value).unpack("H*")[0] @@ -45,11 +220,392 @@ module ActiveRecord end end - # Returns the current database encoding format as a string, eg: 'UTF-8' - def encoding - @connection.encoding.to_s + + def quote_string(s) #:nodoc: + @connection.class.quote(s) + end + + def quote_column_name(name) #:nodoc: + %Q("#{name.to_s.gsub('"', '""')}") + end + + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. + def quoted_date(value) #:nodoc: + if value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + + def type_cast(value, column) # :nodoc: + return value.to_f if BigDecimal === value + return super unless String === value + return super unless column && value + + value = super + if column.type == :string && value.encoding == Encoding::ASCII_8BIT + logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger + value.encode! 'utf-8' + end + value end + # DATABASE STATEMENTS ====================================== + + def explain(arel, binds = []) + sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + end + + class ExplainPrettyPrinter + # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles + # the output of the SQLite shell: + # + # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) + # 0|1|1|SCAN TABLE posts (~100000 rows) + # + def pp(result) # :nodoc: + result.rows.map do |row| + row.join('|') + end.join("\n") + "\n" + end + end + + def exec_query(sql, name = nil, binds = []) + log(sql, name, binds) do + + # Don't cache statements without bind values + if binds.empty? + stmt = @connection.prepare(sql) + cols = stmt.columns + records = stmt.to_a + stmt.close + stmt = records + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + cols = cache[:cols] ||= stmt.columns + stmt.reset! + stmt.bind_params binds.map { |col, val| + type_cast(val, col) + } + end + + ActiveRecord::Result.new(cols, stmt.to_a) + end + end + + def exec_delete(sql, name = 'SQL', binds = []) + exec_query(sql, name, binds) + @connection.changes + end + alias :exec_update :exec_delete + + def last_inserted_id(result) + @connection.last_insert_row_id + end + + def execute(sql, name = nil) #:nodoc: + log(sql, name) { @connection.execute(sql) } + end + + def update_sql(sql, name = nil) #:nodoc: + super + @connection.changes + end + + def delete_sql(sql, name = nil) #:nodoc: + sql += " WHERE 1=1" unless sql =~ /WHERE/i + super sql, name + end + + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + super + id_value || @connection.last_insert_row_id + end + alias :create :insert_sql + + def select_rows(sql, name = nil) + exec_query(sql, name).rows + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + + def begin_db_transaction #:nodoc: + log('begin transaction',nil) { @connection.transaction } + end + + def commit_db_transaction #:nodoc: + log('commit transaction',nil) { @connection.commit } + end + + def rollback_db_transaction #:nodoc: + log('rollback transaction',nil) { @connection.rollback } + end + + # SCHEMA STATEMENTS ======================================== + + def tables(name = 'SCHEMA', table_name = nil) #:nodoc: + sql = <<-SQL + SELECT name + FROM sqlite_master + WHERE type = 'table' AND NOT name = 'sqlite_sequence' + SQL + sql << " AND name = #{quote_table_name(table_name)}" if table_name + + exec_query(sql, name).map do |row| + row['name'] + end + end + + def table_exists?(name) + name && tables('SCHEMA', name).any? + end + + # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. + def columns(table_name) #:nodoc: + table_structure(table_name).map do |field| + case field["dflt_value"] + when /^null$/i + field["dflt_value"] = nil + when /^'(.*)'$/ + field["dflt_value"] = $1.gsub("''", "'") + when /^"(.*)"$/ + field["dflt_value"] = $1.gsub('""', '"') + end + + SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) + end + end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) #:nodoc: + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| + IndexDefinition.new( + table_name, + row['name'], + row['unique'] != 0, + exec_query("PRAGMA index_info('#{row['name']}')").map { |col| + col['name'] + }) + end + end + + def primary_key(table_name) #:nodoc: + column = table_structure(table_name).find { |field| + field['pk'] == 1 + } + column && column['name'] + end + + def remove_index!(table_name, index_name) #:nodoc: + exec_query "DROP INDEX #{quote_column_name(index_name)}" + end + + # Renames a table. + # + # Example: + # rename_table('octopuses', 'octopi') + def rename_table(name, new_name) + exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + end + + # See: http://www.sqlite.org/lang_altertable.html + # SQLite has an additional restriction on the ALTER TABLE statement + def valid_alter_table_options( type, options) + type.to_sym != :primary_key + end + + def add_column(table_name, column_name, type, options = {}) #:nodoc: + if supports_add_column? && valid_alter_table_options( type, options ) + super(table_name, column_name, type, options) + else + alter_table(table_name) do |definition| + definition.column(column_name, type, options) + end + end + end + + def remove_column(table_name, *column_names) #:nodoc: + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? + column_names.each do |column_name| + alter_table(table_name) do |definition| + definition.columns.delete(definition[column_name]) + end + end + end + alias :remove_columns :remove_column + + def change_column_default(table_name, column_name, default) #:nodoc: + alter_table(table_name) do |definition| + definition[column_name].default = default + end + end + + def change_column_null(table_name, column_name, null, default = nil) + unless null || default.nil? + exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + alter_table(table_name) do |definition| + definition[column_name].null = null + end + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + alter_table(table_name) do |definition| + include_default = options_include_default?(options) + definition[column_name].instance_eval do + self.type = type + self.limit = options[:limit] if options.include?(:limit) + self.default = options[:default] if include_default + self.null = options[:null] if options.include?(:null) + self.precision = options[:precision] if options.include?(:precision) + self.scale = options[:scale] if options.include?(:scale) + end + end + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + unless columns(table_name).detect{|c| c.name == column_name.to_s } + raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" + end + alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) + end + + def empty_insert_statement_value + "VALUES(NULL)" + end + + protected + def select(sql, name = nil, binds = []) #:nodoc: + exec_query(sql, name, binds) + end + + def table_structure(table_name) + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash + raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? + structure + end + + def alter_table(table_name, options = {}) #:nodoc: + altered_table_name = "altered_#{table_name}" + caller = lambda {|definition| yield definition if block_given?} + + transaction do + move_table(table_name, altered_table_name, + options.merge(:temporary => true)) + move_table(altered_table_name, table_name, &caller) + end + end + + def move_table(from, to, options = {}, &block) #:nodoc: + copy_table(from, to, options, &block) + drop_table(from) + end + + def copy_table(from, to, options = {}) #:nodoc: + from_primary_key = primary_key(from) + options[:primary_key] = from_primary_key if from_primary_key != 'id' + unless options[:primary_key] + options[:id] = columns(from).detect{|c| c.name == 'id'}.present? && from_primary_key == 'id' + end + create_table(to, options) do |definition| + @definition = definition + columns(from).each do |column| + column_name = options[:rename] ? + (options[:rename][column.name] || + options[:rename][column.name.to_sym] || + column.name) : column.name + + @definition.column(column_name, column.type, + :limit => column.limit, :default => column.default, + :precision => column.precision, :scale => column.scale, + :null => column.null) + end + @definition.primary_key(from_primary_key) if from_primary_key + yield @definition if block_given? + end + + copy_table_indexes(from, to, options[:rename] || {}) + copy_table_contents(from, to, + @definition.columns.map {|column| column.name}, + options[:rename] || {}) + end + + def copy_table_indexes(from, to, rename = {}) #:nodoc: + indexes(from).each do |index| + name = index.name + if to == "altered_#{from}" + name = "temp_#{name}" + elsif from == "altered_#{to}" + name = name[5..-1] + end + + to_column_names = columns(to).map { |c| c.name } + columns = index.columns.map {|c| rename[c] || c }.select do |column| + to_column_names.include?(column) + end + + unless columns.empty? + # index name can't be the same + opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } + opts[:unique] = true if index.unique + add_index(to, columns, opts) + end + end + end + + def copy_table_contents(from, to, columns, rename = {}) #:nodoc: + column_mappings = Hash[columns.map {|name| [name, name]}] + rename.each { |a| column_mappings[a.last] = a.first } + from_columns = columns(from).collect {|col| col.name} + columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + quoted_columns = columns.map { |col| quote_column_name(col) } * ',' + + quoted_to = quote_table_name(to) + exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| + sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" + sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' + sql << ')' + exec_query sql + end + end + + def sqlite_version + @sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)')) + end + + def default_primary_key_type + if supports_autoincrement? + 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' + else + 'INTEGER PRIMARY KEY NOT NULL' + end + end + + def translate_exception(exception, message) + case exception.message + when /column(s)? .* (is|are) not unique/ + RecordNotUnique.new(message, exception) + else + super + end + end + end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb deleted file mode 100644 index c85c4c6e51..0000000000 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ /dev/null @@ -1,563 +0,0 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_record/connection_adapters/statement_pool' -require 'arel/visitors/bind_visitor' - -module ActiveRecord - module ConnectionAdapters #:nodoc: - class SQLiteColumn < Column #:nodoc: - class << self - def binary_to_string(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value - end - end - end - - # The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby - # drivers (available both as gems and from http://rubyforge.org/projects/sqlite-ruby/). - # - # Options: - # - # * <tt>:database</tt> - Path to the database file. - class SQLiteAdapter < AbstractAdapter - class Version - include Comparable - - def initialize(version_string) - @version = version_string.split('.').map { |v| v.to_i } - end - - def <=>(version_string) - @version <=> version_string.split('.').map { |v| v.to_i } - end - end - - class StatementPool < ConnectionAdapters::StatementPool - def initialize(connection, max) - super - @cache = Hash.new { |h,pid| h[pid] = {} } - end - - def each(&block); cache.each(&block); end - def key?(key); cache.key?(key); end - def [](key); cache[key]; end - def length; cache.length; end - - def []=(sql, key) - while @max <= cache.size - dealloc(cache.shift.last[:stmt]) - end - cache[sql] = key - end - - def clear - cache.values.each do |hash| - dealloc hash[:stmt] - end - cache.clear - end - - private - def cache - @cache[$$] - end - - def dealloc(stmt) - stmt.close unless stmt.closed? - end - end - - class BindSubstitution < Arel::Visitors::SQLite # :nodoc: - include Arel::Visitors::BindVisitor - end - - def initialize(connection, logger, config) - super(connection, logger) - @statements = StatementPool.new(@connection, - config.fetch(:statement_limit) { 1000 }) - @config = config - - if config.fetch(:prepared_statements) { true } - @visitor = Arel::Visitors::SQLite.new self - else - @visitor = BindSubstitution.new self - end - end - - def adapter_name #:nodoc: - 'SQLite' - end - - # Returns true if SQLite version is '2.0.0' or greater, false otherwise. - def supports_ddl_transactions? - sqlite_version >= '2.0.0' - end - - # Returns true if SQLite version is '3.6.8' or greater, false otherwise. - def supports_savepoints? - sqlite_version >= '3.6.8' - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # Returns true, since this connection adapter supports migrations. - def supports_migrations? #:nodoc: - true - end - - # Returns true. - def supports_primary_key? #:nodoc: - true - end - - # Returns true. - def supports_explain? - true - end - - def requires_reloading? - true - end - - # Returns true if SQLite version is '3.1.6' or greater, false otherwise. - def supports_add_column? - sqlite_version >= '3.1.6' - end - - # Disconnects from the database if already connected. Otherwise, this - # method does nothing. - def disconnect! - super - clear_cache! - @connection.close rescue nil - end - - # Clears the prepared statements cache. - def clear_cache! - @statements.clear - end - - # Returns true if SQLite version is '3.2.6' or greater, false otherwise. - def supports_count_distinct? #:nodoc: - sqlite_version >= '3.2.6' - end - - # Returns true if SQLite version is '3.1.0' or greater, false otherwise. - def supports_autoincrement? #:nodoc: - sqlite_version >= '3.1.0' - end - - def supports_index_sort_order? - sqlite_version >= '3.3.0' - end - - def native_database_types #:nodoc: - { - :primary_key => default_primary_key_type, - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "integer" }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "boolean" } - } - end - - - # QUOTING ================================================== - - def quote_string(s) #:nodoc: - @connection.class.quote(s) - end - - def quote_column_name(name) #:nodoc: - %Q("#{name.to_s.gsub('"', '""')}") - end - - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value - - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - @logger.error "Binary data inserted for `string` type on column `#{column.name}`" - value.encode! 'utf-8' - end - value - end - - # DATABASE STATEMENTS ====================================== - - def explain(arel, binds = []) - sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter - # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles - # the output of the SQLite shell: - # - # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) - # 0|1|1|SCAN TABLE posts (~100000 rows) - # - def pp(result) # :nodoc: - result.rows.map do |row| - row.join('|') - end.join("\n") + "\n" - end - end - - def exec_query(sql, name = nil, binds = []) - log(sql, name, binds) do - - # Don't cache statements without bind values - if binds.empty? - stmt = @connection.prepare(sql) - cols = stmt.columns - records = stmt.to_a - stmt.close - stmt = records - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - cols = cache[:cols] ||= stmt.columns - stmt.reset! - stmt.bind_params binds.map { |col, val| - type_cast(val, col) - } - end - - ActiveRecord::Result.new(cols, stmt.to_a) - end - end - - def exec_delete(sql, name = 'SQL', binds = []) - exec_query(sql, name, binds) - @connection.changes - end - alias :exec_update :exec_delete - - def last_inserted_id(result) - @connection.last_insert_row_id - end - - def execute(sql, name = nil) #:nodoc: - log(sql, name) { @connection.execute(sql) } - end - - def update_sql(sql, name = nil) #:nodoc: - super - @connection.changes - end - - def delete_sql(sql, name = nil) #:nodoc: - sql += " WHERE 1=1" unless sql =~ /WHERE/i - super sql, name - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super - id_value || @connection.last_insert_row_id - end - alias :create :insert_sql - - def select_rows(sql, name = nil) - exec_query(sql, name).rows - end - - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") - end - - def begin_db_transaction #:nodoc: - log('begin transaction',nil) { @connection.transaction } - end - - def commit_db_transaction #:nodoc: - log('commit transaction',nil) { @connection.commit } - end - - def rollback_db_transaction #:nodoc: - log('rollback transaction',nil) { @connection.rollback } - end - - # SCHEMA STATEMENTS ======================================== - - def tables(name = 'SCHEMA', table_name = nil) #:nodoc: - sql = <<-SQL - SELECT name - FROM sqlite_master - WHERE type = 'table' AND NOT name = 'sqlite_sequence' - SQL - sql << " AND name = #{quote_table_name(table_name)}" if table_name - - exec_query(sql, name).map do |row| - row['name'] - end - end - - def table_exists?(name) - name && tables('SCHEMA', name).any? - end - - # Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+. - def columns(table_name) #:nodoc: - table_structure(table_name).map do |field| - case field["dflt_value"] - when /^null$/i - field["dflt_value"] = nil - when /^'(.*)'$/ - field["dflt_value"] = $1.gsub(/''/, "'") - when /^"(.*)"$/ - field["dflt_value"] = $1.gsub(/""/, '"') - end - - SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) - end - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| - IndexDefinition.new( - table_name, - row['name'], - row['unique'] != 0, - exec_query("PRAGMA index_info('#{row['name']}')").map { |col| - col['name'] - }) - end - end - - def primary_key(table_name) #:nodoc: - column = table_structure(table_name).find { |field| - field['pk'] == 1 - } - column && column['name'] - end - - def remove_index!(table_name, index_name) #:nodoc: - exec_query "DROP INDEX #{quote_column_name(index_name)}" - end - - # Renames a table. - # - # Example: - # rename_table('octopuses', 'octopi') - def rename_table(name, new_name) - exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" - end - - # See: http://www.sqlite.org/lang_altertable.html - # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_options( type, options) - type.to_sym != :primary_key - end - - def add_column(table_name, column_name, type, options = {}) #:nodoc: - if supports_add_column? && valid_alter_table_options( type, options ) - super(table_name, column_name, type, options) - else - alter_table(table_name) do |definition| - definition.column(column_name, type, options) - end - end - end - - def remove_column(table_name, *column_names) #:nodoc: - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? - column_names.flatten.each do |column_name| - alter_table(table_name) do |definition| - definition.columns.delete(definition[column_name]) - end - end - end - alias :remove_columns :remove_column - - def change_column_default(table_name, column_name, default) #:nodoc: - alter_table(table_name) do |definition| - definition[column_name].default = default - end - end - - def change_column_null(table_name, column_name, null, default = nil) - unless null || default.nil? - exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") - end - alter_table(table_name) do |definition| - definition[column_name].null = null - end - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - alter_table(table_name) do |definition| - include_default = options_include_default?(options) - definition[column_name].instance_eval do - self.type = type - self.limit = options[:limit] if options.include?(:limit) - self.default = options[:default] if include_default - self.null = options[:null] if options.include?(:null) - self.precision = options[:precision] if options.include?(:precision) - self.scale = options[:scale] if options.include?(:scale) - end - end - end - - def rename_column(table_name, column_name, new_column_name) #:nodoc: - unless columns(table_name).detect{|c| c.name == column_name.to_s } - raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" - end - alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) - end - - def empty_insert_statement_value - "VALUES(NULL)" - end - - protected - def select(sql, name = nil, binds = []) #:nodoc: - exec_query(sql, name, binds) - end - - def table_structure(table_name) - structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", 'SCHEMA').to_hash - raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? - structure - end - - def alter_table(table_name, options = {}) #:nodoc: - altered_table_name = "altered_#{table_name}" - caller = lambda {|definition| yield definition if block_given?} - - transaction do - move_table(table_name, altered_table_name, - options.merge(:temporary => true)) - move_table(altered_table_name, table_name, &caller) - end - end - - def move_table(from, to, options = {}, &block) #:nodoc: - copy_table(from, to, options, &block) - drop_table(from) - end - - def copy_table(from, to, options = {}) #:nodoc: - options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s)) - create_table(to, options) do |definition| - @definition = definition - columns(from).each do |column| - column_name = options[:rename] ? - (options[:rename][column.name] || - options[:rename][column.name.to_sym] || - column.name) : column.name - - @definition.column(column_name, column.type, - :limit => column.limit, :default => column.default, - :precision => column.precision, :scale => column.scale, - :null => column.null) - end - @definition.primary_key(primary_key(from)) if primary_key(from) - yield @definition if block_given? - end - - copy_table_indexes(from, to, options[:rename] || {}) - copy_table_contents(from, to, - @definition.columns.map {|column| column.name}, - options[:rename] || {}) - end - - def copy_table_indexes(from, to, rename = {}) #:nodoc: - indexes(from).each do |index| - name = index.name - if to == "altered_#{from}" - name = "temp_#{name}" - elsif from == "altered_#{to}" - name = name[5..-1] - end - - to_column_names = columns(to).map { |c| c.name } - columns = index.columns.map {|c| rename[c] || c }.select do |column| - to_column_names.include?(column) - end - - unless columns.empty? - # index name can't be the same - opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } - opts[:unique] = true if index.unique - add_index(to, columns, opts) - end - end - end - - def copy_table_contents(from, to, columns, rename = {}) #:nodoc: - column_mappings = Hash[columns.map {|name| [name, name]}] - rename.each { |a| column_mappings[a.last] = a.first } - from_columns = columns(from).collect {|col| col.name} - columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} - quoted_columns = columns.map { |col| quote_column_name(col) } * ',' - - quoted_to = quote_table_name(to) - exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| - sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' - sql << ')' - exec_query sql - end - end - - def sqlite_version - @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)')) - end - - def default_primary_key_type - if supports_autoincrement? - 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL' - else - 'INTEGER PRIMARY KEY NOT NULL' - end - end - - def translate_exception(exception, message) - case exception.message - when /column(s)? .* (is|are) not unique/ - RecordNotUnique.new(message, exception) - else - super - end - end - - end - end -end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 3f90d25962..f2833fbf3c 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,4 +1,6 @@ require 'active_support/concern' +require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/object/deep_dup' require 'thread' module ActiveRecord @@ -75,7 +77,7 @@ module ActiveRecord ## # :singleton-method: - # Specifies wether or not has_many or has_one association option + # Specifies whether or not has_many or has_one association option # :dependent => :restrict raises an exception. If set to true, the # ActiveRecord::DeleteRestrictionError exception will be raised # along with a DEPRECATION WARNING. If set to false, an error would @@ -125,23 +127,29 @@ module ActiveRecord object.is_a?(self) end + # Returns an instance of <tt>Arel::Table</tt> loaded with the current table name. + # + # class Post < ActiveRecord::Base + # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0)) + # end def arel_table @arel_table ||= Arel::Table.new(table_name, arel_engine) end + # Returns the Arel engine. def arel_engine - @arel_engine ||= connection_handler.connection_pools[name] ? self : active_record_super.arel_engine + @arel_engine ||= connection_handler.retrieve_connection_pool(self) ? self : active_record_super.arel_engine end private def relation #:nodoc: - @relation ||= Relation.new(self, arel_table) + relation = Relation.new(self, arel_table) if finder_needs_type_condition? - @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) else - @relation + relation end end end @@ -164,7 +172,7 @@ module ActiveRecord # # Instantiates a single new object bypassing mass-assignment security # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) def initialize(attributes = nil, options = {}) - @attributes = self.class.initialize_attributes(self.class.column_defaults.dup) + @attributes = self.class.initialize_attributes(self.class.column_defaults.deep_dup) @columns_hash = self.class.column_types.dup init_internals @@ -176,7 +184,7 @@ module ActiveRecord assign_attributes(attributes, options) if attributes yield self if block_given? - run_callbacks :initialize + run_callbacks :initialize if _initialize_callbacks.any? end # Initialize an empty model object from +coder+. +coder+ must contain @@ -193,7 +201,6 @@ module ActiveRecord @attributes = self.class.initialize_attributes(coder['attributes']) @columns_hash = self.class.column_types.merge(coder['column_types'] || {}) - init_internals @new_record = false @@ -204,13 +211,34 @@ module ActiveRecord self end + ## + # :method: clone + # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied. + # That means that modifying attributes of the clone will modify the original, since they will both point to the + # same attributes hash. If you need a copy of your attributes hash, please use the #dup method. + # + # user = User.first + # new_user = user.clone + # user.name # => "Bob" + # new_user.name = "Joe" + # user.name # => "Joe" + # + # user.object_id == new_user.object_id # => false + # user.name.object_id == new_user.name.object_id # => true + # + # user.name.object_id == user.dup.name.object_id # => false + + ## + # :method: dup # Duped objects have no id assigned and are treated as new records. Note # that this is a "shallow" copy as it copies the object's attributes # only, not its associations. The extent of a "deep" copy is application # specific and is therefore left to the application to implement according # to its need. # The dup method does not preserve the timestamps (created|updated)_(at|on). - def initialize_dup(other) + + ## + def initialize_dup(other) # :nodoc: cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) self.class.initialize_attributes(cloned_attributes) @@ -327,6 +355,11 @@ module ActiveRecord "#<#{self.class} #{inspection}>" end + # Returns a hash of the given methods with their names as keys and returned values as values. + def slice(*methods) + Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access + end + private # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements @@ -346,7 +379,6 @@ module ActiveRecord @attributes[pk] = nil unless @attributes.key?(pk) - @relation = nil @aggregation_cache = {} @association_cache = {} @attributes_cache = {} diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 224f5276eb..b163ef3c12 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -69,9 +69,7 @@ module ActiveRecord "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" end - IdentityMap.remove_by_id(symbolized_base_class, id) if IdentityMap.enabled? - - update_all(updates.join(', '), primary_key => id ) + where(primary_key => id).update_all updates.join(', ') end # Increment a number field by one, usually representing a count. diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb deleted file mode 100644 index 38dbbef5fc..0000000000 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ /dev/null @@ -1,84 +0,0 @@ -module ActiveRecord - - # = Active Record Dynamic Finder Match - # - # Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info - # - class DynamicFinderMatch - def self.match(method) - method = method.to_s - klass = [FindBy, FindByBang, FindOrInitializeCreateBy].find do |_klass| - _klass.matches?(method) - end - klass.new(method) if klass - end - - def self.matches?(method) - method =~ self::METHOD_PATTERN - end - - def initialize(method) - @finder = :first - @instantiator = nil - match_data = method.match(self.class::METHOD_PATTERN) - @attribute_names = match_data[-1].split("_and_") - initialize_from_match_data(match_data) - end - - attr_reader :finder, :attribute_names, :instantiator - - def finder? - @finder && !@instantiator - end - - def creator? - @finder == :first && @instantiator == :create - end - - def instantiator? - @instantiator - end - - def bang? - false - end - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - - private - - def initialize_from_match_data(match_data) - end - end - - class FindBy < DynamicFinderMatch - METHOD_PATTERN = /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @finder = :last if match_data[1] == 'last_' - @finder = :all if match_data[1] == 'all_' - end - end - - class FindByBang < DynamicFinderMatch - METHOD_PATTERN = /^find_by_([_a-zA-Z]\w*)\!$/ - - def bang? - true - end - end - - class FindOrInitializeCreateBy < DynamicFinderMatch - METHOD_PATTERN = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @instantiator = match_data[1] == 'initialize' ? :new : :create - end - - def valid_arguments?(arguments) - arguments.size == 1 && arguments.first.is_a?(Hash) || super - end - end -end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 60ce3dd4f1..e278e62ce7 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,79 +1,130 @@ module ActiveRecord - module DynamicMatchers - def respond_to?(method_id, include_private = false) - if match = DynamicFinderMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - elsif match = DynamicScopeMatch.match(method_id) - return true if all_attributes_exists?(match.attribute_names) - end + module DynamicMatchers #:nodoc: + # This code in this file seems to have a lot of indirection, but the indirection + # is there to provide extension points for the active_record_deprecated_finders + # gem. When we stop supporting active_record_deprecated_finders (from Rails 5), + # then we can remove the indirection. - super + def respond_to?(name, include_private = false) + match = Method.match(self, name) + match && match.valid? || super end private - # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and - # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders - # section at the top of this file for more detailed information. - # - # It's even possible to use all the additional parameters to +find+. For example, the - # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. - # - # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it - # is first invoked, so that future attempts to use it do not run through method_missing. - def method_missing(method_id, *arguments, &block) - if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id)) - attribute_names = match.attribute_names - super unless all_attributes_exists?(attribute_names) - unless match.valid_arguments?(arguments) - method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'" - backtrace = [method_trace] + caller - raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace - end - if match.respond_to?(:scope?) && match.scope? - self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) - attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)] - # - scoped(:conditions => attributes) # scoped(:conditions => attributes) - end # end - METHOD - send(method_id, *arguments) - elsif match.finder? - options = arguments.extract_options! - relation = options.any? ? scoped(options) : scoped - relation.send :find_by_attributes, match, attribute_names, *arguments, &block - elsif match.instantiator? - scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block - end + def method_missing(name, *arguments, &block) + match = Method.match(self, name) + + if match && match.valid? + match.define + send(name, *arguments, &block) else super end end - # Similar in purpose to +expand_hash_conditions_for_aggregates+. - def expand_attribute_names_for_aggregates(attribute_names) - attribute_names.map { |attribute_name| - unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? - aggregate_mapping(aggregation).map do |field_attr, _| - field_attr.to_sym - end - else - attribute_name.to_sym + class Method + @matchers = [] + + class << self + attr_reader :matchers + + def match(model, name) + klass = matchers.find { |k| name =~ k.pattern } + klass.new(model, name) if klass + end + + def pattern + /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/ + end + + def prefix + raise NotImplementedError end - }.flatten + + def suffix + '' + end + end + + attr_reader :model, :name, :attribute_names + + def initialize(model, name) + @model = model + @name = name.to_s + @attribute_names = @name.match(self.class.pattern)[1].split('_and_') + end + + def valid? + attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } + end + + def define + model.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def self.#{name}(#{signature}) + #{body} + end + CODE + end + + def body + raise NotImplementedError + end end - def all_attributes_exists?(attribute_names) - (expand_attribute_names_for_aggregates(attribute_names) - - column_methods_hash.keys).empty? + module Finder + # Extended in active_record_deprecated_finders + def body + result + end + + # Extended in active_record_deprecated_finders + def result + "#{finder}(#{attributes_hash})" + end + + # Extended in active_record_deprecated_finders + def signature + attribute_names.join(', ') + end + + def attributes_hash + "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}" + end + + def finder + raise NotImplementedError + end end - def aggregate_mapping(reflection) - mapping = reflection.options[:mapping] || [reflection.name, reflection.name] - mapping.first.is_a?(Array) ? mapping : [mapping] + class FindBy < Method + Method.matchers << self + include Finder + + def self.prefix + "find_by" + end + + def finder + "find_by" + end end + class FindByBang < Method + Method.matchers << self + include Finder + + def self.prefix + "find_by" + end + + def self.suffix + "!" + end + def finder + "find_by!" + end + end end end diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb deleted file mode 100644 index a502155aac..0000000000 --- a/activerecord/lib/active_record/dynamic_scope_match.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActiveRecord - - # = Active Record Dynamic Scope Match - # - # Provides dynamic attribute-based scopes such as <tt>scoped_by_price(4.99)</tt> - # if, for example, the <tt>Product</tt> has an attribute with that name. You can - # chain more <tt>scoped_by_* </tt> methods after the other. It acts like a named - # scope except that it's dynamic. - class DynamicScopeMatch - def self.match(method) - return unless method.to_s =~ /^scoped_by_([_a-zA-Z]\w*)$/ - new(true, $1 && $1.split('_and_')) - end - - def initialize(scope, attribute_names) - @scope = scope - @attribute_names = attribute_names - end - - attr_reader :scope, :attribute_names - alias :scope? :scope - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - end -end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 01cacf6153..313fdb3487 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -70,7 +70,7 @@ module ActiveRecord # the threshold is set to 0. # # As the name of the method suggests this only applies to automatic - # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run. + # EXPLAINs, manual calls to <tt>ActiveRecord::Relation#explain</tt> run. def silence_auto_explain current = Thread.current original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b82d5b5621..7e6512501c 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -4,16 +4,12 @@ require 'zlib' require 'active_support/dependencies' require 'active_support/core_ext/object/blank' require 'active_record/fixtures/file' +require 'active_record/errors' -if defined? ActiveRecord +module ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: end -else - class FixtureClassNotFound < StandardError #:nodoc: - end -end -module ActiveRecord # \Fixtures are a way of organizing data that you want to test against; in short, sample data. # # They are stored in YAML files, one file per model, which are placed in the directory @@ -87,7 +83,7 @@ module ActiveRecord # end # # test "find_alt_method_2" do - # assert_equal "Ruby on Rails", @rubyonrails.news + # assert_equal "Ruby on Rails", @rubyonrails.name # end # # In order to use these methods to access fixtured data within your testcases, you must specify one of the @@ -376,19 +372,23 @@ module ActiveRecord # # Any fixture labeled "DEFAULTS" is safely ignored. class Fixtures + #-- + # NOTE: an instance of Fixtures can be called fixture_set, it is normally stored in a single YAML file and possibly in a folder with the same name. + #++ MAX_ID = 2 ** 30 - 1 @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.default_fixture_model_name(fixture_name) # :nodoc: + def self.default_fixture_model_name(fixture_set_name) # :nodoc: ActiveRecord::Base.pluralize_table_names ? - fixture_name.singularize.camelize : - fixture_name.camelize + fixture_set_name.singularize.camelize : + fixture_set_name.camelize end - def self.default_fixture_table_name(fixture_name) # :nodoc: - "#{ActiveRecord::Base.table_name_prefix}"\ - "#{fixture_name.tr('/', '_')}#{ActiveRecord::Base.table_name_suffix}".to_sym + def self.default_fixture_table_name(fixture_set_name) # :nodoc: + "#{ ActiveRecord::Base.table_name_prefix }"\ + "#{ fixture_set_name.tr('/', '_') }"\ + "#{ ActiveRecord::Base.table_name_suffix }".to_sym end def self.reset_cache @@ -415,11 +415,11 @@ module ActiveRecord cache_for_connection(connection).update(fixtures_map) end - def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true) + def self.instantiate_fixtures(object, fixture_set, load_instances = true) if load_instances - fixtures.each do |name, fixture| + fixture_set.each do |fixture_name, fixture| begin - object.instance_variable_set "@#{name}", fixture.find + object.instance_variable_set "@#{fixture_name}", fixture.find rescue FixtureClassNotFound nil end @@ -428,61 +428,59 @@ module ActiveRecord end def self.instantiate_all_loaded_fixtures(object, load_instances = true) - all_loaded_fixtures.each do |table_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances) + all_loaded_fixtures.each_value do |fixture_set| + instantiate_fixtures(object, fixture_set, load_instances) end end cattr_accessor :all_loaded_fixtures self.all_loaded_fixtures = {} - def self.create_fixtures(fixtures_directory, table_names, class_names = {}) - table_names = Array(table_names).map(&:to_s) + def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}) + fixture_set_names = Array(fixture_set_names).map(&:to_s) class_names = class_names.stringify_keys # FIXME: Apparently JK uses this. connection = block_given? ? yield : ActiveRecord::Base.connection - files_to_read = table_names.reject { |table_name| - fixture_is_cached?(connection, table_name) + files_to_read = fixture_set_names.reject { |fs_name| + fixture_is_cached?(connection, fs_name) } unless files_to_read.empty? connection.disable_referential_integrity do fixtures_map = {} - fixture_files = files_to_read.map do |path| - fixture_name = path - - fixtures_map[fixture_name] = new( # ActiveRecord::Fixtures.new + fixture_sets = files_to_read.map do |fs_name| + fixtures_map[fs_name] = new( # ActiveRecord::Fixtures.new connection, - fixture_name, - class_names[fixture_name.to_s] || default_fixture_model_name(fixture_name), - ::File.join(fixtures_directory, path)) + fs_name, + class_names[fs_name] || default_fixture_model_name(fs_name), + ::File.join(fixtures_directory, fs_name)) end all_loaded_fixtures.update(fixtures_map) connection.transaction(:requires_new => true) do - fixture_files.each do |ff| - conn = ff.model_class.respond_to?(:connection) ? ff.model_class.connection : connection - table_rows = ff.table_rows + fixture_sets.each do |fs| + conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection + table_rows = fs.table_rows table_rows.keys.each do |table| conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' end - table_rows.each do |table_name,rows| + table_rows.each do |fixture_set_name, rows| rows.each do |row| - conn.insert_fixture(row, table_name) + conn.insert_fixture(row, fixture_set_name) end end end # Cap primary key sequences to max(pk). if connection.respond_to?(:reset_pk_sequence!) - fixture_files.each do |ff| - connection.reset_pk_sequence!(ff.table_name) + fixture_sets.each do |fs| + connection.reset_pk_sequence!(fs.table_name) end end end @@ -490,7 +488,7 @@ module ActiveRecord cache_fixtures(connection, fixtures_map) end end - cached_fixtures(connection, table_names) + cached_fixtures(connection, fixture_set_names) end # Returns a consistent, platform-independent identifier for +label+. @@ -501,26 +499,23 @@ module ActiveRecord attr_reader :table_name, :name, :fixtures, :model_class - def initialize(connection, fixture_name, class_name, fixture_path) - @connection = connection - @fixture_path = fixture_path - @name = fixture_name - @class_name = class_name + def initialize(connection, name, class_name, path) + @fixtures = {} # Ordered hash + @name = name + @path = path - @fixtures = {} - - # Should be an AR::Base type class - if class_name.is_a?(Class) + if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name else @model_class = class_name.constantize rescue nil end - @connection = model_class.connection if model_class && model_class.respond_to?(:connection) + @connection = ( model_class.respond_to?(:connection) ? + model_class.connection : connection ) @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : - self.class.default_fixture_table_name(fixture_name) ) + self.class.default_fixture_table_name(name) ) read_fixture_files end @@ -559,8 +554,8 @@ module ActiveRecord if model_class && model_class < ActiveRecord::Model # fill in timestamp columns if they aren't specified and the model is set to record_timestamps if model_class.record_timestamps - timestamp_column_names.each do |name| - row[name] = now unless row.key?(name) + timestamp_column_names.each do |c_name| + row[c_name] = now unless row.key?(c_name) end end @@ -638,26 +633,23 @@ module ActiveRecord end def read_fixture_files - yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f| + yaml_files = Dir["#{@path}/**/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path] yaml_files.each do |file| Fixtures::File.open(file) do |fh| - fh.each do |name, row| - fixtures[name] = ActiveRecord::Fixture.new(row, model_class) + fh.each do |fixture_name, row| + fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end end end end def yaml_file_path - "#{@fixture_path}.yml" + "#{@path}.yml" end - def yaml_fixtures_key(path) - ::File.basename(@fixture_path).split(".").first - end end class Fixture #:nodoc: @@ -712,7 +704,7 @@ module ActiveRecord class_attribute :fixture_table_names class_attribute :fixture_class_names class_attribute :use_transactional_fixtures - class_attribute :use_instantiated_fixtures # true, false, or :no_instances + class_attribute :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures self.fixture_table_names = [] @@ -720,9 +712,8 @@ module ActiveRecord self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false - self.fixture_class_names = Hash.new do |h, fixture_name| - fixture_name = fixture_name.to_s - h[fixture_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_name) + self.fixture_class_names = Hash.new do |h, fixture_set_name| + h[fixture_set_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_set_name) end end @@ -746,18 +737,18 @@ module ActiveRecord self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) end - def fixtures(*fixture_names) - if fixture_names.first == :all - fixture_names = Dir["#{fixture_path}/**/*.yml"].map { |f| + def fixtures(*fixture_set_names) + if fixture_set_names.first == :all + fixture_set_names = Dir["#{fixture_path}/**/*.yml"].map { |f| File.basename f, '.yml' } else - fixture_names = fixture_names.flatten.map { |n| n.to_s } + fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } end - self.fixture_table_names |= fixture_names - require_fixture_classes(fixture_names) - setup_fixture_accessors(fixture_names) + self.fixture_table_names |= fixture_set_names + require_fixture_classes(fixture_set_names) + setup_fixture_accessors(fixture_set_names) end def try_to_load_dependency(file_name) @@ -772,35 +763,39 @@ module ActiveRecord end end - def require_fixture_classes(fixture_names = nil) - (fixture_names || fixture_table_names).each do |fixture_name| - file_name = fixture_name.to_s + def require_fixture_classes(fixture_set_names = nil) + if fixture_set_names + fixture_set_names = fixture_set_names.map { |n| n.to_s } + else + fixture_set_names = fixture_table_names + end + + fixture_set_names.each do |file_name| file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names try_to_load_dependency(file_name) end end - def setup_fixture_accessors(fixture_names = nil) - fixture_names = Array(fixture_names || fixture_table_names) + def setup_fixture_accessors(fixture_set_names = nil) + fixture_set_names = Array(fixture_set_names || fixture_table_names) methods = Module.new do - fixture_names.each do |fixture_name| - fixture_name = fixture_name.to_s - accessor_name = fixture_name.tr('/', '_').to_sym + fixture_set_names.each do |fs_name| + fs_name = fs_name.to_s + accessor_name = fs_name.tr('/', '_').to_sym - define_method(accessor_name) do |*fixtures| - force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload + define_method(accessor_name) do |*fixture_names| + force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload - @fixture_cache[fixture_name] ||= {} + @fixture_cache[fs_name] ||= {} - instances = fixtures.map do |fixture| - @fixture_cache[fixture_name].delete(fixture) if force_reload + instances = fixture_names.map do |f_name| + f_name = f_name.to_s + @fixture_cache[fs_name].delete(f_name) if force_reload - if @loaded_fixtures[fixture_name][fixture.to_s] - ActiveRecord::IdentityMap.without do - @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find - end + if @loaded_fixtures[fs_name][f_name] + @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find else - raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'" + raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'" end end @@ -829,7 +824,7 @@ module ActiveRecord end def setup_fixtures - return unless !ActiveRecord::Base.configurations.blank? + return if ActiveRecord::Base.configurations.blank? if pre_loaded_fixtures && !use_transactional_fixtures raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' @@ -907,8 +902,8 @@ module ActiveRecord ActiveRecord::Fixtures.instantiate_all_loaded_fixtures(self, load_instances?) else raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? - @loaded_fixtures.each do |fixture_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?) + @loaded_fixtures.each_value do |fixture_set| + ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_set, load_instances?) end end end diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb deleted file mode 100644 index d9777bb2f6..0000000000 --- a/activerecord/lib/active_record/identity_map.rb +++ /dev/null @@ -1,144 +0,0 @@ -module ActiveRecord - # = Active Record Identity Map - # - # Ensures that each object gets loaded only once by keeping every loaded - # object in a map. Looks up objects using the map when referring to them. - # - # More information on Identity Map pattern: - # http://www.martinfowler.com/eaaCatalog/identityMap.html - # - # == Configuration - # - # In order to enable IdentityMap, set <tt>config.active_record.identity_map = true</tt> - # in your <tt>config/application.rb</tt> file. - # - # IdentityMap is disabled by default and still in development (i.e. use it with care). - # - # == Associations - # - # Active Record Identity Map does not track associations yet. For example: - # - # comment = @post.comments.first - # comment.post = nil - # @post.comments.include?(comment) #=> true - # - # Ideally, the example above would return false, removing the comment object from the - # post association when the association is nullified. This may cause side effects, as - # in the situation below, if Identity Map is enabled: - # - # Post.has_many :comments, :dependent => :destroy - # - # comment = @post.comments.first - # comment.post = nil - # comment.save - # Post.destroy(@post.id) - # - # Without using Identity Map, the code above will destroy the @post object leaving - # the comment object intact. However, once we enable Identity Map, the post loaded - # by Post.destroy is exactly the same object as the object @post. As the object @post - # still has the comment object in @post.comments, once Identity Map is enabled, the - # comment object will be accidently removed. - # - # This inconsistency is meant to be fixed in future Rails releases. - # - module IdentityMap - - class << self - def enabled=(flag) - Thread.current[:identity_map_enabled] = flag - end - - def enabled - Thread.current[:identity_map_enabled] - end - alias enabled? enabled - - def repository - Thread.current[:identity_map] ||= Hash.new { |h,k| h[k] = {} } - end - - def use - old, self.enabled = enabled, true - - yield if block_given? - ensure - self.enabled = old - clear - end - - def without - old, self.enabled = enabled, false - - yield if block_given? - ensure - self.enabled = old - end - - def get(klass, primary_key) - record = repository[klass.symbolized_sti_name][primary_key] - - if record.is_a?(klass) - ActiveSupport::Notifications.instrument("identity.active_record", - :line => "From Identity Map (id: #{primary_key})", - :name => "#{klass} Loaded", - :connection_id => object_id) - - record - else - nil - end - end - - def add(record) - repository[record.class.symbolized_sti_name][record.id] = record - end - - def remove(record) - repository[record.class.symbolized_sti_name].delete(record.id) - end - - def remove_by_id(symbolized_sti_name, id) - repository[symbolized_sti_name].delete(id) - end - - def clear - repository.clear - end - end - - # Reinitialize an Identity Map model object from +coder+. - # +coder+ must contain the attributes necessary for initializing an empty - # model object. - def reinit_with(coder) - @attributes_cache = {} - dirty = @changed_attributes.keys - attributes = self.class.initialize_attributes(coder['attributes'].except(*dirty)) - @attributes.update(attributes) - @changed_attributes.update(coder['attributes'].slice(*dirty)) - @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]} - - run_callbacks :find - - self - end - - class Middleware - def initialize(app) - @app = app - end - - def call(env) - enabled = IdentityMap.enabled - IdentityMap.enabled = true - - response = @app.call(env) - response[2] = Rack::BodyProxy.new(response[2]) do - IdentityMap.enabled = enabled - IdentityMap.clear - end - - response - end - end - end -end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 2c766411a0..46d253b0a7 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -48,6 +48,20 @@ module ActiveRecord end # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). + # If you are using inheritance with ActiveRecord and don't want child classes + # to utilize the implied STI table name of the parent class, this will need to be true. + # For example, given the following: + # + # class SuperClass < ActiveRecord::Base + # self.abstract_class = true + # end + # class Child < SuperClass + # self.table_name = 'the_table_i_really_want' + # end + # + # + # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt> + # attr_accessor :abstract_class # Returns whether this class is an abstract class or not. @@ -63,26 +77,9 @@ module ActiveRecord # single-table inheritance model that makes it possible to create # objects of different types from the same table. def instantiate(record, column_types = {}) - sti_class = find_sti_class(record[inheritance_column]) - record_id = sti_class.primary_key && record[sti_class.primary_key] - - if ActiveRecord::IdentityMap.enabled? && record_id - if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number? - record_id = record_id.to_i - end - if instance = IdentityMap.get(sti_class, record_id) - instance.reinit_with('attributes' => record) - else - instance = sti_class.allocate.init_with('attributes' => record) - IdentityMap.add(instance) - end - else - column_types = sti_class.decorate_columns(column_types) - instance = sti_class.allocate.init_with('attributes' => record, - 'column_types' => column_types) - end - - instance + sti_class = find_sti_class(record[inheritance_column]) + column_types = sti_class.decorate_columns(column_types) + sti_class.allocate.init_with('attributes' => record, 'column_types' => column_types) end # For internal use. diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 2c42f4cca5..23c272ef12 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -39,7 +39,7 @@ module ActiveRecord when new_record? "#{self.class.model_name.cache_key}/new" when timestamp = self[:updated_at] - timestamp = timestamp.utc.to_s(:number) + timestamp = timestamp.utc.to_s(:nsec) "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" else "#{self.class.model_name.cache_key}/#{id}" diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 4d73cdd37a..a3412582fa 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -84,7 +84,7 @@ module ActiveRecord relation.table[self.class.primary_key].eq(id).and( relation.table[lock_col].eq(quote_value(previous_lock_value)) ) - ).arel.compile_update(arel_attributes_values(false, false, attribute_names)) + ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names)) affected_rows = connection.update stmt @@ -101,24 +101,29 @@ module ActiveRecord end end - def destroy #:nodoc: - return super unless locking_enabled? + def destroy_row + affected_rows = super - if persisted? - table = self.class.arel_table - lock_col = self.class.locking_column - predicate = table[self.class.primary_key].eq(id). - and(table[lock_col].eq(send(lock_col).to_i)) + if locking_enabled? && affected_rows != 1 + raise ActiveRecord::StaleObjectError.new(self, "destroy") + end - affected_rows = self.class.unscoped.where(predicate).delete_all + affected_rows + end - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "destroy") - end + def relation_for_destroy + relation = super + + if locking_enabled? + column_name = self.class.locking_column + column = self.class.columns_hash[column_name] + substitute = connection.substitute_at(column, relation.bind_values.length) + + relation = relation.where(self.class.arel_table[column_name].eq(substitute)) + relation.bind_values << [column, self[column_name].to_i] end - @destroyed = true - freeze + relation end module ClassMethods @@ -133,8 +138,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - @original_locking_column = @locking_column if defined?(@locking_column) - @locking_column = value.to_s + @locking_column = value.to_s end # The version column used for optimistic locking. Defaults to +lock_version+. diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb index 86de5ab2fa..105d1e0e2b 100644 --- a/activerecord/lib/active_record/model.rb +++ b/activerecord/lib/active_record/model.rb @@ -60,7 +60,6 @@ module ActiveRecord include AttributeMethods include Callbacks, ActiveModel::Observing, Timestamp include Associations - include IdentityMap include ActiveModel::SecurePassword include AutosaveAssociation, NestedAttributes include Aggregations, Transactions, Reflection, Serialization, Store diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index b8764217d3..7f38dda11e 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -114,11 +114,18 @@ module ActiveRecord # You can also just define your own <tt>self.table_name</tt> method; see # the documentation for ActiveRecord::Base#table_name. def table_name=(value) - @original_table_name = @table_name if defined?(@table_name) - @table_name = value && value.to_s - @quoted_table_name = nil - @arel_table = nil - @relation = Relation.new(self, arel_table) + value = value && value.to_s + + if defined?(@table_name) + return if value == @table_name + reset_column_information if connected? + end + + @table_name = value + @quoted_table_name = nil + @arel_table = nil + @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name + @relation = Relation.new(self, arel_table) end # Returns a quoted version of the table name, used to construct SQL statements. @@ -152,8 +159,8 @@ module ActiveRecord # Sets the value of inheritance_column def inheritance_column=(value) - @original_inheritance_column = inheritance_column - @inheritance_column = value.to_s + @inheritance_column = value.to_s + @explicit_inheritance_column = true end def sequence_name @@ -165,7 +172,8 @@ module ActiveRecord end def reset_sequence_name #:nodoc: - self.sequence_name = connection.default_sequence_name(table_name, primary_key) + @explicit_sequence_name = false + @sequence_name = connection.default_sequence_name(table_name, primary_key) end # Sets the name of the sequence to use when generating ids to the given @@ -183,8 +191,8 @@ module ActiveRecord # self.sequence_name = "projectseq" # default would have been "project_seq" # end def sequence_name=(value) - @original_sequence_name = @sequence_name if defined?(@sequence_name) @sequence_name = value.to_s + @explicit_sequence_name = true end # Indicates whether the table associated with this class exists @@ -296,7 +304,7 @@ module ActiveRecord @column_types = nil @content_columns = nil @dynamic_methods_hash = nil - @inheritance_column = nil + @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @relation = nil end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 6bf0becad8..95a2ddcc11 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -19,10 +19,10 @@ module ActiveRecord # = Active Record Nested Attributes # # Nested attributes allow you to save attributes on associated records - # through the parent. By default nested attribute updating is turned off, - # you can enable it using the accepts_nested_attributes_for class method. - # When you enable nested attributes an attribute writer is defined on - # the model. + # through the parent. By default nested attribute updating is turned off + # and you can enable it using the accepts_nested_attributes_for class + # method. When you enable nested attributes an attribute writer is + # defined on the model. # # The attribute writer is named after the association, which means that # in the following example, two new methods are added to your model: @@ -288,7 +288,7 @@ module ActiveRecord # def pirate_attributes=(attributes) # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options) # end - class_eval <<-eoruby, __FILE__, __LINE__ + 1 + generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1 if method_defined?(:#{association_name}_attributes=) remove_method(:#{association_name}_attributes=) end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 60c37ac2b7..c2d3eeb8ce 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -6,5 +6,58 @@ module ActiveRecord def exec_queries @records = [] end + + def pluck(column_name) + [] + end + + def delete_all(conditions = nil) + 0 + end + + def update_all(updates, conditions = nil, options = {}) + 0 + end + + def delete(id_or_array) + 0 + end + + def size + 0 + end + + def empty? + true + end + + def any? + false + end + + def many? + false + end + + def to_sql + @to_sql ||= "" + end + + def where_values_hash + {} + end + + def count + 0 + end + + def calculate(operation, column_name, options = {}) + nil + end + + def exists?(id = false) + false + end + end end
\ No newline at end of file diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 9bc046c775..a1bc39a32d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -115,10 +115,7 @@ module ActiveRecord # callbacks, Observer methods, or any <tt>:dependent</tt> association # options, use <tt>#destroy</tt>. def delete - if persisted? - self.class.delete(id) - IdentityMap.remove(self) if IdentityMap.enabled? - end + self.class.delete(id) if persisted? @destroyed = true freeze end @@ -126,21 +123,9 @@ module ActiveRecord # Deletes the record in the database and freezes this instance to reflect # that no changes should be made (since they can't be persisted). def destroy + raise ReadOnlyRecord if readonly? destroy_associations - - if persisted? - IdentityMap.remove(self) if IdentityMap.enabled? - pk = self.class.primary_key - column = self.class.columns_hash[pk] - substitute = connection.substitute_at(column, 0) - - relation = self.class.unscoped.where( - self.class.arel_table[pk].eq(substitute)) - - relation.bind_values = [[column, id]] - relation.delete_all - end - + destroy_row if persisted? @destroyed = true freeze end @@ -176,7 +161,7 @@ module ActiveRecord # def update_attribute(name, value) name = name.to_s - raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) + verify_readonly_attribute(name) send("#{name}=", value) save(:validate => false) end @@ -191,10 +176,10 @@ module ActiveRecord # attribute is marked as readonly. def update_column(name, value) name = name.to_s - raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) + verify_readonly_attribute(name) raise ActiveRecordError, "can not update on a new record object" unless persisted? raw_write_attribute(name, value) - self.class.update_all({ name => value }, self.class.primary_key => id) == 1 + self.class.where(self.class.primary_key => id).update_all(name => value) == 1 end # Updates the attributes of the model from the passed-in hash and saves the @@ -284,11 +269,15 @@ module ActiveRecord clear_aggregation_cache clear_association_cache - IdentityMap.without do - fresh_object = self.class.unscoped { self.class.find(id, options) } - @attributes.update(fresh_object.instance_variable_get('@attributes')) - @columns_hash = fresh_object.instance_variable_get('@columns_hash') - end + fresh_object = + if options && options[:lock] + self.class.unscoped { self.class.lock.find(id) } + else + self.class.unscoped { self.class.find(id) } + end + + @attributes.update(fresh_object.instance_variable_get('@attributes')) + @columns_hash = fresh_object.instance_variable_get('@columns_hash') @attributes_cache = {} self @@ -323,14 +312,15 @@ module ActiveRecord changes = {} attributes.each do |column| - changes[column.to_s] = write_attribute(column.to_s, current_time) + column = column.to_s + changes[column] = write_attribute(column, current_time) end changes[self.class.locking_column] = increment_lock if locking_enabled? @changed_attributes.except!(*changes.keys) primary_key = self.class.primary_key - self.class.unscoped.update_all(changes, { primary_key => self[primary_key] }) == 1 + self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 end end @@ -340,6 +330,22 @@ module ActiveRecord def destroy_associations end + def destroy_row + relation_for_destroy.delete_all + end + + def relation_for_destroy + pk = self.class.primary_key + column = self.class.columns_hash[pk] + substitute = connection.substitute_at(column, 0) + + relation = self.class.unscoped.where( + self.class.arel_table[pk].eq(substitute)) + + relation.bind_values = [[column, id]] + relation + end + def create_or_update raise ReadOnlyRecord if readonly? result = new_record? ? create : update @@ -349,7 +355,7 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def update(attribute_names = @attributes.keys) - attributes_with_values = arel_attributes_values(false, false, attribute_names) + attributes_with_values = arel_attributes_with_values_for_update(attribute_names) return 0 if attributes_with_values.empty? klass = self.class stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values) @@ -359,15 +365,17 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. def create - attributes_values = arel_attributes_values(!id.nil?) + attributes_values = arel_attributes_with_values_for_create(!id.nil?) new_id = self.class.unscoped.insert attributes_values - self.id ||= new_id if self.class.primary_key - IdentityMap.add(self) if IdentityMap.enabled? @new_record = false id end + + def verify_readonly_attribute(name) + raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) + end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 0e6fecbc4b..4d8283bcff 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -3,14 +3,15 @@ require 'active_support/deprecation' module ActiveRecord module Querying - delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped + delegate :find, :take, :take!, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped + delegate :find_by, :find_by!, :to => :scoped delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped delegate :find_each, :find_in_batches, :to => :scoped delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :references, :none, :to => :scoped - delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped + delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :scoped # Executes a custom SQL query against your database and returns all the results. The results will # be returned as an array with columns requested encapsulated as attributes of the model you call diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 058dd58efb..eb2769f1ef 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -53,11 +53,6 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end - initializer "active_record.identity_map" do |app| - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::IdentityMap::Middleware" if config.active_record.delete(:identity_map) - end - initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do if app.config.active_record.delete(:whitelist_attributes) @@ -73,7 +68,9 @@ module ActiveRecord # and then establishes the connection. initializer "active_record.initialize_database" do |app| ActiveSupport.on_load(:active_record) do - self.configurations = app.config.database_configuration + unless ENV['DATABASE_URL'] + self.configurations = app.config.database_configuration + end establish_connection end end @@ -107,7 +104,7 @@ module ActiveRecord config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"] end - config.after_initialize do + config.after_initialize do |app| ActiveSupport.on_load(:active_record) do ActiveRecord::Base.instantiate_observers @@ -115,6 +112,21 @@ module ActiveRecord ActiveRecord::Base.instantiate_observers end end + + ActiveSupport.on_load(:active_record) do + if app.config.use_schema_cache_dump + filename = File.join(app.config.paths["db"].first, "schema_cache.dump") + if File.file?(filename) + cache = Marshal.load File.binread filename + if cache.version == ActiveRecord::Migrator.current_version + ActiveRecord::Base.connection.schema_cache = cache + else + warn "schema_cache.dump is expired. Current version is #{ActiveRecord::Migrator.current_version}, but cache version is #{cache.version}." + end + end + end + end + end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 14bb0a724e..791a22958c 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -372,6 +372,25 @@ db_namespace = namespace :db do task :load_if_ruby => 'db:create' do db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby end + + namespace :cache do + desc 'Create a db/schema_cache.dump file.' + task :dump => :environment do + con = ActiveRecord::Base.connection + filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump") + + con.schema_cache.clear! + con.tables.each { |table| con.schema_cache.add(table) } + open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) } + end + + desc 'Clear a db/schema_cache.dump file.' + task :clear => :environment do + filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump") + FileUtils.rm(filename) if File.exists?(filename) + end + end + end namespace :structure do @@ -387,9 +406,9 @@ db_namespace = namespace :db do set_psql_env(abcs[Rails.env]) search_path = abcs[Rails.env]['schema_search_path'] unless search_path.blank? - search_path = search_path.split(",").map{|search_path_part| "--schema=#{search_path_part.strip}" }.join(" ") + search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") end - `pg_dump -i -s -x -O -f #{filename} #{search_path} #{abcs[Rails.env]['database']}` + `pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(abcs[Rails.env]['database'])}` raise 'Error dumping database' if $?.exitstatus == 1 when /sqlite/ dbfile = abcs[Rails.env]['database'] @@ -407,6 +426,7 @@ db_namespace = namespace :db do if ActiveRecord::Base.connection.supports_migrations? File.open(filename, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } end + db_namespace['structure:dump'].reenable end # desc "Recreate the databases from the structure.sql file" @@ -458,7 +478,7 @@ db_namespace = namespace :db do db_namespace["test:load_schema"].invoke when :sql db_namespace["test:load_structure"].invoke - end + end end # desc "Recreate the test database from an existent structure.sql file" @@ -546,7 +566,7 @@ namespace :railties do # desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2" task :migrations => :'db:load_config' do to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip } - railties = ActiveSupport::OrderedHash.new + railties = {} Rails.application.railties.all do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb index 0b75983484..6a38211bff 100644 --- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb +++ b/activerecord/lib/active_record/railties/jdbcmysql_error.rb @@ -1,6 +1,6 @@ #FIXME Remove if ArJdbcMysql will give. module ArJdbcMySQL #:nodoc: - class Error < StandardError + class Error < StandardError #:nodoc: attr_accessor :error_number, :sql_state def initialize msg diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 8f8a3ec3bb..c380b5c029 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -23,11 +23,11 @@ module ActiveRecord module ClassMethods def create_reflection(macro, name, options, active_record) case macro - when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many - klass = options[:through] ? ThroughReflection : AssociationReflection - reflection = klass.new(macro, name, options, active_record) - when :composed_of - reflection = AggregateReflection.new(macro, name, options, active_record) + when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many + klass = options[:through] ? ThroughReflection : AssociationReflection + reflection = klass.new(macro, name, options, active_record) + when :composed_of + reflection = AggregateReflection.new(macro, name, options, active_record) end self.reflections = self.reflections.merge(name => reflection) @@ -44,7 +44,8 @@ module ActiveRecord # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection # def reflect_on_aggregation(aggregation) - reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil + reflection = reflections[aggregation] + reflection if reflection.is_a?(AggregateReflection) end # Returns an array of AssociationReflection objects for all the @@ -68,7 +69,8 @@ module ActiveRecord # Invoice.reflect_on_association(:line_items).macro # returns :has_many # def reflect_on_association(association) - reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil + reflection = reflections[association] + reflection if reflection.is_a?(AssociationReflection) end # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. @@ -77,7 +79,6 @@ module ActiveRecord end end - # Abstract base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. class MacroReflection @@ -152,6 +153,10 @@ module ActiveRecord # Holds all the meta-data about an aggregation as it was specified in the # Active Record class. class AggregateReflection < MacroReflection #:nodoc: + def mapping + mapping = options[:mapping] || [name, name] + mapping.first.is_a?(Array) ? mapping : [mapping] + end end # Holds all the meta-data about an association as it was specified in the @@ -452,7 +457,6 @@ module ActiveRecord # Recursively fill out the rest of the array from the through reflection conditions += through_conditions - # And return conditions end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 7531e1fe6f..05ced3299b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -6,28 +6,30 @@ module ActiveRecord # = Active Record Relation class Relation JoinOperation = Struct.new(:relation, :join_class, :on) - ASSOCIATION_METHODS = [:includes, :eager_load, :preload] - MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind, :references] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :uniq] + + MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, + :order, :joins, :where, :having, :bind, :references, + :extending] + + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, + :reverse_order, :uniq, :create_with] + + VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded - attr_accessor :extensions, :default_scoped + attr_accessor :default_scoped alias :loaded? :loaded alias :default_scoped? :default_scoped - def initialize(klass, table) - @klass, @table = klass, table - + def initialize(klass, table, values = {}) + @klass = klass + @table = table + @values = values @implicit_readonly = nil @loaded = false @default_scoped = false - - SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)} - (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} - @extensions = [] - @create_with_value = {} end def insert(values) @@ -78,7 +80,8 @@ module ActiveRecord end def initialize_copy(other) - @bind_values = @bind_values.dup + @values = @values.dup + @values[:bind] = @values[:bind].dup if @values[:bind] reset end @@ -168,23 +171,17 @@ module ActiveRecord default_scoped = with_default_scope if default_scoped.equal?(self) - @records = if @readonly_value.nil? && !@klass.locking_enabled? - eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) - else - IdentityMap.without do - eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) - end - end + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) - preload = @preload_values - preload += @includes_values unless eager_loading? + preload = preload_values + preload += includes_values unless eager_loading? preload.each do |associations| ActiveRecord::Associations::Preloader.new(@records, associations).run end # @readonly_value is true only if set explicitly. @implicit_readonly is true if there # are JOINS and no explicit SELECT. - readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value + readonly = readonly_value.nil? ? @implicit_readonly : readonly_value @records.each { |record| record.readonly! } if readonly else @records = default_scoped.to_a @@ -224,7 +221,7 @@ module ActiveRecord if block_given? to_a.many? { |*block_args| yield(*block_args) } else - @limit_value ? to_a.many? : size > 1 + limit_value ? to_a.many? : size > 1 end end @@ -239,7 +236,10 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - @klass.with_scope(self, :overwrite) { yield } + previous, klass.current_scope = klass.current_scope, self + yield + ensure + klass.current_scope = previous end # Updates all records with details given if they match a set of conditions supplied, limits and order can @@ -260,40 +260,26 @@ module ActiveRecord # Customer.update_all :wants_email => true # # # Update all books with 'Rails' in their title - # Book.update_all "author = 'David'", "title LIKE '%Rails%'" - # - # # Update all avatars migrated more than a week ago - # Avatar.update_all ['migrated_at = ?', Time.now.utc], ['migrated_at > ?', 1.week.ago] - # - # # Update all books that match conditions, but limit it to 5 ordered by date - # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5 - # - # # Conditions from the current relation also works # Book.where('title LIKE ?', '%Rails%').update_all(:author => 'David') # - # # The same idea applies to limit and order + # # Update all books that match conditions, but limit it to 5 ordered by date # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David') - def update_all(updates, conditions = nil, options = {}) - IdentityMap.repository[symbolized_base_class].clear if IdentityMap.enabled? - if conditions || options.present? - where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) - else - stmt = Arel::UpdateManager.new(arel.engine) + def update_all(updates) + stmt = Arel::UpdateManager.new(arel.engine) - stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) - stmt.table(table) - stmt.key = table[primary_key] + stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.table(table) + stmt.key = table[primary_key] - if joins_values.any? - @klass.connection.join_to_update(stmt, arel) - else - stmt.take(arel.limit) - stmt.order(*arel.orders) - stmt.wheres = arel.constraints - end - - @klass.connection.update stmt, 'SQL', bind_values + if joins_values.any? + @klass.connection.join_to_update(stmt, arel) + else + stmt.take(arel.limit) + stmt.order(*arel.orders) + stmt.wheres = arel.constraints end + + @klass.connection.update stmt, 'SQL', bind_values end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -384,17 +370,12 @@ module ActiveRecord end end - # Deletes the records matching +conditions+ without instantiating the records first, and hence not - # calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that - # goes straight to the database, much more efficient than +destroy_all+. Be careful with relations - # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns - # the number of rows affected. - # - # ==== Parameters - # - # * +conditions+ - Conditions are specified the same way as with +find+ method. - # - # ==== Example + # Deletes the records matching +conditions+ without instantiating the records + # first, and hence not calling the +destroy+ method nor invoking callbacks. This + # is a single SQL DELETE statement that goes straight to the database, much more + # efficient than +destroy_all+. Be careful with relations though, in particular + # <tt>:dependent</tt> rules defined on associations are not honored. Returns the + # number of rows affected. # # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) @@ -403,13 +384,27 @@ module ActiveRecord # Both calls delete the affected posts all at once with a single DELETE statement. # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. + # + # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error: + # + # Post.limit(100).delete_all + # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope def delete_all(conditions = nil) - IdentityMap.repository[symbolized_base_class] = {} if IdentityMap.enabled? + raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value + if conditions where(conditions).delete_all else - statement = arel.compile_delete - affected = @klass.connection.delete(statement, 'SQL', bind_values) + stmt = Arel::DeleteManager.new(arel.engine) + stmt.from(table) + + if joins_values.any? + @klass.connection.join_to_delete(stmt, arel, table[primary_key]) + else + stmt.wheres = arel.constraints + end + + affected = @klass.connection.delete(stmt, 'SQL', bind_values) reset affected @@ -437,7 +432,6 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) - IdentityMap.remove_by_id(self.symbolized_base_class, id_or_array) if IdentityMap.enabled? where(primary_key => id_or_array).delete_all end @@ -455,7 +449,7 @@ module ActiveRecord end def to_sql - @to_sql ||= klass.connection.to_sql(arel, @bind_values.dup) + @to_sql ||= klass.connection.to_sql(arel, bind_values.dup) end def where_values_hash @@ -477,8 +471,8 @@ module ActiveRecord def eager_loading? @should_eager_load ||= - @eager_load_values.any? || - @includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?) + eager_load_values.any? || + includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?) end # Joins that are also marked for preloading. In which case we should just eager load them. @@ -486,7 +480,7 @@ module ActiveRecord # represent the same association, but that aren't matched by this. Also, we could have # nested hashes which partially match, e.g. { :a => :b } & { :a => [:b, :c] } def joined_includes_values - @includes_values & @joins_values + includes_values & joins_values end def ==(other) @@ -516,6 +510,14 @@ module ActiveRecord end end + def blank? + to_a.blank? + end + + def values + @values.dup + end + private def references_eager_loaded_tables? diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 2fd89882ff..15f838a5ab 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -46,19 +46,14 @@ module ActiveRecord # group.each { |person| person.party_all_night! } # end def find_in_batches(options = {}) + options.assert_valid_keys(:start, :batch_size) + relation = self unless arel.orders.blank? && arel.taken.blank? ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end - if (finder_options = options.except(:start, :batch_size)).present? - raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order].present? - raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit].present? - - relation = apply_finder_options(finder_options) - end - start = options.delete(:start).to_i batch_size = options.delete(:batch_size) || 1000 diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 510d01085d..31d99f0192 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -3,56 +3,19 @@ require 'active_support/core_ext/object/try' module ActiveRecord module Calculations - # Count operates using three different approaches. + # Count the records. # - # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. - # * Count using column: By passing a column name to count, it will return a count of all the - # rows for the model with supplied column present. - # * Count using options will find the row count matched by the options used. + # Person.count + # # => the total count of all people # - # The third approach, count using options, accepts an option hash as the only parameter. The options are: + # Person.count(:age) + # # => returns the total count of all people whose age is present in database # - # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. - # See conditions in the intro to ActiveRecord::Base. - # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" - # (rarely needed) or named associations in the same form used for the <tt>:include</tt> option, which will - # perform an INNER JOIN on the associated table(s). If the value is a string, then the records - # will be returned read-only since they will have attributes that do not correspond to the table's columns. - # Pass <tt>:readonly => false</tt> to override. - # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. - # The symbols named refer to already defined associations. When using named associations, count - # returns the number of DISTINCT items for the model you're counting. - # See eager loading under Associations. - # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). - # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, - # want to do a join but not include the joined columns. - # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as - # SELECT COUNT(DISTINCT posts.id) ... - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an - # alternate table name (or even the name of a database view). + # Person.count(:all) + # # => performs a COUNT(*) (:all is an alias for '*') # - # Examples for counting all: - # Person.count # returns the total count of all people - # - # Examples for counting by column: - # Person.count(:age) # returns the total count of all people whose age is present in database - # - # Examples for count with options: - # Person.count(:conditions => "age > 26") - # - # # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. - # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) - # - # # finds the number of rows matching the conditions and joins. - # Person.count(:conditions => "age > 26 AND job.salary > 60000", - # :joins => "LEFT JOIN jobs on jobs.person_id = person.id") - # - # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id) - # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*') - # - # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. - # Use Person.count instead. + # Person.count(:age, distinct: true) + # # => counts the number of different age values def count(column_name = nil, options = {}) column_name, options = nil, column_name if column_name.is_a?(Hash) calculate(:count, column_name, options) @@ -98,21 +61,22 @@ module ActiveRecord end # This calculates aggregate values in the given column. Methods for count, sum, average, - # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>, - # <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query. + # minimum, and maximum have been added as shortcuts. # # There are two basic forms of output: + # # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float # for AVG, and the given column's type for everything else. - # * Grouped values: This returns an ordered hash of the values and groups them by the - # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association. # - # values = Person.maximum(:age, :group => 'last_name') + # * Grouped values: This returns an ordered hash of the values and groups them. It + # takes either a column name, or the name of a belongs_to association. + # + # values = Person.group('last_name').maximum(:age) # puts values["Drake"] # => 43 # # drake = Family.find_by_last_name('Drake') - # values = Person.maximum(:age, :group => :family) # Person belongs_to :family + # values = Person.group(:family).maximum(:age) # Person belongs_to :family # puts values[drake] # => 43 # @@ -120,83 +84,94 @@ module ActiveRecord # ... # end # - # Options: - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. - # See conditions in the intro to ActiveRecord::Base. - # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, - # the purpose of this is to access fields on joined tables in your conditions, order, or group clauses. - # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". - # (Rarely needed). - # The records will be returned read-only since they will have attributes that do not correspond to the - # table's columns. - # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). - # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example - # want to do a join, but not include the joined columns. - # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as - # SELECT COUNT(DISTINCT posts.id) ... - # # Examples: # Person.calculate(:count, :all) # The same as Person.count # Person.average(:age) # SELECT AVG(age) FROM people... - # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for - # # everyone with a last name other than 'Drake' # # # Selects the minimum age for any family without any minors - # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) + # Person.group(:last_name).having("min(age) > 17").minimum(:age) # # Person.sum("2 * age") def calculate(operation, column_name, options = {}) - if options.except(:distinct).present? - apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct]) - else - relation = with_default_scope - - if relation.equal?(self) - if eager_loading? || (includes_values.present? && references_eager_loaded_tables?) - construct_relation_for_association_calculations.calculate(operation, column_name, options) - else - perform_calculation(operation, column_name, options) - end + relation = with_default_scope + + if relation.equal?(self) + if eager_loading? || (includes_values.present? && references_eager_loaded_tables?) + construct_relation_for_association_calculations.calculate(operation, column_name, options) else - relation.calculate(operation, column_name, options) + perform_calculation(operation, column_name, options) end + else + relation.calculate(operation, column_name, options) end rescue ThrowResult 0 end - # This method is designed to perform select by a single column as direct SQL query - # Returns <tt>Array</tt> with values of the specified column name - # The values has same data type as column. + # Use <tt>pluck</tt> as a shortcut to select a single attribute without + # loading a bunch of records just to grab one attribute you want. + # + # Person.pluck(:name) + # + # instead of + # + # Person.all.map(&:name) + # + # Pluck returns an <tt>Array</tt> of attribute values type-casted to match + # the plucked column name, if it can be deduced. Plucking a SQL fragment + # returns String values by default. # # Examples: # - # Person.pluck(:id) # SELECT people.id FROM people - # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people - # Person.where(:confirmed => true).limit(5).pluck(:id) + # Person.pluck(:id) + # # SELECT people.id FROM people + # # => [1, 2, 3] + # + # Person.uniq.pluck(:role) + # # SELECT DISTINCT role FROM people + # # => ['admin', 'member', 'guest'] + # + # Person.where(:age => 21).limit(5).pluck(:id) + # # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5 + # # => [2, 3] + # + # Person.pluck('DATEDIFF(updated_at, created_at)') + # # SELECT DATEDIFF(updated_at, created_at) FROM people + # # => ['0', '27761', '173'] # def pluck(column_name) - key = column_name.to_s.split('.', 2).last - if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s) column_name = "#{table_name}.#{column_name}" end result = klass.connection.select_all(select(column_name).arel, nil, bind_values) - types = result.column_types.merge klass.column_types - column = types[key] + + key = result.columns.first + column = klass.column_types.fetch(key) { + result.column_types.fetch(key) { + Class.new { def type_cast(v); v; end }.new + } + } result.map do |attributes| - value = klass.initialize_attributes(attributes)[key] - if column - column.type_cast value - else - value - end + raise ArgumentError, "Pluck expects to select just one attribute: #{attributes.inspect}" unless attributes.one? + + value = klass.initialize_attributes(attributes).values.first + + column.type_cast(value) end end + # Pluck all the ID's for the relation using the table's primary key + # + # Examples: + # + # Person.ids # SELECT people.id FROM people + # Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id + def ids + pluck primary_key + end + private def perform_calculation(operation, column_name, options = {}) @@ -216,7 +191,7 @@ module ActiveRecord distinct = nil if column_name =~ /\s*DISTINCT\s+/i end - if @group_values.any? + if group_values.any? execute_grouped_calculation(operation, column_name, distinct) else execute_simple_calculation(operation, column_name, distinct) @@ -259,7 +234,7 @@ module ActiveRecord end def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: - group_attr = @group_values + group_attr = group_values association = @klass.reflect_on_association(group_attr.first.to_sym) associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attr) @@ -282,7 +257,7 @@ module ActiveRecord operation, distinct).as(aggregate_alias) ] - select_values += @select_values unless @having_values.empty? + select_values += select_values unless having_values.empty? select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| "#{field} AS #{aliaz}" @@ -299,7 +274,7 @@ module ActiveRecord key_records = Hash[key_records.map { |r| [r.id, r] }] end - ActiveSupport::OrderedHash[calculated_data.map do |row| + Hash[calculated_data.map do |row| key = group_columns.map { |aliaz, column| type_cast_calculated_value(row[aliaz], column) } @@ -347,8 +322,8 @@ module ActiveRecord end def select_for_count - if @select_values.present? - select = @select_values.join(", ") + if select_values.present? + select = select_values.join(", ") select if select !~ /[,*]/ end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 4cd703e0a5..4fedd33d64 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -3,83 +3,24 @@ require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord module FinderMethods - # Find operates with four different retrieval approaches: - # - # * Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). - # If no record can be found for all of the listed ids, then RecordNotFound will be raised. - # * Find first - This will return the first record matched by the options used. These options can either be specific - # conditions or merely an order. If no record can be matched, +nil+ is returned. Use - # <tt>Model.find(:first, *args)</tt> or its shortcut <tt>Model.first(*args)</tt>. - # * Find last - This will return the last record matched by the options used. These options can either be specific - # conditions or merely an order. If no record can be matched, +nil+ is returned. Use - # <tt>Model.find(:last, *args)</tt> or its shortcut <tt>Model.last(*args)</tt>. - # * Find all - This will return all the records matched by the options used. - # If no records are found, an empty array is returned. Use - # <tt>Model.find(:all, *args)</tt> or its shortcut <tt>Model.all(*args)</tt>. - # - # All approaches accept an options hash as their last parameter. - # - # ==== Options - # - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>["user_name = ?", username]</tt>, - # or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro. - # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name". - # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a - # <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause. - # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned. - # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, - # it would skip rows 0 through 4. - # * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed), - # named associations in the same form used for the <tt>:include</tt> option, which will perform an - # <tt>INNER JOIN</tt> on the associated table(s), - # or an array containing a mixture of both strings and named associations. - # If the value is a string, then the records will be returned read-only since they will - # have attributes that do not correspond to the table's columns. - # Pass <tt>:readonly => false</tt> to override. - # * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer - # to already defined associations. See eager loading under Associations. - # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, - # for example, want to do a join but not include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name"). - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed - # to an alternate table name (or even the name of a database view). - # * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated. - # * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". - # <tt>:lock => true</tt> gives connection's default exclusive lock, usually "FOR UPDATE". + # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). + # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key + # is an integer, find by id coerces its arguments using +to_i+. # # ==== Examples # - # # find by id # Person.find(1) # returns the object for ID = 1 + # Person.find("1") # returns the object for ID = 1 # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) # Person.find([1]) # returns an array for the object with ID = 1 # Person.where("administrator = 1").order("created_on DESC").find(1) # # Note that returned records may not be in the same order as the ids you - # provide since database rows are unordered. Give an explicit <tt>:order</tt> + # provide since database rows are unordered. Give an explicit <tt>order</tt> # to ensure the results are sorted. # - # ==== Examples - # - # # find first - # Person.first # returns the first object fetched by SELECT * FROM people - # Person.where(["user_name = ?", user_name]).first - # Person.where(["user_name = :u", { :u => user_name }]).first - # Person.order("created_on DESC").offset(5).first - # - # # find last - # Person.last # returns the last object fetched by SELECT * FROM people - # Person.where(["user_name = ?", user_name]).last - # Person.order("created_on DESC").offset(5).last - # - # # find all - # Person.all # returns an array of objects for all the rows fetched by SELECT * FROM people - # Person.where(["category IN (?)", categories]).limit(50).all - # Person.where({ :friends => ["Bob", "Steve", "Fred"] }).all - # Person.offset(10).limit(10).all - # Person.includes([:account, :friends]).all - # Person.group("category").all + # ==== Find with lock # # Example for find with a lock: Imagine two concurrent transactions: # each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting @@ -93,30 +34,66 @@ module ActiveRecord # person.save! # end def find(*args) - return to_a.find { |*block_args| yield(*block_args) } if block_given? - - options = args.extract_options! - - if options.present? - apply_finder_options(options).find(*args) + if block_given? + to_a.find { |*block_args| yield(*block_args) } else - case args.first - when :first, :last, :all - send(args.first) - else - find_with_ids(*args) - end + find_with_ids(*args) end end - # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the - # same arguments to this method as you can to <tt>find(:first)</tt>. - def first(*args) - if args.any? - if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) - limit(*args).to_a + # Finds the first record matching the specified conditions. There + # is no implied ording so if order matters, you should specify it + # yourself. + # + # If no record is found, returns <tt>nil</tt>. + # + # Post.find_by name: 'Spartacus', rating: 4 + # Post.find_by "published_at < ?", 2.weeks.ago + # + def find_by(*args) + where(*args).take + end + + # Like <tt>find_by</tt>, except that if no record is found, raises + # an <tt>ActiveRecord::RecordNotFound</tt> error. + def find_by!(*args) + where(*args).take! + end + + # Gives a record (or N records if a parameter is supplied) without any implied + # order. The order will depend on the database implementation. + # If an order is supplied it will be respected. + # + # Examples: + # + # Person.take # returns an object fetched by SELECT * FROM people + # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5 + # Person.where(["name LIKE '%?'", name]).take + def take(limit = nil) + limit ? limit(limit).to_a : find_take + end + + # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. Note that <tt>take!</tt> accepts no arguments. + def take! + take or raise RecordNotFound + end + + # Find the first record (or first N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # + # Examples: + # + # Person.first # returns the first object fetched by SELECT * FROM people + # Person.where(["user_name = ?", user_name]).first + # Person.where(["user_name = :u", { :u => user_name }]).first + # Person.order("created_on DESC").offset(5).first + def first(limit = nil) + if limit + if order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(limit).to_a else - apply_finder_options(args.first).first + limit(limit).to_a end else find_first @@ -129,18 +106,20 @@ module ActiveRecord first or raise RecordNotFound end - # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the - # same arguments to this method as you can to <tt>find(:last)</tt>. - def last(*args) - if args.any? - if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) - if order_values.empty? - order("#{primary_key} DESC").limit(*args).reverse - else - to_a.last(*args) - end + # Find the last record (or last N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # + # Examples: + # + # Person.last # returns the last object fetched by SELECT * FROM people + # Person.where(["user_name = ?", user_name]).last + # Person.order("created_on DESC").offset(5).last + def last(limit = nil) + if limit + if order_values.empty? && primary_key + order(arel_table[primary_key].desc).limit(limit).reverse else - apply_finder_options(args.first).last + to_a.last(limit) end else find_last @@ -153,10 +132,16 @@ module ActiveRecord last or raise RecordNotFound end - # A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the - # same arguments to this method as you can to <tt>find(:all)</tt>. - def all(*args) - args.any? ? apply_finder_options(args.first).to_a : to_a + # Examples: + # + # Person.all # returns an array of objects for all the rows fetched by SELECT * FROM people + # Person.where(["category IN (?)", categories]).limit(50).all + # Person.where({ :friends => ["Bob", "Steve", "Fred"] }).all + # Person.offset(10).limit(10).all + # Person.includes([:account, :friends]).all + # Person.group("category").all + def all + to_a end # Returns true if a record exists in the table that matches the +id+ or @@ -185,9 +170,8 @@ module ActiveRecord # Person.exists?(['name LIKE ?', "%#{query}%"]) # Person.exists? def exists?(id = false) - return false if id.nil? - id = id.id if ActiveRecord::Model === id + return false if id.nil? join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) @@ -200,7 +184,7 @@ module ActiveRecord relation = relation.where(table[primary_key].eq(id)) if id end - connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false + connection.select_value(relation, "#{name} Exists", relation.bind_values) end protected @@ -215,12 +199,12 @@ module ActiveRecord end def construct_join_dependency_for_association_find - including = (@eager_load_values + @includes_values).uniq + including = (eager_load_values + includes_values).uniq ActiveRecord::Associations::JoinDependency.new(@klass, including, []) end def construct_relation_for_association_calculations - including = (@eager_load_values + @includes_values).uniq + including = (eager_load_values + includes_values).uniq join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first) relation = except(:includes, :eager_load, :preload) apply_join_dependency(relation, join_dependency) @@ -258,44 +242,6 @@ module ActiveRecord ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end - def find_by_attributes(match, attributes, *args) - conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}] - result = where(conditions).send(match.finder) - - if match.bang? && result.blank? - raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}" - else - yield(result) if block_given? - result - end - end - - def find_or_instantiator_by_attributes(match, attributes, *args) - options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {} - protected_attributes_for_create, unprotected_attributes_for_create = {}, {} - args.each_with_index do |arg, i| - if arg.is_a?(Hash) - protected_attributes_for_create = args[i].with_indifferent_access - else - unprotected_attributes_for_create[attributes[i]] = args[i] - end - end - - conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys - - record = where(conditions).first - - unless record - record = @klass.new(protected_attributes_for_create, options) do |r| - r.assign_attributes(unprotected_attributes_for_create, :without_protection => true) - end - yield(record) if block_given? - record.save if match.instantiator == :create - end - - record - end - def find_with_ids(*ids) return to_a.find { |*block_args| yield(*block_args) } if block_given? @@ -318,21 +264,11 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id - if IdentityMap.enabled? && where_values.blank? && - limit_value.blank? && order_values.blank? && - includes_values.blank? && preload_values.blank? && - readonly_value.nil? && joins_values.blank? && - !@klass.locking_enabled? && - record = IdentityMap.get(@klass, id) - return record - end - column = columns_hash[primary_key] - - substitute = connection.substitute_at(column, @bind_values.length) + substitute = connection.substitute_at(column, bind_values.length) relation = where(table[primary_key].eq(substitute)) relation.bind_values += [[column, id]] - record = relation.first + record = relation.take unless record conditions = arel.where_sql @@ -347,15 +283,15 @@ module ActiveRecord result = where(table[primary_key].in(ids)).all expected_size = - if @limit_value && ids.size > @limit_value - @limit_value + if limit_value && ids.size > limit_value + limit_value else ids.size end # 11 ids with limit 3, offset 9 should give 2 results. - if @offset_value && (ids.size - @offset_value < expected_size) - expected_size = ids.size - @offset_value + if offset_value && (ids.size - offset_value < expected_size) + expected_size = ids.size - offset_value end if result.size == expected_size @@ -370,11 +306,24 @@ module ActiveRecord end end + def find_take + if loaded? + @records.first + else + @take ||= limit(1).to_a.first + end + end + def find_first if loaded? @records.first else - @first ||= limit(1).to_a[0] + @first ||= + if order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(1).to_a.first + else + limit(1).to_a.first + end end end @@ -386,7 +335,7 @@ module ActiveRecord if offset_value || limit_value to_a.last else - reverse_order.limit(1).to_a[0] + reverse_order.limit(1).to_a.first end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb new file mode 100644 index 0000000000..36f98c6480 --- /dev/null +++ b/activerecord/lib/active_record/relation/merger.rb @@ -0,0 +1,122 @@ +require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/keys' + +module ActiveRecord + class Relation + class HashMerger + attr_reader :relation, :hash + + def initialize(relation, hash) + hash.assert_valid_keys(*Relation::VALUE_METHODS) + + @relation = relation + @hash = hash + end + + def merge + Merger.new(relation, other).merge + end + + # Applying values to a relation has some side effects. E.g. + # interpolation might take place for where values. So we should + # build a relation to merge in rather than directly merging + # the values. + def other + other = Relation.new(relation.klass, relation.table) + hash.each { |k, v| other.send("#{k}!", v) } + other + end + end + + class Merger + attr_reader :relation, :values + + def initialize(relation, other) + if other.default_scoped? && other.klass != relation.klass + other = other.with_default_scope + end + + @relation = relation + @values = other.values + end + + def normal_values + Relation::SINGLE_VALUE_METHODS + + Relation::MULTI_VALUE_METHODS - + [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] + end + + def merge + normal_values.each do |name| + value = values[name] + relation.send("#{name}!", value) unless value.blank? + end + + merge_multi_values + merge_single_values + + relation + end + + private + + def merge_multi_values + relation.where_values = merged_wheres + relation.bind_values = merged_binds + + if values[:reordering] + # override any order specified in the original relation + relation.reorder! values[:order] + elsif values[:order] + # merge in order_values from r + relation.order! values[:order] + end + + relation.extend(*values[:extending]) unless values[:extending].blank? + end + + def merge_single_values + relation.from_value = values[:from] unless relation.from_value + relation.lock_value = values[:lock] unless relation.lock_value + relation.reverse_order_value = values[:reverse_order] + + unless values[:create_with].blank? + relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with]) + end + end + + def merged_binds + if values[:bind] + (relation.bind_values + values[:bind]).uniq(&:first) + else + relation.bind_values + end + end + + def merged_wheres + if values[:where] + merged_wheres = relation.where_values + values[:where] + + unless relation.where_values.empty? + # Remove duplicates, last one wins. + seen = Hash.new { |h,table| h[table] = {} } + merged_wheres = merged_wheres.reverse.reject { |w| + nuke = false + if w.respond_to?(:operator) && w.operator == :== + name = w.left.name + table = w.left.relation.name + nuke = seen[table][name] + seen[table][name] = true + end + nuke + }.reverse + end + + merged_wheres + else + relation.where_values + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 1d04e763f6..6a0cdd5917 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,7 +1,7 @@ module ActiveRecord class PredicateBuilder # :nodoc: def self.build_from_hash(engine, attributes, default_table) - predicates = attributes.map do |column, value| + attributes.map do |column, value| table = default_table if value.is_a?(Hash) @@ -17,50 +17,49 @@ module ActiveRecord build(table[column.to_sym], value) end - end - predicates.flatten + end.flatten end def self.references(attributes) - references = attributes.map do |key, value| + attributes.map do |key, value| if value.is_a?(Hash) key else key = key.to_s key.split('.').first.to_sym if key.include?('.') end - end - references.compact + end.compact end private def self.build(attribute, value) case value - when ActiveRecord::Relation - value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? - attribute.in(value.arel.ast) when Array, ActiveRecord::Associations::CollectionProxy values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x} - ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)} + ranges, values = values.partition {|v| v.is_a?(Range)} - array_predicates = ranges.map {|range| attribute.in(range)} - - if values.include?(nil) + values_predicate = if values.include?(nil) values = values.compact + case values.length when 0 - array_predicates << attribute.eq(nil) + attribute.eq(nil) when 1 - array_predicates << attribute.eq(values.first).or(attribute.eq(nil)) + attribute.eq(values.first).or(attribute.eq(nil)) else - array_predicates << attribute.in(values).or(attribute.eq(nil)) + attribute.in(values).or(attribute.eq(nil)) end else - array_predicates << attribute.in(values) + attribute.in(values) end - array_predicates.inject {|composite, predicate| composite.or(predicate)} - when Range, Arel::Relation + array_predicates = ranges.map { |range| attribute.in(range) } + array_predicates << values_predicate + array_predicates.inject { |composite, predicate| composite.or(predicate) } + when ActiveRecord::Relation + value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? + attribute.in(value.arel.ast) + when Range attribute.in(value) when ActiveRecord::Model attribute.eq(value.id) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 87dd513880..19fe8155d9 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -5,37 +5,67 @@ module ActiveRecord module QueryMethods extend ActiveSupport::Concern - attr_accessor :includes_values, :eager_load_values, :preload_values, - :select_values, :group_values, :order_values, :joins_values, - :where_values, :having_values, :bind_values, - :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, - :from_value, :reordering_value, :reverse_order_value, - :uniq_value, :references_values + Relation::MULTI_VALUE_METHODS.each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_values # def select_values + @values[:#{name}] || [] # @values[:select] || [] + end # end + # + def #{name}_values=(values) # def select_values=(values) + @values[:#{name}] = values # @values[:select] = values + end # end + CODE + end + + (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name| + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}_value # def readonly_value + @values[:#{name}] # @values[:readonly] + end # end + # + def #{name}_value=(value) # def readonly_value=(value) + @values[:#{name}] = value # @values[:readonly] = value + end # end + CODE + end + + def create_with_value + @values[:create_with] || {} + end + + def create_with_value=(value) + @values[:create_with] = value + end + + alias extensions extending_values def includes(*args) - args.reject! {|a| a.blank? } + args.empty? ? self : spawn.includes!(*args) + end - return self if args.empty? + def includes!(*args) + args.reject! {|a| a.blank? } - relation = clone - relation.includes_values = (relation.includes_values + args).flatten.uniq - relation + self.includes_values = (includes_values + args).flatten.uniq + self end def eager_load(*args) - return self if args.blank? + args.blank? ? self : spawn.eager_load!(*args) + end - relation = clone - relation.eager_load_values += args - relation + def eager_load!(*args) + self.eager_load_values += args + self end def preload(*args) - return self if args.blank? + args.blank? ? self : spawn.preload!(*args) + end - relation = clone - relation.preload_values += args - relation + def preload!(*args) + self.preload_values += args + self end # Used to indicate that an association is referenced by an SQL string, and should @@ -49,11 +79,12 @@ module ActiveRecord # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN def references(*args) - return self if args.blank? + args.blank? ? self : spawn.references!(*args) + end - relation = clone - relation.references_values = (references_values + args.flatten.map(&:to_s)).uniq - relation + def references!(*args) + self.references_values = (references_values + args.flatten.map(&:to_s)).uniq + self end # Works in two unique ways. @@ -87,34 +118,40 @@ module ActiveRecord # => ActiveModel::MissingAttributeError: missing attribute: other_field def select(value = Proc.new) if block_given? - to_a.select {|*block_args| value.call(*block_args) } + to_a.select { |*block_args| value.call(*block_args) } else - relation = clone - relation.select_values += Array.wrap(value) - relation + spawn.select!(value) end end + def select!(value) + self.select_values += Array.wrap(value) + self + end + def group(*args) - return self if args.blank? + args.blank? ? self : spawn.group!(*args) + end - relation = clone - relation.group_values += args.flatten - relation + def group!(*args) + self.group_values += args.flatten + self end def order(*args) - return self if args.blank? + args.blank? ? self : spawn.order!(*args) + end + def order!(*args) args = args.flatten + references = args.reject { |arg| Arel::Node === arg } .map { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 } .compact + references!(references) if references.any? - relation = clone - relation = relation.references(references) if references.any? - relation.order_values += args - relation + self.order_values += args + self end # Replaces any existing order defined on the relation with the specified order. @@ -128,72 +165,88 @@ module ActiveRecord # generates a query with 'ORDER BY id ASC, name ASC'. # def reorder(*args) - return self if args.blank? + args.blank? ? self : spawn.reorder!(*args) + end - relation = clone - relation.reordering_value = true - relation.order_values = args.flatten - relation + def reorder!(*args) + self.reordering_value = true + self.order_values = args.flatten + self end def joins(*args) - return self if args.compact.blank? - - relation = clone + args.compact.blank? ? self : spawn.joins!(*args) + end + def joins!(*args) args.flatten! - relation.joins_values += args - relation + self.joins_values += args + self end def bind(value) - relation = clone - relation.bind_values += [value] - relation + spawn.bind!(value) + end + + def bind!(value) + self.bind_values += [value] + self end def where(opts, *rest) - return self if opts.blank? + opts.blank? ? self : spawn.where!(opts, *rest) + end - relation = clone - relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts - relation.where_values += build_where(opts, rest) - relation + def where!(opts, *rest) + references!(PredicateBuilder.references(opts)) if Hash === opts + + self.where_values += build_where(opts, rest) + self end def having(opts, *rest) - return self if opts.blank? + opts.blank? ? self : spawn.having!(opts, *rest) + end - relation = clone - relation = relation.references(PredicateBuilder.references(opts)) if Hash === opts - relation.having_values += build_where(opts, rest) - relation + def having!(opts, *rest) + references!(PredicateBuilder.references(opts)) if Hash === opts + + self.having_values += build_where(opts, rest) + self end def limit(value) - relation = clone - relation.limit_value = value - relation + spawn.limit!(value) + end + + def limit!(value) + self.limit_value = value + self end def offset(value) - relation = clone - relation.offset_value = value - relation + spawn.offset!(value) + end + + def offset!(value) + self.offset_value = value + self end def lock(locks = true) - relation = clone + spawn.lock!(locks) + end + def lock!(locks = true) case locks when String, TrueClass, NilClass - relation.lock_value = locks || true + self.lock_value = locks || true else - relation.lock_value = false + self.lock_value = false end - relation + self end # Returns a chainable relation with zero records, specifically an @@ -230,21 +283,43 @@ module ActiveRecord end def readonly(value = true) - relation = clone - relation.readonly_value = value - relation + spawn.readonly!(value) + end + + def readonly!(value = true) + self.readonly_value = value + self end def create_with(value) - relation = clone - relation.create_with_value = value ? create_with_value.merge(value) : {} - relation + spawn.create_with!(value) end - def from(value) - relation = clone - relation.from_value = value - relation + def create_with!(value) + self.create_with_value = value ? create_with_value.merge(value) : {} + self + end + + # Specifies table from which the records will be fetched. For example: + # + # Topic.select('title').from('posts') + # #=> SELECT title FROM posts + # + # Can accept other relation objects. For example: + # + # Topic.select('title').from(Topics.approved) + # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery + # + # Topics.select('a.title').from(Topics.approved, :a) + # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a + # + def from(value, subquery_name = nil) + spawn.from!(value, subquery_name) + end + + def from!(value, subquery_name = nil) + self.from_value = [value, subquery_name] + self end # Specifies whether the records should be unique or not. For example: @@ -258,9 +333,12 @@ module ActiveRecord # User.select(:name).uniq.uniq(false) # # => You can also remove the uniqueness def uniq(value = true) - relation = clone - relation.uniq_value = value - relation + spawn.uniq!(value) + end + + def uniq!(value = true) + self.uniq_value = value + self end # Used to extend a scope with additional methods, either through @@ -299,20 +377,30 @@ module ActiveRecord # # pagination code goes here # end # end - def extending(*modules) - modules << Module.new(&Proc.new) if block_given? + def extending(*modules, &block) + if modules.any? || block + spawn.extending!(*modules, &block) + else + self + end + end - return self if modules.empty? + def extending!(*modules, &block) + modules << Module.new(&block) if block_given? - relation = clone - relation.send(:apply_modules, modules.flatten) - relation + self.extending_values = modules.flatten + extend(*extending_values) if extending_values.any? + + self end def reverse_order - relation = clone - relation.reverse_order_value = !relation.reverse_order_value - relation + spawn.reverse_order! + end + + def reverse_order! + self.reverse_order_value = !reverse_order_value + self end def arel @@ -322,26 +410,26 @@ module ActiveRecord def build_arel arel = table.from table - build_joins(arel, @joins_values) unless @joins_values.empty? + build_joins(arel, joins_values) unless joins_values.empty? - collapse_wheres(arel, (@where_values - ['']).uniq) + collapse_wheres(arel, (where_values - ['']).uniq) - arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty? + arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty? - arel.take(connection.sanitize_limit(@limit_value)) if @limit_value - arel.skip(@offset_value) if @offset_value + arel.take(connection.sanitize_limit(limit_value)) if limit_value + arel.skip(offset_value.to_i) if offset_value - arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? + arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty? - order = @order_values - order = reverse_sql_order(order) if @reverse_order_value + order = order_values + order = reverse_sql_order(order) if reverse_order_value arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty? - build_select(arel, @select_values.uniq) + build_select(arel, select_values.uniq) - arel.distinct(@uniq_value) - arel.from(@from_value) if @from_value - arel.lock(@lock_value) if @lock_value + arel.distinct(uniq_value) + arel.from(build_from) if from_value + arel.lock(lock_value) if lock_value arel end @@ -389,6 +477,17 @@ module ActiveRecord end end + def build_from + opts, name = from_value + case opts + when Relation + name ||= 'subquery' + opts.arel.as(name.to_s) + else + opts + end + end + def build_joins(manager, joins) buckets = joins.group_by do |join| case join @@ -443,13 +542,6 @@ module ActiveRecord end end - def apply_modules(modules) - unless modules.empty? - @extensions += modules - modules.each {|extension| extend(extension) } - end - end - def reverse_sql_order(order_query) order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 03ba8c8628..80d087a9ea 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,81 +1,42 @@ require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/slice' +require 'active_record/relation/merger' module ActiveRecord module SpawnMethods - def merge(r) - return self unless r - return to_a & r if r.is_a?(Array) - merged_relation = clone - - r = r.with_default_scope if r.default_scoped? && r.klass != klass - - Relation::ASSOCIATION_METHODS.each do |method| - value = r.send(:"#{method}_values") - - unless value.empty? - if method == :includes - merged_relation = merged_relation.includes(value) - else - merged_relation.send(:"#{method}_values=", value) - end - end - end - - (Relation::MULTI_VALUE_METHODS - [:joins, :where, :order, :binds]).each do |method| - value = r.send(:"#{method}_values") - next if value.empty? - - value += merged_relation.send(:"#{method}_values") - merged_relation.send :"#{method}_values=", value - end - - merged_relation.joins_values += r.joins_values - - merged_wheres = @where_values + r.where_values - - merged_binds = (@bind_values + r.bind_values).uniq(&:first) - - unless @where_values.empty? - # Remove duplicates, last one wins. - seen = Hash.new { |h,table| h[table] = {} } - merged_wheres = merged_wheres.reverse.reject { |w| - nuke = false - if w.respond_to?(:operator) && w.operator == :== - name = w.left.name - table = w.left.relation.name - nuke = seen[table][name] - seen[table][name] = true - end - nuke - }.reverse - end - - merged_relation.where_values = merged_wheres - merged_relation.bind_values = merged_binds - - (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with, :reordering]).each do |method| - value = r.send(:"#{method}_value") - merged_relation.send(:"#{method}_value=", value) unless value.nil? - end - - merged_relation.lock_value = r.lock_value unless merged_relation.lock_value - - merged_relation = merged_relation.create_with(r.create_with_value) unless r.create_with_value.empty? + # This is overridden by Associations::CollectionProxy + def spawn #:nodoc: + clone + end - if (r.reordering_value) - # override any order specified in the original relation - merged_relation.reordering_value = true - merged_relation.order_values = r.order_values + # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>. + # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. + # + # ==== Examples + # + # Post.where(:published => true).joins(:comments).merge( Comment.where(:spam => false) ) + # # Performs a single join query with both where conditions. + # + # recent_posts = Post.order('created_at DESC').first(5) + # Post.where(:published => true).merge(recent_posts) + # # Returns the intersection of all published posts with the 5 most recently created posts. + # # (This is just an example. You'd probably want to do this with a single query!) + # + def merge(other) + if other.is_a?(Array) + to_a & other + elsif other + spawn.merge!(other) else - # merge in order_values from r - merged_relation.order_values += r.order_values + self end + end - # Apply scope extension modules - merged_relation.send :apply_modules, r.extensions - - merged_relation + def merge!(other) + klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger + klass.new(self, other).merge end # Removes from the query the condition(s) specified in +skips+. @@ -86,20 +47,9 @@ module ActiveRecord # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order # def except(*skips) - result = self.class.new(@klass, table) + result = Relation.new(klass, table, values.except(*skips)) result.default_scoped = default_scoped - - ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method| - result.send(:"#{method}_values=", send(:"#{method}_values")) - end - - (Relation::SINGLE_VALUE_METHODS - skips).each do |method| - result.send(:"#{method}_value=", send(:"#{method}_value")) - end - - # Apply scope extension modules - result.send(:apply_modules, extensions) - + result.extend(*extending_values) if extending_values.any? result end @@ -111,44 +61,11 @@ module ActiveRecord # Post.order('id asc').only(:where, :order) # uses the specified order # def only(*onlies) - result = self.class.new(@klass, table) + result = Relation.new(klass, table, values.slice(*onlies)) result.default_scoped = default_scoped - - ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) & onlies).each do |method| - result.send(:"#{method}_values=", send(:"#{method}_values")) - end - - (Relation::SINGLE_VALUE_METHODS & onlies).each do |method| - result.send(:"#{method}_value=", send(:"#{method}_value")) - end - - # Apply scope extension modules - result.send(:apply_modules, extensions) - + result.extend(*extending_values) if extending_values.any? result end - VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :extend, :references, - :order, :select, :readonly, :group, :having, :from, :lock ] - - def apply_finder_options(options) - relation = clone - return relation unless options - - options.assert_valid_keys(VALID_FIND_OPTIONS) - finders = options.dup - finders.delete_if { |key, value| value.nil? && key != :limit } - - ((VALID_FIND_OPTIONS - [:conditions, :include, :extend]) & finders.keys).each do |finder| - relation = relation.send(finder, finders[finder]) - end - - relation = relation.where(finders[:conditions]) if options.has_key?(:conditions) - relation = relation.includes(finders[:include]) if options.has_key?(:include) - relation = relation.extending(finders[:extend]) if options.has_key?(:extend) - - relation - end - end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index fb4b89b87b..fd276ccf5d 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -28,6 +28,7 @@ module ActiveRecord alias :map! :map alias :collect! :map + # Returns true if there are no records. def empty? rows.empty? end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 2d7d83d160..5530be3219 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -37,9 +37,9 @@ module ActiveRecord # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'" def sanitize_sql_for_assignment(assignments) case assignments - when Array; sanitize_sql_array(assignments) - when Hash; sanitize_sql_hash_for_assignment(assignments) - else assignments + when Array; sanitize_sql_array(assignments) + when Hash; sanitize_sql_hash_for_assignment(assignments) + else assignments end end @@ -57,8 +57,8 @@ module ActiveRecord def expand_hash_conditions_for_aggregates(attrs) expanded_attrs = {} attrs.each do |attr, value| - unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil? - mapping = aggregate_mapping(aggregation) + if aggregation = reflect_on_aggregation(attr.to_sym) + mapping = aggregation.mapping mapping.each do |field_attr, aggregate_attr| if mapping.size == 1 && !value.respond_to?(aggregate_attr) expanded_attrs[field_attr] = value diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index d815ab05ac..599e68379a 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -34,6 +34,15 @@ module ActiveRecord ActiveRecord::Migrator.migrations_paths end + def define(info, &block) + instance_eval(&block) + + unless info[:version].blank? + initialize_schema_migrations_table + assume_migrated_upto_version(info[:version], migrations_paths) + end + end + # Eval the given block. All methods available to the current connection # adapter are available within the block, so you can easily use the # database definition DSL to build up your schema (+create_table+, @@ -46,13 +55,7 @@ module ActiveRecord # ... # end def self.define(info={}, &block) - schema = new - schema.instance_eval(&block) - - unless info[:version].blank? - initialize_schema_migrations_table - assume_migrated_upto_version(info[:version], schema.migrations_paths) - end + new.define(info, &block) end end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index dcbd165e58..7cbe2db408 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -40,7 +40,7 @@ module ActiveRecord def header(stream) define_params = @version ? ":version => #{@version}" : "" - if stream.respond_to?(:external_encoding) + if stream.respond_to?(:external_encoding) && stream.external_encoding stream.puts "# encoding: #{stream.external_encoding.name}" end @@ -55,7 +55,7 @@ module ActiveRecord # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # -# It's strongly recommended to check this file into your version control system. +# It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(#{define_params}) do diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index a8f5e96190..66a486ae0a 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -10,118 +10,6 @@ module ActiveRecord end module ClassMethods - # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be - # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while - # <tt>:create</tt> parameters are an attributes hash. - # - # class Article < ActiveRecord::Base - # def self.create_with_scope - # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do - # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 - # a = create(1) - # a.blog_id # => 1 - # end - # end - # end - # - # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of - # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged. - # - # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing - # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the - # array of strings format for your joins. - # - # class Article < ActiveRecord::Base - # def self.find_with_scope - # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do - # with_scope(:find => limit(10)) do - # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 - # end - # with_scope(:find => where(:author_id => 3)) do - # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 - # end - # end - # end - # end - # - # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method. - # - # class Article < ActiveRecord::Base - # def self.find_with_exclusive_scope - # with_scope(:find => where(:blog_id => 1).limit(1)) do - # with_exclusive_scope(:find => limit(10)) do - # all # => SELECT * from articles LIMIT 10 - # end - # end - # end - # end - # - # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. - def with_scope(scope = {}, action = :merge, &block) - # If another Active Record class has been passed in, get its current scope - scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope) - - previous_scope = self.current_scope - - if scope.is_a?(Hash) - # Dup first and second level of hash (method and params). - scope = scope.dup - scope.each do |method, params| - scope[method] = params.dup unless params == true - end - - scope.assert_valid_keys([ :find, :create ]) - relation = construct_finder_arel(scope[:find] || {}) - relation.default_scoped = true unless action == :overwrite - - if previous_scope && previous_scope.create_with_value && scope[:create] - scope_for_create = if action == :merge - previous_scope.create_with_value.merge(scope[:create]) - else - scope[:create] - end - - relation = relation.create_with(scope_for_create) - else - scope_for_create = scope[:create] - scope_for_create ||= previous_scope.create_with_value if previous_scope - relation = relation.create_with(scope_for_create) if scope_for_create - end - - scope = relation - end - - scope = previous_scope.merge(scope) if previous_scope && action == :merge - - self.current_scope = scope - begin - yield - ensure - self.current_scope = previous_scope - end - end - - protected - - # Works like with_scope, but discards any nested properties. - def with_exclusive_scope(method_scoping = {}, &block) - if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) } - raise ArgumentError, <<-MSG - New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope: - - User.unscoped.where(:active => true) - - Or call unscoped with a block: - - User.unscoped do - User.where(:active => true).all - end - - MSG - end - with_scope(method_scoping, :overwrite, &block) - end - def current_scope #:nodoc: Thread.current["#{self}_current_scope"] end @@ -129,15 +17,6 @@ module ActiveRecord def current_scope=(scope) #:nodoc: Thread.current["#{self}_current_scope"] = scope end - - private - - def construct_finder_arel(options = {}, scope = nil) - relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options - relation = scope.merge(relation) if scope - relation - end - end def populate_with_current_scope_attributes diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 5f05d146f2..db833fc7f1 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -1,4 +1,5 @@ require 'active_support/concern' +require 'active_support/deprecation' module ActiveRecord module Scoping @@ -30,7 +31,7 @@ module ActiveRecord # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } # - # It is recommended to use the block form of unscoped because chaining + # It is recommended that you use the block form of unscoped because chaining # unscoped with <tt>scope</tt> does not work. Assuming that # <tt>published</tt> is a <tt>scope</tt>, the following two statements # are equal: the default_scope is applied on both. @@ -51,7 +52,7 @@ module ActiveRecord # the model. # # class Article < ActiveRecord::Base - # default_scope where(:published => true) + # default_scope { where(:published => true) } # end # # Article.all # => SELECT * FROM articles WHERE published = true @@ -62,12 +63,6 @@ module ActiveRecord # Article.new.published # => true # Article.create.published # => true # - # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: - # - # class Article < ActiveRecord::Base - # default_scope { where(:published_at => Time.now - 1.week) } - # end - # # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> # macro, and it will be called when building the default scope.) # @@ -75,8 +70,8 @@ module ActiveRecord # be merged together: # # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # default_scope where(:rating => 'G') + # default_scope { where(:published => true) } + # default_scope { where(:rating => 'G') } # end # # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' @@ -92,21 +87,30 @@ module ActiveRecord # # Should return a scope, you can call 'super' here etc. # end # end - def default_scope(scope = {}) + def default_scope(scope = nil) scope = Proc.new if block_given? + + if scope.is_a?(Relation) || !scope.respond_to?(:call) + ActiveSupport::Deprecation.warn( + "Calling #default_scope without a block is deprecated. For example instead " \ + "of `default_scope where(color: 'red')`, please use " \ + "`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \ + "self.default_scope.)" + ) + end + self.default_scopes = default_scopes + [scope] end def build_default_scope #:nodoc: - if method(:default_scope).owner != ActiveRecord::Scoping::Default::ClassMethods + if !Base.is_a?(method(:default_scope).owner) + # The user has defined their own default scope method, so call that evaluate_default_scope { default_scope } elsif default_scopes.any? evaluate_default_scope do default_scopes.inject(relation) do |default_scope, scope| - if scope.is_a?(Hash) - default_scope.apply_finder_options(scope) - elsif !scope.is_a?(Relation) && scope.respond_to?(:call) - default_scope.merge(scope.call) + if !scope.is_a?(Relation) && scope.respond_to?(:call) + default_scope.merge(unscoped { scope.call }) else default_scope.merge(scope) end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 0edc3f1dcc..2af476c1ba 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/class/attribute' +require 'active_support/deprecation' module ActiveRecord # = Active Record Named \Scopes @@ -28,17 +29,16 @@ module ActiveRecord # You can define a \scope that applies to all finders using # ActiveRecord::Base.default_scope. def scoped(options = nil) - if options - scoped.apply_finder_options(options) + if current_scope + scope = current_scope.clone else - if current_scope - current_scope.clone - else - scope = relation.clone - scope.default_scoped = true - scope - end + scope = relation + scope.default_scoped = true + scope end + + scope.merge!(options) if options + scope end ## @@ -48,7 +48,7 @@ module ActiveRecord if current_scope current_scope.scope_for_create else - scope = relation.clone + scope = relation scope.default_scoped = true scope.scope_for_create end @@ -171,30 +171,28 @@ module ActiveRecord # Article.published.featured.latest_article # Article.featured.titles - def scope(name, scope_options = {}) - name = name.to_sym - valid_scope_name?(name) - extension = Module.new(&Proc.new) if block_given? + def scope(name, body, &block) + extension = Module.new(&block) if block - scope_proc = lambda do |*args| - options = scope_options.respond_to?(:call) ? unscoped { scope_options.call(*args) } : scope_options - options = scoped.apply_finder_options(options) if options.is_a?(Hash) + # Check body.is_a?(Relation) to prevent the relation actually being + # loaded by respond_to? + if body.is_a?(Relation) || !body.respond_to?(:call) + ActiveSupport::Deprecation.warn( + "Using #scope without passing a callable object is deprecated. For " \ + "example `scope :red, where(color: 'red')` should be changed to " \ + "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \ + "in the former usage and it makes the implementation more complicated " \ + "and buggy. (If you prefer, you can just define a class method named " \ + "`self.red`.)" + ) + end + singleton_class.send(:define_method, name) do |*args| + options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body relation = scoped.merge(options) extension ? relation.extending(extension) : relation end - - singleton_class.send(:redefine_method, name, &scope_proc) - end - - protected - - def valid_scope_name?(name) - if respond_to?(name, true) - logger.warn "Creating scope :#{name}. " \ - "Overwriting existing method #{self.name}.#{name}." - end end end end diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 2e60521638..b833af64fe 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -18,8 +18,8 @@ module ActiveRecord #:nodoc: # <id type="integer">1</id> # <approved type="boolean">false</approved> # <replies-count type="integer">0</replies-count> - # <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="datetime">2003-07-16T09:28:00+1200</written-on> + # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> + # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> # <content>Have a nice day</content> # <author-email-address>david@loudthinking.com</author-email-address> # <parent-id></parent-id> diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index ce43ae8066..5a256b040b 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -119,7 +119,7 @@ module ActiveRecord class << self; remove_possible_method :find_by_session_id; end def self.find_by_session_id(session_id) - find :first, :conditions => {:session_id=>session_id} + where(session_id: session_id).first end end end @@ -201,10 +201,10 @@ module ActiveRecord class << self alias :data_column_name :data_column - + # Use the ActiveRecord::Base.connection by default. attr_writer :connection - + # Use the ActiveRecord::Base.connection_pool by default. attr_writer :connection_pool @@ -218,12 +218,12 @@ module ActiveRecord # Look up a session by id and unmarshal its data if found. def find_by_session_id(session_id) - if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id)}") + if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id.to_s)}") new(:session_id => session_id, :marshaled_data => record['data']) end end end - + delegate :connection, :connection=, :connection_pool, :connection_pool=, :to => self attr_reader :session_id, :new_record @@ -241,6 +241,11 @@ module ActiveRecord @new_record = @marshaled_data.nil? end + # Returns true if the record is persisted, i.e. it's not a new record + def persisted? + !@new_record + end + # Lazy-unmarshal session state. def data unless @data @@ -287,7 +292,7 @@ module ActiveRecord connect = connection connect.delete <<-end_sql, 'Destroy session' DELETE FROM #{table_name} - WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} + WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id.to_s)} end_sql end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 1c7b839e5e..fdd82b489a 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/indifferent_access' + module ActiveRecord # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. # It's like a simple key/value store backed into your record when you don't care about being able to @@ -10,16 +12,23 @@ module ActiveRecord # Make sure that you declare the database column used for the serialized store as a text, so there's # plenty of room. # + # You can set custom coder to encode/decode your serialized attributes to/from different formats. + # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. + # # Examples: # # class User < ActiveRecord::Base - # store :settings, accessors: [ :color, :homepage ] + # store :settings, accessors: [ :color, :homepage ], coder: JSON # end # # u = User.new(color: 'black', homepage: '37signals.com') # u.color # Accessor stored attribute # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor # + # # There is no difference between strings and symbols for accessing custom attributes + # u.settings[:country] # => 'Denmark' + # u.settings['country'] # => 'Denmark' + # # # Add additional accessors to an existing store through store_accessor # class SuperUser < User # store_accessor :settings, :privileges, :servants @@ -29,24 +38,38 @@ module ActiveRecord module ClassMethods def store(store_attribute, options = {}) - serialize store_attribute, Hash + serialize store_attribute, options.fetch(:coder, ActiveSupport::HashWithIndifferentAccess) store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors end def store_accessor(store_attribute, *keys) keys.flatten.each do |key| define_method("#{key}=") do |value| - send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) + initialize_store_attribute(store_attribute) send(store_attribute)[key] = value send("#{store_attribute}_will_change!") end define_method(key) do - send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) + initialize_store_attribute(store_attribute) send(store_attribute)[key] end end end end + + private + def initialize_store_attribute(store_attribute) + case attribute = send(store_attribute) + when ActiveSupport::HashWithIndifferentAccess + # Already initialized. Do nothing. + when Hash + # Initialized as a Hash. Convert to indifferent access. + send :"#{store_attribute}=", attribute.with_indifferent_access + else + # Uninitialized. Set to an indifferent hash. + send :"#{store_attribute}=", ActiveSupport::HashWithIndifferentAccess.new + end + end end end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 4d881f0f7d..fcaa4b74a6 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -7,20 +7,10 @@ module ActiveRecord # # Defines some test assertions to test against SQL queries. class TestCase < ActiveSupport::TestCase #:nodoc: - setup :cleanup_identity_map - - def setup - cleanup_identity_map - end - def teardown SQLCounter.log.clear end - def cleanup_identity_map - ActiveRecord::IdentityMap.clear - end - def assert_date_from_db(expected, actual, message = nil) # SybaseAdapter doesn't have a separate column type just for dates, # so the time is in the string and incorrectly formatted diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index b492377d18..9cb9b4627b 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -251,7 +251,6 @@ module ActiveRecord remember_transaction_record_state yield rescue Exception - IdentityMap.remove(self) if IdentityMap.enabled? restore_transaction_record_state raise ensure @@ -270,7 +269,6 @@ module ActiveRecord def rolledback!(force_restore_state = false) #:nodoc: run_callbacks :rollback ensure - IdentityMap.remove(self) if IdentityMap.enabled? restore_transaction_record_state(force_restore_state) end @@ -292,7 +290,15 @@ module ActiveRecord status = nil self.class.transaction do add_to_transaction - status = yield + begin + status = yield + rescue ActiveRecord::Rollback + if defined?(@_start_transaction_state) + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + end + status = nil + end + raise ActiveRecord::Rollback unless status end status @@ -304,12 +310,8 @@ module ActiveRecord def remember_transaction_record_state #:nodoc: @_start_transaction_state ||= {} @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) - unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @destroyed - end + @_start_transaction_state[:new_record] = @new_record + @_start_transaction_state[:destroyed] = @destroyed @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 end @@ -327,7 +329,8 @@ module ActiveRecord @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 restore_state = remove_instance_variable(:@_start_transaction_state) - @attributes = @attributes.dup if @attributes.frozen? + was_frozen = @attributes.frozen? + @attributes = @attributes.dup if was_frozen @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] if restore_state.has_key?(:id) @@ -336,6 +339,7 @@ module ActiveRecord @attributes.delete(self.class.primary_key) @attributes_cache.delete(self.class.primary_key) end + @attributes.freeze if was_frozen end end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 4b075183c3..d06020b3ce 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -14,7 +14,7 @@ module ActiveRecord def initialize(record) @record = record errors = @record.errors.full_messages.join(", ") - super(I18n.t("activerecord.errors.messages.record_invalid", :errors => errors)) + super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid")) end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 9556878f63..9e4b588ac2 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -35,8 +35,14 @@ module ActiveRecord relation = relation.and(table[scope_item].eq(scope_value)) end - if finder_class.unscoped.where(relation).exists? - record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value)) + relation = finder_class.unscoped.where(relation) + + if options[:conditions] + relation = relation.merge(options[:conditions]) + end + + if relation.exists? + record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope, :conditions).merge(:value => value)) end end @@ -102,6 +108,14 @@ module ActiveRecord # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id] # end # + # It is also possible to limit the uniqueness constraint to a set of records matching certain conditions. + # In this example archived articles are not being taken into consideration when validating uniqueness + # of the title attribute: + # + # class Article < ActiveRecord::Base + # validates_uniqueness_of :title, :conditions => where('status != ?', 'archived') + # end + # # When the record is created, a check is performed to make sure that no record exists in the database # with the given value for the specified attribute (that maps to a column). When the record is updated, # the same check is made but disregarding the record itself. @@ -109,6 +123,8 @@ module ActiveRecord # Configuration options: # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken"). # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint. + # * <tt>:conditions</tt> - Specify the conditions to be included as a <tt>WHERE</tt> SQL fragment to limit + # the uniqueness constraint lookup. (e.g. <tt>:conditions => where('status = ?', 'active')</tt>) # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). @@ -180,7 +196,6 @@ module ActiveRecord # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception: # * ActiveRecord::ConnectionAdapters::MysqlAdapter # * ActiveRecord::ConnectionAdapters::Mysql2Adapter - # * ActiveRecord::ConnectionAdapters::SQLiteAdapter # * ActiveRecord::ConnectionAdapters::SQLite3Adapter # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter # diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index d084a00ed7..b1a0f83b5f 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -5,17 +5,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> <%- if attribute.has_index? -%> add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> - <%- end %> + <%- end -%> <%- end -%> end <%- else -%> def up <% attributes.each do |attribute| -%> <%- if migration_action -%> - <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> - <% if attribute.has_index? && migration_action == 'add' %> - add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> - <% end -%> + <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %> <%- end -%> <%- end -%> end @@ -23,7 +20,10 @@ class <%= migration_class_name %> < ActiveRecord::Migration def down <% attributes.reverse.each do |attribute| -%> <%- if migration_action -%> - <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> + add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- if attribute.has_index? -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> <%- end -%> <%- end -%> end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 99a022461e..8e6ef20285 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -14,6 +14,7 @@ module ActiveRecord def create_migration_file return unless options[:migration] && options[:parent].nil? + attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false migration_template "migration.rb", "db/migrate/create_#{table_name}.rb" end @@ -27,7 +28,11 @@ module ActiveRecord end def attributes_with_index - attributes.select { |a| a.has_index? || (a.reference? && options[:indexes]) } + attributes.select { |a| !a.reference? && a.has_index? } + end + + def accessible_attributes + attributes.reject(&:reference?) end hook_for :test_framework diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 5c47f8b241..d56f9f57a4 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -3,5 +3,10 @@ class <%= class_name %> < <%= parent_class_name.classify %> <% attributes.select {|attr| attr.reference? }.each do |attribute| -%> belongs_to :<%= attribute.name %> <% end -%> +<% if !accessible_attributes.empty? -%> + attr_accessible <%= accessible_attributes.map {|a| ":#{a.name}" }.sort.join(', ') %> +<% else -%> + # attr_accessible :title, :body +<% end -%> end <% end -%> diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index fa2ba8d592..5e1c52c9ba 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -120,6 +120,19 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_mysql_default_in_strict_mode + result = @connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [["STRICT_ALL_TABLES"]], result.rows + end + + def test_mysql_strict_mode_disabled + run_without_connection do |orig_connection| + ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false})) + result = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows + end + end + private def run_without_connection diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 7fe2c02c04..475a292f85 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -46,6 +46,46 @@ module ActiveRecord assert_equal str, value end + def test_tables_quoting + begin + @conn.tables(nil, "foo-bar", nil) + flunk + rescue => e + # assertion for *quoted* database properly + assert_match(/database 'foo-bar'/, e.inspect) + end + end + + def test_pk_and_sequence_for + pk, seq = @conn.pk_and_sequence_for('ex') + assert_equal 'id', pk + assert_equal @conn.default_sequence_name('ex', 'id'), seq + end + + def test_pk_and_sequence_for_with_non_standard_primary_key + @conn.exec_query('drop table if exists ex_with_non_standard_pk') + @conn.exec_query(<<-eosql) + CREATE TABLE `ex_with_non_standard_pk` ( + `code` INT(11) DEFAULT NULL auto_increment, + PRIMARY KEY (`code`)) + eosql + pk, seq = @conn.pk_and_sequence_for('ex_with_non_standard_pk') + assert_equal 'code', pk + assert_equal @conn.default_sequence_name('ex_with_non_standard_pk', 'code'), seq + end + + def test_pk_and_sequence_for_with_custom_index_type_pk + @conn.exec_query('drop table if exists ex_with_custom_index_type_pk') + @conn.exec_query(<<-eosql) + CREATE TABLE `ex_with_custom_index_type_pk` ( + `id` INT(11) DEFAULT NULL auto_increment, + PRIMARY KEY USING BTREE (`id`)) + eosql + pk, seq = @conn.pk_and_sequence_for('ex_with_custom_index_type_pk') + assert_equal 'id', pk + assert_equal @conn.default_sequence_name('ex_with_custom_index_type_pk', 'id'), seq + end + private def insert(ctx, data) binds = data.map { |name, value| diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 292c7efebb..6faceaf7c0 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -130,7 +130,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase end def test_associations_work_with_reserved_words - assert_nothing_raised { Select.find(:all, :include => [:groups]) } + assert_nothing_raised { Select.scoped(:includes => [:groups]).all } end #the following functions were added to DRY test cases diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index 29f885c6e7..d94bb629a7 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -20,7 +20,7 @@ module ActiveRecord end def test_schema - assert @omgpost.find(:first) + assert @omgpost.first end def test_primary_key diff --git a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb index cd9c1041dc..5e8065d80d 100644 --- a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb @@ -9,7 +9,7 @@ module ActiveRecord def test_update_question_marks str = "foo?bar" - x = Topic.find :first + x = Topic.first x.title = str x.content = str x.save! @@ -28,7 +28,7 @@ module ActiveRecord def test_update_null_bytes str = "foo\0bar" - x = Topic.find :first + x = Topic.first x.title = str x.content = str x.save! diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 8e2b9ca9a5..684c7f5929 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -29,6 +29,22 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert @connection.active? end + # TODO: Below is a straight up copy/paste from mysql/connection_test.rb + # I'm not sure what the correct way is to share these tests between + # adapters in minitest. + def test_mysql_default_in_strict_mode + result = @connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [["STRICT_ALL_TABLES"]], result.rows + end + + def test_mysql_strict_mode_disabled + run_without_connection do |orig_connection| + ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false})) + result = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows + end + end + private def run_without_connection diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 3a9744e78f..32d4282623 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -130,7 +130,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase end def test_associations_work_with_reserved_words - assert_nothing_raised { Select.find(:all, :include => [:groups]) } + assert_nothing_raised { Select.scoped(:includes => [:groups]).all } end #the following functions were added to DRY test cases diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index d5676bc522..2c0ed73c92 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -20,7 +20,7 @@ module ActiveRecord end def test_schema - assert @omgpost.find(:first) + assert @omgpost.first end def test_primary_key @@ -35,6 +35,17 @@ module ActiveRecord def test_table_exists_wrong_schema assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") end + + def test_tables_quoting + begin + @connection.tables(nil, "foo-bar", nil) + flunk + rescue => e + # assertion for *quoted* database properly + assert_match(/database 'foo-bar'/, e.inspect) + end + end + end end end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index ce08e4c6a7..34660577da 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -86,9 +86,9 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_data_type_of_network_address_types - assert_equal :string, @first_network_address.column_for_attribute(:cidr_address).type - assert_equal :string, @first_network_address.column_for_attribute(:inet_address).type - assert_equal :string, @first_network_address.column_for_attribute(:mac_address).type + assert_equal :cidr, @first_network_address.column_for_attribute(:cidr_address).type + assert_equal :inet, @first_network_address.column_for_attribute(:inet_address).type + assert_equal :macaddr, @first_network_address.column_for_attribute(:mac_address).type end def test_data_type_of_bit_string_types @@ -134,9 +134,12 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal '-1 years -2 days', @first_time.time_interval end - def test_network_address_values - assert_equal '192.168.0.0/24', @first_network_address.cidr_address - assert_equal '172.16.1.254', @first_network_address.inet_address + def test_network_address_values_ipaddr + cidr_address = IPAddr.new '192.168.0.0/24' + inet_address = IPAddr.new '172.16.1.254' + + assert_equal cidr_address, @first_network_address.cidr_address + assert_equal inet_address, @first_network_address.inet_address assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address end @@ -200,8 +203,8 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end def test_update_network_address - new_cidr_address = '10.1.2.3/32' - new_inet_address = '10.0.0.0/8' + new_inet_address = '10.1.2.3/32' + new_cidr_address = '10.0.0.0/8' new_mac_address = 'bc:de:f0:12:34:56' assert @first_network_address.cidr_address = new_cidr_address assert @first_network_address.inet_address = new_inet_address diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 0b61f61572..619d581d5f 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -22,6 +22,13 @@ module ActiveRecord assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain assert_match %(Seq Scan on audit_logs), explain end + + def test_dont_explain_for_set_search_path + queries = Thread.current[:available_queries_for_explain] = [] + ActiveRecord::Base.connection.schema_search_path = "public" + assert queries.empty? + end + end end end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 1644a58d92..23bafde17b 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -1,3 +1,5 @@ +# encoding: utf-8 + require "cases/helper" require 'active_record/base' require 'active_record/connection_adapters/postgresql_adapter' @@ -88,7 +90,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_rewrite @connection.execute "insert into hstores (tags) VALUES ('1=>2')" - x = Hstore.find :first + x = Hstore.first x.tags = { '"a\'' => 'b' } assert x.save! end @@ -96,13 +98,13 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_select @connection.execute "insert into hstores (tags) VALUES ('1=>2')" - x = Hstore.find :first + x = Hstore.first assert_equal({'1' => '2'}, x.tags) end def test_select_multikey @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')" - x = Hstore.find :first + x = Hstore.first assert_equal({'1' => '2', '2' => '3'}, x.tags) end @@ -134,13 +136,19 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_cycle('a=>b' => 'bar', '1"foo' => '2') end + def test_quoting_special_characters + assert_cycle('ca' => 'cà', 'ac' => 'àc') + end + private def assert_cycle hash + # test creation x = Hstore.create!(:tags => hash) x.reload assert_equal(hash, x.tags) - # make sure updates work + # test updating + x = Hstore.create!(:tags => {}) x.tags = hash x.save! x.reload diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index a71d0bb848..92e31a3e44 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -49,6 +49,33 @@ module ActiveRecord assert_equal expect, id end + def test_insert_sql_with_returning_disabled + connection = connection_without_insert_returning + id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)") + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect, id + end + + def test_exec_insert_with_returning_disabled + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq') + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect, result.rows.first.first + end + + def test_exec_insert_with_returning_disabled_and_no_sequence_name_given + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id') + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect, result.rows.first.first + end + + def test_sql_for_insert_with_returning_disabled + connection = connection_without_insert_returning + result = connection.sql_for_insert('sql', nil, nil, nil, 'binds') + assert_equal ['sql', 'binds'], result + end + def test_serial_sequence assert_equal 'public.accounts_id_seq', @connection.serial_sequence('accounts', 'id') @@ -204,6 +231,10 @@ module ActiveRecord ctx.exec_insert(sql, 'SQL', binds) end + + def connection_without_insert_returning + ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false)) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 172055f15c..f8a605b67c 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -19,6 +19,18 @@ module ActiveRecord assert_equal 'f', @conn.type_cast(false, nil) assert_equal 'f', @conn.type_cast(false, c) end + + def test_quote_float_nan + nan = 0.0/0 + c = Column.new(nil, 1, 'float') + assert_equal "'NaN'", @conn.quote(nan, c) + end + + def test_quote_float_infinity + infinity = 1.0/0 + c = Column.new(nil, 1, 'float') + assert_equal "'Infinity'", @conn.quote(infinity, c) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 18c81d2b09..9208f53997 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -71,6 +71,45 @@ class SchemaTest < ActiveRecord::TestCase @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE" end + def test_schema_names + assert_equal ["public", "test_schema", "test_schema2"], @connection.schema_names + end + + def test_create_schema + begin + @connection.create_schema "test_schema3" + assert @connection.schema_names.include? "test_schema3" + ensure + @connection.drop_schema "test_schema3" + end + end + + def test_raise_create_schema_with_existing_schema + begin + @connection.create_schema "test_schema3" + assert_raises(ActiveRecord::StatementInvalid) do + @connection.create_schema "test_schema3" + end + ensure + @connection.drop_schema "test_schema3" + end + end + + def test_drop_schema + begin + @connection.create_schema "test_schema3" + ensure + @connection.drop_schema "test_schema3" + end + assert !@connection.schema_names.include?("test_schema3") + end + + def test_raise_drop_schema_with_nonexisting_schema + assert_raises(ActiveRecord::StatementInvalid) do + @connection.drop_schema "test_schema3" + end + end + def test_schema_change_with_prepared_stmt altered = false @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]] @@ -183,13 +222,13 @@ class SchemaTest < ActiveRecord::TestCase end def test_raise_on_unquoted_schema_name - assert_raise(ActiveRecord::StatementInvalid) do + assert_raises(ActiveRecord::StatementInvalid) do with_schema_search_path '$user,public' end end def test_without_schema_search_path - assert_raise(ActiveRecord::StatementInvalid) { columns(TABLE_NAME) } + assert_raises(ActiveRecord::StatementInvalid) { columns(TABLE_NAME) } end def test_ignore_nil_schema_search_path diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 575b4806c1..7eef4ace81 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -57,6 +57,14 @@ class CopyTableTest < ActiveRecord::TestCase end end + def test_copy_table_with_unconventional_primary_key + test_copy_table('owners', 'owners_unconventional') do |from, to, options| + original_pk = @connection.primary_key('owners') + copied_pk = @connection.primary_key('owners_unconventional') + assert_equal original_pk, copied_pk + end + end + protected def copy_table(from, to, options = {}) @connection.copy_table(from, to, {:temporary => true}.merge(options)) diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 46da1b0a2b..2ba9143cd5 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -1,10 +1,11 @@ require "cases/helper" require 'bigdecimal' require 'yaml' +require 'securerandom' module ActiveRecord module ConnectionAdapters - class SQLiteAdapter + class SQLite3Adapter class QuotingTest < ActiveRecord::TestCase def setup @conn = Base.sqlite3_connection :database => ':memory:', @@ -12,6 +13,14 @@ module ActiveRecord :timeout => 100 end + def test_type_cast_binary_encoding_without_logger + @conn.extend(Module.new { def logger; end }) + column = Struct.new(:type, :name).new(:string, "foo") + binary = SecureRandom.hex + expected = binary.dup.encode!('utf-8') + assert_equal expected, @conn.type_cast(binary, column) + end + def test_type_cast_symbol assert_equal 'foo', @conn.type_cast(:foo, nil) end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 17bde6cb62..8a7f44d0a3 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -35,6 +35,11 @@ module ActiveRecord assert(!result.rows.first.include?("blob"), "should not store blobs") end + def test_time_column + owner = Owner.create!(:eats_at => Time.utc(1995,1,1,6,0)) + assert_match(/1995-01-01/, owner.reload.eats_at.to_s) + end + def test_exec_insert column = @conn.columns('items').find { |col| col.name == 'number' } vals = [[column, 10]] @@ -58,18 +63,6 @@ module ActiveRecord end end - def test_connection_no_adapter - assert_raises(ArgumentError) do - Base.sqlite3_connection :database => ':memory:' - end - end - - def test_connection_wrong_adapter - assert_raises(ArgumentError) do - Base.sqlite3_connection :database => ':memory:',:adapter => 'vuvuzela' - end - end - def test_bad_timeout assert_raises(TypeError) do Base.sqlite3_connection :database => ':memory:', diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index ae272e2c4b..2f04c60a9a 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' module ActiveRecord::ConnectionAdapters - class SQLiteAdapter + class SQLite3Adapter class StatementPoolTest < ActiveRecord::TestCase def test_cache_is_per_pid return skip('must support fork') unless Process.respond_to?(:fork) diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index 3e0e6dce2c..5bd8f76ba2 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -109,6 +109,24 @@ class AggregationsTest < ActiveRecord::TestCase assert_nil customers(:david).gps_location end + def test_nil_return_from_converter_is_respected_when_allow_nil_is_true + customers(:david).non_blank_gps_location = "" + customers(:david).save + customers(:david).reload + assert_nil customers(:david).non_blank_gps_location + end + + def test_nil_return_from_converter_results_in_failure_when_allow_nil_is_false + assert_raises(NoMethodError) do + customers(:barney).gps_location = "" + end + end + + def test_do_not_run_the_converter_when_nil_was_set + customers(:david).non_blank_gps_location = nil + assert_nil Customer.gps_conversion_was_run + end + def test_custom_constructor assert_equal 'Barney GUMBLE', customers(:barney).fullname.to_s assert_kind_of Fullname, customers(:barney).fullname diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 588adc38e3..b2eac0349b 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -37,6 +37,13 @@ if ActiveRecord::Base.connection.supports_migrations? end end end + + def test_schema_subclass + Class.new(ActiveRecord::Schema).define(:version => 9) do + create_table :fruits + end + assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 1160d236c9..2c7a240915 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -73,14 +73,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_eager_loading_with_primary_key Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") - citibank_result = Client.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key) + citibank_result = Client.scoped(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key).first assert citibank_result.association_cache.key?(:firm_with_primary_key) end def test_eager_loading_with_primary_key_as_symbol Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") - citibank_result = Client.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key_symbols) + citibank_result = Client.scoped(:where => {:name => "Citibank"}, :includes => :firm_with_primary_key_symbols).first assert citibank_result.association_cache.key?(:firm_with_primary_key_symbols) end @@ -168,6 +168,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase sponsor.sponsorable = Member.new :name => "Bert" assert_equal Member, sponsor.association(:sponsorable).send(:klass) + assert_equal "members", sponsor.association(:sponsorable).aliased_table_name end def test_with_polymorphic_and_condition @@ -181,7 +182,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_with_select assert_equal Company.find(2).firm_with_select.attributes.size, 1 - assert_equal Company.find(2, :include => :firm_with_select ).firm_with_select.attributes.size, 1 + assert_equal Company.scoped(:includes => :firm_with_select ).find(2).firm_with_select.attributes.size, 1 end def test_belongs_to_counter @@ -333,7 +334,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_new_record_with_foreign_key_but_no_object c = Client.new("firm_id" => 1) # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - assert_equal Firm.find(:first, :order => "id"), c.firm_with_basic_id + assert_equal Firm.scoped(:order => "id").first, c.firm_with_basic_id end def test_setting_foreign_key_after_nil_target_loaded @@ -393,9 +394,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_association_assignment_sticks - post = Post.find(:first) + post = Post.first - author1, author2 = Author.find(:all, :limit => 2) + author1, author2 = Author.scoped(:limit => 2).all assert_not_nil author1 assert_not_nil author2 @@ -497,14 +498,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do Account.find(@account.id).save! - Account.find(@account.id, :include => :firm).save! + Account.scoped(:includes => :firm).find(@account.id).save! end @account.firm.delete assert_nothing_raised do Account.find(@account.id).save! - Account.find(@account.id, :include => :firm).save! + Account.scoped(:includes => :firm).find(@account.id).save! end end @@ -517,7 +518,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase authors(:david).destroy end - assert_equal [], AuthorAddress.find_all_by_id([author_address.id, author_address_extra.id]) + assert_equal [], AuthorAddress.where(id: [author_address.id, author_address_extra.id]) assert_equal [author_address.id], AuthorAddress.destroyed_author_address_ids end @@ -704,4 +705,27 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal toy, sponsor.reload.sponsorable end + + test "stale tracking doesn't care about the type" do + apple = Firm.create("name" => "Apple") + citibank = Account.create("credit_limit" => 10) + + citibank.firm_id = apple.id + citibank.firm # load it + + citibank.firm_id = apple.id.to_s + + assert !citibank.association(:firm).stale_target? + end + + def test_reflect_the_most_recent_change + author1, author2 = Author.limit(2) + post = Post.new(:title => "foo", :body=> "bar") + + post.author = author1 + post.author_id = author2.id + + assert post.save + assert_equal post.author_id, author2.id + end end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 6733f3e889..01f7f18397 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -16,7 +16,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase :categorizations, :people, :categories, :edges, :vertices def test_eager_association_loading_with_cascaded_two_levels - authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id") + authors = Author.scoped(:includes=>{:posts=>:comments}, :order=>"authors.id").all assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size @@ -24,7 +24,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_and_one_level - authors = Author.find(:all, :include=>[{:posts=>:comments}, :categorizations], :order=>"authors.id") + authors = Author.scoped(:includes=>[{:posts=>:comments}, :categorizations], :order=>"authors.id").all assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size @@ -84,7 +84,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations - authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id") + authors = Author.scoped(:includes=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id").all assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size @@ -92,7 +92,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference - authors = Author.find(:all, :include=>{:posts=>[:comments, :author]}, :order=>"authors.id") + authors = Author.scoped(:includes=>{:posts=>[:comments, :author]}, :order=>"authors.id").all assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal authors(:david).name, authors[0].name @@ -100,13 +100,13 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_with_condition - authors = Author.find(:all, :include=>{:posts=>:comments}, :conditions=>"authors.id=1", :order=>"authors.id") + authors = Author.scoped(:includes=>{:posts=>:comments}, :where=>"authors.id=1", :order=>"authors.id").all assert_equal 1, authors.size assert_equal 5, authors[0].posts.size end def test_eager_association_loading_with_cascaded_three_levels_by_ping_pong - firms = Firm.find(:all, :include=>{:account=>{:firm=>:account}}, :order=>"companies.id") + firms = Firm.scoped(:includes=>{:account=>{:firm=>:account}}, :order=>"companies.id").all assert_equal 2, firms.size assert_equal firms.first.account, firms.first.account.firm.account assert_equal companies(:first_firm).account, assert_no_queries { firms.first.account.firm.account } @@ -114,7 +114,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_has_many_sti - topics = Topic.find(:all, :include => :replies, :order => 'topics.id') + topics = Topic.scoped(:includes => :replies, :order => 'topics.id').all first, second, = topics(:first).replies.size, topics(:second).replies.size assert_no_queries do assert_equal first, topics[0].replies.size @@ -127,7 +127,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase silly.parent_id = 1 assert silly.save - topics = Topic.find(:all, :include => :replies, :order => ['topics.id', 'replies_topics.id']) + topics = Topic.scoped(:includes => :replies, :order => ['topics.id', 'replies_topics.id']).all assert_no_queries do assert_equal 2, topics[0].replies.size assert_equal 0, topics[1].replies.size @@ -135,14 +135,14 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_belongs_to_sti - replies = Reply.find(:all, :include => :topic, :order => 'topics.id') + replies = Reply.scoped(:includes => :topic, :order => 'topics.id').all assert replies.include?(topics(:second)) assert !replies.include?(topics(:first)) assert_equal topics(:first), assert_no_queries { replies.first.topic } end def test_eager_association_loading_with_multiple_stis_and_order - author = Author.find(:first, :include => { :posts => [ :special_comments , :very_special_comment ] }, :order => ['authors.name', 'comments.body', 'very_special_comments_posts.body'], :conditions => 'posts.id = 4') + author = Author.scoped(:includes => { :posts => [ :special_comments , :very_special_comment ] }, :order => ['authors.name', 'comments.body', 'very_special_comments_posts.body'], :where => 'posts.id = 4').first assert_equal authors(:david), author assert_no_queries do author.posts.first.special_comments @@ -151,7 +151,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_of_stis_with_multiple_references - authors = Author.find(:all, :include => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4') + authors = Author.scoped(:includes => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :where => 'posts.id = 4').all assert_equal [authors(:david)], authors assert_no_queries do authors.first.posts.first.special_comments.first.post.special_comments @@ -160,7 +160,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_where_first_level_returns_nil - authors = Author.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC') + authors = Author.scoped(:includes => {:post_about_thinking => :comments}, :order => 'authors.id DESC').all assert_equal [authors(:bob), authors(:mary), authors(:david)], authors assert_no_queries do authors[2].post_about_thinking.comments.first @@ -168,12 +168,12 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through - source = Vertex.find(:first, :include=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id') + source = Vertex.scoped(:includes=>{:sinks=>{:sinks=>{:sinks=>:sinks}}}, :order => 'vertices.id').first assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first } end def test_eager_association_loading_with_recursive_cascading_four_levels_has_and_belongs_to_many - sink = Vertex.find(:first, :include=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC') + sink = Vertex.scoped(:includes=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC').first assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first } end end diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb index 7965bb404c..75a6295350 100644 --- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -24,12 +24,11 @@ class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = false - post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging ) + post = Namespaced::Post.includes(:tagging).find_by_title('Great stuff') assert_nil post.tagging - ActiveRecord::IdentityMap.clear ActiveRecord::Base.store_full_sti_class = true - post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging ) + post = Namespaced::Post.includes(:tagging).find_by_title('Great stuff') assert_instance_of Tagging, post.tagging ensure ActiveRecord::Base.store_full_sti_class = old diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index 1e1958410c..bb0d6bc70b 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -92,8 +92,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase end def test_include_query - res = 0 - res = ShapeExpression.find :all, :include => [ :shape, { :paint => :non_poly } ] + res = ShapeExpression.scoped(:includes => [ :shape, { :paint => :non_poly } ]).all assert_equal NUM_SHAPE_EXPRESSIONS, res.size assert_queries(0) do res.each do |se| @@ -123,7 +122,7 @@ class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase assert_nothing_raised do # @davey_mcdave doesn't have any author_favorites includes = {:posts => :comments, :categorizations => :category, :author_favorites => :favorite_author } - Author.all :include => includes, :conditions => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name' + Author.scoped(:includes => includes, :where => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name').to_a end end end diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb index 07d0b24613..5805e71249 100644 --- a/activerecord/test/cases/associations/eager_singularization_test.rb +++ b/activerecord/test/cases/associations/eager_singularization_test.rb @@ -103,43 +103,43 @@ class EagerSingularizationTest < ActiveRecord::TestCase def test_eager_no_extra_singularization_belongs_to return unless @have_tables assert_nothing_raised do - Virus.find(:all, :include => :octopus) + Virus.scoped(:includes => :octopus).all end end def test_eager_no_extra_singularization_has_one return unless @have_tables assert_nothing_raised do - Octopus.find(:all, :include => :virus) + Octopus.scoped(:includes => :virus).all end end def test_eager_no_extra_singularization_has_many return unless @have_tables assert_nothing_raised do - Bus.find(:all, :include => :passes) + Bus.scoped(:includes => :passes).all end end def test_eager_no_extra_singularization_has_and_belongs_to_many return unless @have_tables assert_nothing_raised do - Crisis.find(:all, :include => :messes) - Mess.find(:all, :include => :crises) + Crisis.scoped(:includes => :messes).all + Mess.scoped(:includes => :crises).all end end def test_eager_no_extra_singularization_has_many_through_belongs_to return unless @have_tables assert_nothing_raised do - Crisis.find(:all, :include => :successes) + Crisis.scoped(:includes => :successes).all end end def test_eager_no_extra_singularization_has_many_through_has_many return unless @have_tables assert_nothing_raised do - Crisis.find(:all, :include => :compresses) + Crisis.scoped(:includes => :compresses).all end end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index b79c69bbb5..08467900f9 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -35,42 +35,42 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_one_through_join_model_with_conditions_on_the_through - member = Member.find(members(:some_other_guy).id, :include => :favourite_club) + member = Member.scoped(:includes => :favourite_club).find(members(:some_other_guy).id) assert_nil member.favourite_club end def test_loading_with_one_association - posts = Post.find(:all, :include => :comments) + posts = Post.scoped(:includes => :comments).all post = posts.find { |p| p.id == 1 } assert_equal 2, post.comments.size assert post.comments.include?(comments(:greetings)) - post = Post.find(:first, :include => :comments, :conditions => "posts.title = 'Welcome to the weblog'") + post = Post.scoped(:includes => :comments, :where => "posts.title = 'Welcome to the weblog'").first assert_equal 2, post.comments.size assert post.comments.include?(comments(:greetings)) - posts = Post.find(:all, :include => :last_comment) + posts = Post.scoped(:includes => :last_comment).all post = posts.find { |p| p.id == 1 } assert_equal Post.find(1).last_comment, post.last_comment end def test_loading_with_one_association_with_non_preload - posts = Post.find(:all, :include => :last_comment, :order => 'comments.id DESC') + posts = Post.scoped(:includes => :last_comment, :order => 'comments.id DESC').all post = posts.find { |p| p.id == 1 } assert_equal Post.find(1).last_comment, post.last_comment end def test_loading_conditions_with_or - posts = authors(:david).posts.find( - :all, :include => :comments, :references => :comments, - :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'" - ) + posts = authors(:david).posts.references(:comments).scoped( + :includes => :comments, + :where => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'" + ).all assert_nil posts.detect { |p| p.author_id != authors(:david).id }, "expected to find only david's posts" end def test_with_ordering - list = Post.find(:all, :include => :comments, :order => "posts.id DESC") + list = Post.scoped(:includes => :comments, :order => "posts.id DESC").all [:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments, :authorless, :thinking, :welcome ].each_with_index do |post, index| @@ -84,14 +84,14 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_loading_with_multiple_associations - posts = Post.find(:all, :include => [ :comments, :author, :categories ], :order => "posts.id") + posts = Post.scoped(:includes => [ :comments, :author, :categories ], :order => "posts.id").all assert_equal 2, posts.first.comments.size assert_equal 2, posts.first.categories.size assert posts.first.comments.include?(comments(:greetings)) end def test_duplicate_middle_objects - comments = Comment.find :all, :conditions => 'post_id = 1', :include => [:post => :author] + comments = Comment.scoped(:where => 'post_id = 1', :includes => [:post => :author]).all assert_no_queries do comments.each {|comment| comment.post.author.name} end @@ -99,25 +99,25 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.find(:all, :include=>:comments) + posts = Post.scoped(:includes=>:comments).all assert_equal 11, posts.size end def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.find(:all, :include=>:comments) + posts = Post.scoped(:includes=>:comments).all assert_equal 11, posts.size end def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.find(:all, :include=>:categories) + posts = Post.scoped(:includes=>:categories).all assert_equal 11, posts.size end def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle Post.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.find(:all, :include=>:categories) + posts = Post.scoped(:includes=>:categories).all assert_equal 11, posts.size end @@ -154,8 +154,8 @@ class EagerAssociationTest < ActiveRecord::TestCase popular_post.readers.create!(:person => people(:michael)) popular_post.readers.create!(:person => people(:david)) - readers = Reader.find(:all, :conditions => ["post_id = ?", popular_post.id], - :include => {:post => :comments}) + readers = Reader.scoped(:where => ["post_id = ?", popular_post.id], + :includes => {:post => :comments}).all readers.each do |reader| assert_equal [comment], reader.post.comments end @@ -167,8 +167,8 @@ class EagerAssociationTest < ActiveRecord::TestCase car_post.categories << categories(:technology) comment = car_post.comments.create!(:body => "hmm") - categories = Category.find(:all, :conditions => { 'posts.id' => car_post.id }, - :include => {:posts => :comments}) + categories = Category.scoped(:where => { 'posts.id' => car_post.id }, + :includes => {:posts => :comments}).all categories.each do |category| assert_equal [comment], category.posts[0].comments end @@ -186,10 +186,10 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_has_many_association_with_same_include_includes_only_once author_id = authors(:david).id - author = assert_queries(3) { Author.find(author_id, :include => {:posts_with_comments => :comments}) } # find the author, then find the posts, then find the comments + author = assert_queries(3) { Author.scoped(:includes => {:posts_with_comments => :comments}).find(author_id) } # find the author, then find the posts, then find the comments author.posts_with_comments.each do |post_with_comments| assert_equal post_with_comments.comments.length, post_with_comments.comments.count - assert_nil post_with_comments.comments.uniq! + assert_nil post_with_comments.comments.to_a.uniq! end end @@ -197,7 +197,7 @@ class EagerAssociationTest < ActiveRecord::TestCase author = authors(:david) post = author.post_about_thinking_with_last_comment last_comment = post.last_comment - author = assert_queries(ActiveRecord::IdentityMap.enabled? ? 2 : 3) { Author.find(author.id, :include => {:post_about_thinking_with_last_comment => :last_comment})} # find the author, then find the posts, then find the comments + author = assert_queries(3) { Author.scoped(:includes => {:post_about_thinking_with_last_comment => :last_comment}).find(author.id)} # find the author, then find the posts, then find the comments assert_no_queries do assert_equal post, author.post_about_thinking_with_last_comment assert_equal last_comment, author.post_about_thinking_with_last_comment.last_comment @@ -208,7 +208,7 @@ class EagerAssociationTest < ActiveRecord::TestCase post = posts(:welcome) author = post.author author_address = author.author_address - post = assert_queries(ActiveRecord::IdentityMap.enabled? ? 2 : 3) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author, then find the address + post = assert_queries(3) { Post.scoped(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author, then find the address assert_no_queries do assert_equal author, post.author_with_address assert_equal author_address, post.author_with_address.author_address @@ -218,7 +218,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once post = posts(:welcome) post.update_attributes!(:author => nil) - post = assert_queries(1) { Post.find(post.id, :include => {:author_with_address => :author_address}) } # find the post, then find the author which is null so no query for the author or address + post = assert_queries(1) { Post.scoped(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author which is null so no query for the author or address assert_no_queries do assert_equal nil, post.author_with_address end @@ -227,85 +227,85 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_null_belongs_to_polymorphic_association sponsor = sponsors(:moustache_club_sponsor_for_groucho) sponsor.update_attributes!(:sponsorable => nil) - sponsor = assert_queries(1) { Sponsor.find(sponsor.id, :include => :sponsorable) } + sponsor = assert_queries(1) { Sponsor.scoped(:includes => :sponsorable).find(sponsor.id) } assert_no_queries do assert_equal nil, sponsor.sponsorable end end def test_loading_from_an_association - posts = authors(:david).posts.find(:all, :include => :comments, :order => "posts.id") + posts = authors(:david).posts.scoped(:includes => :comments, :order => "posts.id").all assert_equal 2, posts.first.comments.size end def test_loading_from_an_association_that_has_a_hash_of_conditions assert_nothing_raised do - Author.find(:all, :include => :hello_posts_with_hash_conditions) + Author.scoped(:includes => :hello_posts_with_hash_conditions).all end - assert !Author.find(authors(:david).id, :include => :hello_posts_with_hash_conditions).hello_posts.empty? + assert !Author.scoped(:includes => :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts.empty? end def test_loading_with_no_associations - assert_nil Post.find(posts(:authorless).id, :include => :author).author + assert_nil Post.scoped(:includes => :author).find(posts(:authorless).id).author end def test_nested_loading_with_no_associations assert_nothing_raised do - Post.find(posts(:authorless).id, :include => {:author => :author_addresss}) + Post.scoped(:includes => {:author => :author_addresss}).find(posts(:authorless).id) end end def test_nested_loading_through_has_one_association - aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}) + aa = AuthorAddress.scoped(:includes => {:author => :posts}).find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order - aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'author_addresses.id') + aa = AuthorAddress.scoped(:includes => {:author => :posts}, :order => 'author_addresses.id').find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order_on_association - aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'authors.id') + aa = AuthorAddress.scoped(:includes => {:author => :posts}, :order => 'authors.id').find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order_on_nested_association - aa = AuthorAddress.find(author_addresses(:david_address).id, :include => {:author => :posts}, :order => 'posts.id') + aa = AuthorAddress.scoped(:includes => {:author => :posts}, :order => 'posts.id').find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_conditions - aa = AuthorAddress.find( - author_addresses(:david_address).id, :include => {:author => :posts}, - :conditions => "author_addresses.id > 0", :references => :author_addresses - ) + aa = AuthorAddress.references(:author_addresses).scoped( + :includes => {:author => :posts}, + :where => "author_addresses.id > 0" + ).find author_addresses(:david_address).id assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_conditions_on_association - aa = AuthorAddress.find( - author_addresses(:david_address).id, :include => {:author => :posts}, - :conditions => "authors.id > 0", :references => :authors - ) + aa = AuthorAddress.references(:authors).scoped( + :includes => {:author => :posts}, + :where => "authors.id > 0" + ).find author_addresses(:david_address).id assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_conditions_on_nested_association - aa = AuthorAddress.find( - author_addresses(:david_address).id, :include => {:author => :posts}, - :conditions => "posts.id > 0", :references => :posts - ) + aa = AuthorAddress.references(:posts).scoped( + :includes => {:author => :posts}, + :where => "posts.id > 0" + ).find author_addresses(:david_address).id assert_equal aa.author.posts.count, aa.author.posts.length end def test_eager_association_loading_with_belongs_to_and_foreign_keys - pets = Pet.find(:all, :include => :owner) + pets = Pet.scoped(:includes => :owner).all assert_equal 3, pets.length end def test_eager_association_loading_with_belongs_to - comments = Comment.find(:all, :include => :post) + comments = Comment.scoped(:includes => :post).all assert_equal 11, comments.length titles = comments.map { |c| c.post.title } assert titles.include?(posts(:welcome).title) @@ -313,31 +313,31 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_association_loading_with_belongs_to_and_limit - comments = Comment.find(:all, :include => :post, :limit => 5, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :limit => 5, :order => 'comments.id').all assert_equal 5, comments.length assert_equal [1,2,3,5,6], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_conditions - comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :where => 'post_id = 4', :limit => 3, :order => 'comments.id').all assert_equal 3, comments.length assert_equal [5,6,7], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset - comments = Comment.find(:all, :include => :post, :limit => 3, :offset => 2, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :limit => 3, :offset => 2, :order => 'comments.id').all assert_equal 3, comments.length assert_equal [3,5,6], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions - comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :where => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id').all assert_equal 3, comments.length assert_equal [6,7,8], comments.collect { |c| c.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array - comments = Comment.find(:all, :include => :post, :conditions => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :where => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id').all assert_equal 3, comments.length assert_equal [6,7,8], comments.collect { |c| c.id } end @@ -345,7 +345,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name assert_nothing_raised do ActiveSupport::Deprecation.silence do - Comment.find(:all, :include => :post, :conditions => ['posts.id = ?',4]) + Comment.scoped(:includes => :post, :where => ['posts.id = ?',4]).all end end end @@ -353,7 +353,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_hash comments = [] assert_nothing_raised do - comments = Comment.find(:all, :include => :post, :conditions => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id') + comments = Comment.scoped(:includes => :post, :where => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id').all end assert_equal 3, comments.length assert_equal [5,6,7], comments.collect { |c| c.id } @@ -366,14 +366,14 @@ class EagerAssociationTest < ActiveRecord::TestCase quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id') assert_nothing_raised do ActiveSupport::Deprecation.silence do - Comment.find(:all, :include => :post, :conditions => ["#{quoted_posts_id} = ?",4]) + Comment.scoped(:includes => :post, :where => ["#{quoted_posts_id} = ?",4]).all end end end def test_eager_association_loading_with_belongs_to_and_order_string_with_unquoted_table_name assert_nothing_raised do - Comment.find(:all, :include => :post, :order => 'posts.id') + Comment.scoped(:includes => :post, :order => 'posts.id').all end end @@ -381,55 +381,55 @@ class EagerAssociationTest < ActiveRecord::TestCase quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id') assert_nothing_raised do ActiveSupport::Deprecation.silence do - Comment.find(:all, :include => :post, :order => quoted_posts_id) + Comment.scoped(:includes => :post, :order => quoted_posts_id).all end end end def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations - posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :order => 'posts.id') + posts = Post.scoped(:includes => [:author, :very_special_comment], :limit => 1, :order => 'posts.id').all assert_equal 1, posts.length assert_equal [1], posts.collect { |p| p.id } end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations - posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id') + posts = Post.scoped(:includes => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id').all assert_equal 1, posts.length assert_equal [2], posts.collect { |p| p.id } end def test_eager_association_loading_with_belongs_to_inferred_foreign_key_from_association_name - author_favorite = AuthorFavorite.find(:first, :include => :favorite_author) + author_favorite = AuthorFavorite.scoped(:includes => :favorite_author).first assert_equal authors(:mary), assert_no_queries { author_favorite.favorite_author } end def test_eager_load_belongs_to_quotes_table_and_column_names - job = Job.find jobs(:unicyclist).id, :include => :ideal_reference + job = Job.includes(:ideal_reference).find jobs(:unicyclist).id references(:michael_unicyclist) assert_no_queries{ assert_equal references(:michael_unicyclist), job.ideal_reference} end def test_eager_load_has_one_quotes_table_and_column_names - michael = Person.find(people(:michael), :include => :favourite_reference) + michael = Person.scoped(:includes => :favourite_reference).find(people(:michael)) references(:michael_unicyclist) assert_no_queries{ assert_equal references(:michael_unicyclist), michael.favourite_reference} end def test_eager_load_has_many_quotes_table_and_column_names - michael = Person.find(people(:michael), :include => :references) + michael = Person.scoped(:includes => :references).find(people(:michael)) references(:michael_magician,:michael_unicyclist) assert_no_queries{ assert_equal references(:michael_magician,:michael_unicyclist), michael.references.sort_by(&:id) } end def test_eager_load_has_many_through_quotes_table_and_column_names - michael = Person.find(people(:michael), :include => :jobs) + michael = Person.scoped(:includes => :jobs).find(people(:michael)) jobs(:magician, :unicyclist) assert_no_queries{ assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) } end def test_eager_load_has_many_with_string_keys subscriptions = subscriptions(:webster_awdr, :webster_rfr) - subscriber =Subscriber.find(subscribers(:second).id, :include => :subscriptions) + subscriber =Subscriber.scoped(:includes => :subscriptions).find(subscribers(:second).id) assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id) end @@ -447,25 +447,25 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_load_has_many_through_with_string_keys books = books(:awdr, :rfr) - subscriber = Subscriber.find(subscribers(:second).id, :include => :books) + subscriber = Subscriber.scoped(:includes => :books).find(subscribers(:second).id) assert_equal books, subscriber.books.sort_by(&:id) end def test_eager_load_belongs_to_with_string_keys subscriber = subscribers(:second) - subscription = Subscription.find(subscriptions(:webster_awdr).id, :include => :subscriber) + subscription = Subscription.scoped(:includes => :subscriber).find(subscriptions(:webster_awdr).id) assert_equal subscriber, subscription.subscriber end def test_eager_association_loading_with_explicit_join - posts = Post.find(:all, :include => :comments, :joins => "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", :limit => 1, :order => 'author_id') + posts = Post.scoped(:includes => :comments, :joins => "INNER JOIN authors ON posts.author_id = authors.id AND authors.name = 'Mary'", :limit => 1, :order => 'author_id').all assert_equal 1, posts.length end def test_eager_with_has_many_through - posts_with_comments = people(:michael).posts.find(:all, :include => :comments, :order => 'posts.id') - posts_with_author = people(:michael).posts.find(:all, :include => :author, :order => 'posts.id') - posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ], :order => 'posts.id') + posts_with_comments = people(:michael).posts.scoped(:includes => :comments, :order => 'posts.id').all + posts_with_author = people(:michael).posts.scoped(:includes => :author, :order => 'posts.id').all + posts_with_comments_and_author = people(:michael).posts.scoped(:includes => [ :comments, :author ], :order => 'posts.id').all assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size } assert_equal authors(:david), assert_no_queries { posts_with_author.first.author } assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author } @@ -476,32 +476,32 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.create!(:author => author, :title => "TITLE", :body => "BODY") author.author_favorites.create(:favorite_author_id => 1) author.author_favorites.create(:favorite_author_id => 2) - posts_with_author_favorites = author.posts.find(:all, :include => :author_favorites) + posts_with_author_favorites = author.posts.scoped(:includes => :author_favorites).all assert_no_queries { posts_with_author_favorites.first.author_favorites.first.author_id } end def test_eager_with_has_many_through_an_sti_join_model - author = Author.find(:first, :include => :special_post_comments, :order => 'authors.id') + author = Author.scoped(:includes => :special_post_comments, :order => 'authors.id').first assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments } end def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both - author = Author.find(:first, :include => :special_nonexistant_post_comments, :order => 'authors.id') + author = Author.scoped(:includes => :special_nonexistant_post_comments, :order => 'authors.id').first assert_equal [], author.special_nonexistant_post_comments end def test_eager_with_has_many_through_join_model_with_conditions - assert_equal Author.find(:first, :include => :hello_post_comments, - :order => 'authors.id').hello_post_comments.sort_by(&:id), - Author.find(:first, :order => 'authors.id').hello_post_comments.sort_by(&:id) + assert_equal Author.scoped(:includes => :hello_post_comments, + :order => 'authors.id').first.hello_post_comments.sort_by(&:id), + Author.scoped(:order => 'authors.id').first.hello_post_comments.sort_by(&:id) end def test_eager_with_has_many_through_join_model_with_conditions_on_top_level - assert_equal comments(:more_greetings), Author.find(authors(:david).id, :include => :comments_with_order_and_conditions).comments_with_order_and_conditions.first + assert_equal comments(:more_greetings), Author.scoped(:includes => :comments_with_order_and_conditions).find(authors(:david).id).comments_with_order_and_conditions.first end def test_eager_with_has_many_through_join_model_with_include - author_comments = Author.find(authors(:david).id, :include => :comments_with_include).comments_with_include.to_a + author_comments = Author.scoped(:includes => :comments_with_include).find(authors(:david).id).comments_with_include.to_a assert_no_queries do author_comments.first.post.title end @@ -509,7 +509,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_through_with_conditions_join_model_with_include post_tags = Post.find(posts(:welcome).id).misc_tags - eager_post_tags = Post.find(1, :include => :misc_tags).misc_tags + eager_post_tags = Post.scoped(:includes => :misc_tags).find(1).misc_tags assert_equal post_tags, eager_post_tags end @@ -520,16 +520,16 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit - posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2) + posts = Post.scoped(:order => 'posts.id asc', :includes => [ :author, :comments ], :limit => 2).all assert_equal 2, posts.size assert_equal 3, posts.inject(0) { |sum, post| sum += post.comments.size } end def test_eager_with_has_many_and_limit_and_conditions if current_adapter?(:OpenBaseAdapter) - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id") + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id").all else - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.body = 'hello'", :order => "posts.id") + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").all end assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } @@ -537,9 +537,9 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions_array if current_adapter?(:OpenBaseAdapter) - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id") + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id").all else - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "posts.body = ?", 'hello' ], :order => "posts.id") + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").all end assert_equal 2, posts.size assert_equal [4,5], posts.collect { |p| p.id } @@ -547,7 +547,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers posts = ActiveSupport::Deprecation.silence do - Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ]) + Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => [ "authors.name = ?", 'David' ]).all end assert_equal 2, posts.size @@ -558,41 +558,41 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit_and_high_offset - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => { 'authors.name' => 'David' }) + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, :where => { 'authors.name' => 'David' }).all assert_equal 0, posts.size end def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions assert_queries(1) do - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, - :conditions => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ], - :references => [:authors, :comments]) + posts = Post.references(:authors, :comments). + scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, + :where => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ]).all assert_equal 0, posts.size end end def test_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions assert_queries(1) do - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, - :conditions => { 'authors.name' => 'David', 'comments.body' => 'go crazy' }) + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, + :where => { 'authors.name' => 'David', 'comments.body' => 'go crazy' }).all assert_equal 0, posts.size end end def test_count_eager_with_has_many_and_limit_and_high_offset - posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => { 'authors.name' => 'David' }) + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :offset => 10, :where => { 'authors.name' => 'David' }).count(:all) assert_equal 0, posts end def test_eager_with_has_many_and_limit_with_no_results - posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.title = 'magic forest'") + posts = Post.scoped(:includes => [ :author, :comments ], :limit => 2, :where => "posts.title = 'magic forest'").all assert_equal 0, posts.size end def test_eager_count_performed_on_a_has_many_association_with_multi_table_conditional author = authors(:david) author_posts_without_comments = author.posts.select { |post| post.comments.blank? } - assert_equal author_posts_without_comments.size, author.posts.count(:all, :include => :comments, :conditions => 'comments.id is null', :references => :comments) + assert_equal author_posts_without_comments.size, author.posts.includes(:comments).where('comments.id is null').references(:comments).count end def test_eager_count_performed_on_a_has_many_through_association_with_multi_table_conditional @@ -602,7 +602,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_and_belongs_to_many_and_limit - posts = Post.find(:all, :include => :categories, :order => "posts.id", :limit => 3) + posts = Post.scoped(:includes => :categories, :order => "posts.id", :limit => 3).all assert_equal 3, posts.size assert_equal 2, posts[0].categories.size assert_equal 1, posts[1].categories.size @@ -611,9 +611,9 @@ class EagerAssociationTest < ActiveRecord::TestCase assert posts[1].categories.include?(categories(:general)) end - # This is only really relevant when the identity map is off. Since the preloader for habtm - # gets raw row hashes from the database and then instantiates them, this test ensures that - # it only instantiates one actual object per record from the database. + # Since the preloader for habtm gets raw row hashes from the database and then + # instantiates them, this test ensures that it only instantiates one actual + # object per record from the database. def test_has_and_belongs_to_many_should_not_instantiate_same_records_multiple_times welcome = posts(:welcome) categories = Category.includes(:posts) @@ -628,80 +628,47 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers - posts = authors(:david).posts.find(:all, - :include => :comments, - :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", - :references => :comments, - :limit => 2 - ) + posts = + authors(:david).posts + .includes(:comments) + .where("comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'") + .references(:comments) + .limit(2) + .to_a assert_equal 2, posts.size - count = Post.count( - :include => [ :comments, :author ], - :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", - :references => [:authors, :comments], - :limit => 2 - ) + count = + Post.includes(:comments, :author) + .where("authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')") + .references(:authors, :comments) + .limit(2) + .count assert_equal count, posts.size end def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers posts = nil - Post.send(:with_scope, :find => { - :include => :comments, - :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", - :references => :comments - }) do - posts = authors(:david).posts.find(:all, :limit => 2) - assert_equal 2, posts.size - end + Post.includes(:comments) + .where("comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'") + .references(:comments) + .scoping do - Post.send(:with_scope, :find => { - :include => [ :comments, :author ], - :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", - :references => [:authors, :comments] - }) do - count = Post.count(:limit => 2) - assert_equal count, posts.size + posts = authors(:david).posts.limit(2).to_a + assert_equal 2, posts.size end - end - def test_eager_with_has_many_and_limit_and_scoped_and_explicit_conditions_on_the_eagers - Post.send(:with_scope, :find => { :conditions => "1=1" }) do - posts = authors(:david).posts.find(:all, - :include => :comments, - :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'", - :references => :comments, - :limit => 2 - ) - assert_equal 2, posts.size + Post.includes(:comments, :author) + .where("authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')") + .references(:authors, :comments) + .scoping do - count = Post.count( - :include => [ :comments, :author ], - :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')", - :references => [:authors, :comments], - :limit => 2 - ) + count = Post.limit(2).count assert_equal count, posts.size end end - def test_eager_with_scoped_order_using_association_limiting_without_explicit_scope - posts_with_explicit_order = Post.find( - :all, :conditions => 'comments.id is not null', :references => :comments, - :include => :comments, :order => 'posts.id DESC', :limit => 2 - ) - posts_with_scoped_order = Post.send(:with_scope, :find => {:order => 'posts.id DESC'}) do - Post.find( - :all, :conditions => 'comments.id is not null', - :references => :comments, :include => :comments, :limit => 2 - ) - end - assert_equal posts_with_explicit_order, posts_with_scoped_order - end - def test_eager_association_loading_with_habtm - posts = Post.find(:all, :include => :categories, :order => "posts.id") + posts = Post.scoped(:includes => :categories, :order => "posts.id").all assert_equal 2, posts[0].categories.size assert_equal 1, posts[1].categories.size assert_equal 0, posts[2].categories.size @@ -710,23 +677,23 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_inheritance - SpecialPost.find(:all, :include => [ :comments ]) + SpecialPost.scoped(:includes => [ :comments ]).all end def test_eager_has_one_with_association_inheritance - post = Post.find(4, :include => [ :very_special_comment ]) + post = Post.scoped(:includes => [ :very_special_comment ]).find(4) assert_equal "VerySpecialComment", post.very_special_comment.class.to_s end def test_eager_has_many_with_association_inheritance - post = Post.find(4, :include => [ :special_comments ]) + post = Post.scoped(:includes => [ :special_comments ]).find(4) post.special_comments.each do |special_comment| assert special_comment.is_a?(SpecialComment) end end def test_eager_habtm_with_association_inheritance - post = Post.find(6, :include => [ :special_categories ]) + post = Post.scoped(:includes => [ :special_categories ]).find(6) assert_equal 1, post.special_categories.size post.special_categories.each do |special_category| assert_equal "SpecialCategory", special_category.class.to_s @@ -735,8 +702,8 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_one_dependent_does_not_destroy_dependent assert_not_nil companies(:first_firm).account - f = Firm.find(:first, :include => :account, - :conditions => ["companies.name = ?", "37signals"]) + f = Firm.scoped(:includes => :account, + :where => ["companies.name = ?", "37signals"]).first assert_not_nil f.account assert_equal companies(:first_firm, :reload).account, f.account end @@ -750,16 +717,16 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_invalid_association_reference assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.find(6, :include=> :monkeys ) + Post.scoped(:includes=> :monkeys ).find(6) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.find(6, :include=>[ :monkeys ]) + Post.scoped(:includes=>[ :monkeys ]).find(6) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.find(6, :include=>[ 'monkeys' ]) + Post.scoped(:includes=>[ 'monkeys' ]).find(6) } assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { - Post.find(6, :include=>[ :monkeys, :elephants ]) + Post.scoped(:includes=>[ :monkeys, :elephants ]).find(6) } end @@ -804,52 +771,51 @@ class EagerAssociationTest < ActiveRecord::TestCase end def find_all_ordered(className, include=nil) - className.find(:all, :order=>"#{className.table_name}.#{className.primary_key}", :include=>include) + className.scoped(:order=>"#{className.table_name}.#{className.primary_key}", :includes=>include).all end def test_limited_eager_with_order assert_equal( posts(:thinking, :sti_comments), - Post.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.scoped( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => 'UPPER(posts.title)', :limit => 2, :offset => 1 - ) + ).all ) assert_equal( posts(:sti_post_and_comments, :sti_comments), - Post.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.scoped( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1 - ) + ).all ) end def test_limited_eager_with_multiple_order_columns assert_equal( posts(:thinking, :sti_comments), - Post.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.scoped( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => ['UPPER(posts.title)', 'posts.id'], :limit => 2, :offset => 1 - ) + ).all ) assert_equal( posts(:sti_post_and_comments, :sti_comments), - Post.find( - :all, :include => [:author, :comments], :conditions => { 'authors.name' => 'David' }, + Post.scoped( + :includes => [:author, :comments], :where => { 'authors.name' => 'David' }, :order => ['UPPER(posts.title) DESC', 'posts.id'], :limit => 2, :offset => 1 - ) + ).all ) end def test_limited_eager_with_numeric_in_association assert_equal( people(:david, :susan), - Person.find( - :all, :include => [:readers, :primary_contact, :number1_fan], - :conditions => "number1_fans_people.first_name like 'M%'", - :references => :number1_fans_people, + Person.references(:number1_fans_people).scoped( + :includes => [:readers, :primary_contact, :number1_fan], + :where => "number1_fans_people.first_name like 'M%'", :order => 'people.id', :limit => 2, :offset => 0 - ) + ).all ) end @@ -862,9 +828,9 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_polymorphic_type_condition - post = Post.find(posts(:thinking).id, :include => :taggings) + post = Post.scoped(:includes => :taggings).find(posts(:thinking).id) assert post.taggings.include?(taggings(:thinking_general)) - post = SpecialPost.find(posts(:thinking).id, :include => :taggings) + post = SpecialPost.scoped(:includes => :taggings).find(posts(:thinking).id) assert post.taggings.include?(taggings(:thinking_general)) end @@ -915,13 +881,13 @@ class EagerAssociationTest < ActiveRecord::TestCase end end def test_eager_with_valid_association_as_string_not_symbol - assert_nothing_raised { Post.find(:all, :include => 'comments') } + assert_nothing_raised { Post.scoped(:includes => 'comments').all } end def test_eager_with_floating_point_numbers assert_queries(2) do # Before changes, the floating point numbers will be interpreted as table names and will cause this to run in one query - Comment.find :all, :conditions => "123.456 = 123.456", :include => :post + Comment.scoped(:where => "123.456 = 123.456", :includes => :post).all end end @@ -965,31 +931,31 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_count_with_include if current_adapter?(:SybaseAdapter) - assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15", :references => :comments) + assert_equal 3, authors(:david).posts_with_comments.where("len(comments.body) > 15").references(:comments).count elsif current_adapter?(:OpenBaseAdapter) - assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(FETCHBLOB(comments.body)) > 15", :references => :comments) + assert_equal 3, authors(:david).posts_with_comments.where("length(FETCHBLOB(comments.body)) > 15").references(:comments).count else - assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(comments.body) > 15", :references => :comments) + assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count end end def test_load_with_sti_sharing_association assert_queries(2) do #should not do 1 query per subclass - Comment.find :all, :include => :post + Comment.includes(:post).all end end def test_conditions_on_join_table_with_include_and_limit - assert_equal 3, Developer.find(:all, :include => 'projects', :conditions => { 'developers_projects.access_level' => 1 }, :limit => 5).size + assert_equal 3, Developer.scoped(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).all.size end def test_order_on_join_table_with_include_and_limit - assert_equal 5, Developer.find(:all, :include => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).size + assert_equal 5, Developer.scoped(:includes => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).all.size end def test_eager_loading_with_order_on_joined_table_preloads posts = assert_queries(2) do - Post.find(:all, :joins => :comments, :include => :author, :order => 'comments.id DESC') + Post.scoped(:joins => :comments, :includes => :author, :order => 'comments.id DESC').all end assert_equal posts(:eager_other), posts[1] assert_equal authors(:mary), assert_no_queries { posts[1].author} @@ -997,24 +963,24 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_loading_with_conditions_on_joined_table_preloads posts = assert_queries(2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => [:comments], :where => "comments.body like 'Thank you%'", :order => 'posts.id').all end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} - posts = assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + posts = assert_queries(2) do + Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => [:comments], :where => "comments.body like 'Thank you%'", :order => 'posts.id').all end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} - posts = assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'", :order => 'posts.id') + posts = assert_queries(2) do + Post.scoped(:includes => :author, :joins => {:taggings => :tag}, :where => "tags.name = 'General'", :order => 'posts.id').all end assert_equal posts(:welcome, :thinking), posts - posts = assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2", :order => 'posts.id') + posts = assert_queries(2) do + Post.scoped(:includes => :author, :joins => {:taggings => {:tag => :taggings}}, :where => "taggings_tags.super_tag_id=2", :order => 'posts.id').all end assert_equal posts(:welcome, :thinking), posts @@ -1022,13 +988,13 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_loading_with_conditions_on_string_joined_table_preloads posts = assert_queries(2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :where => "comments.body like 'Thank you%'", :order => 'posts.id').all end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} - posts = assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') + posts = assert_queries(2) do + Post.scoped(:select => 'distinct posts.*', :includes => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :where => "comments.body like 'Thank you%'", :order => 'posts.id').all end assert_equal [posts(:welcome)], posts assert_equal authors(:david), assert_no_queries { posts[0].author} @@ -1037,7 +1003,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_loading_with_select_on_joined_table_preloads posts = assert_queries(2) do - Post.find(:all, :select => 'posts.*, authors.name as author_name', :include => :comments, :joins => :author, :order => 'posts.id') + Post.scoped(:select => 'posts.*, authors.name as author_name', :includes => :comments, :joins => :author, :order => 'posts.id').all end assert_equal 'David', posts[0].author_name assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments} @@ -1047,14 +1013,14 @@ class EagerAssociationTest < ActiveRecord::TestCase Author.columns authors = assert_queries(2) do - Author.find(:all, :include => :author_address, :joins => :comments, :conditions => "posts.title like 'Welcome%'") + Author.scoped(:includes => :author_address, :joins => :comments, :where => "posts.title like 'Welcome%'").all end assert_equal authors(:david), authors[0] assert_equal author_addresses(:david_address), authors[0].author_address end def test_preload_belongs_to_uses_exclusive_scope - people = Person.males.find(:all, :include => :primary_contact) + people = Person.males.scoped(:includes => :primary_contact).all assert_not_equal people.length, 0 people.each do |person| assert_no_queries {assert_not_nil person.primary_contact} @@ -1063,15 +1029,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_preload_has_many_uses_exclusive_scope - people = Person.males.find :all, :include => :agents + people = Person.males.includes(:agents).all people.each do |person| assert_equal Person.find(person.id).agents, person.agents end end def test_preload_has_many_using_primary_key - expected = Firm.find(:first).clients_using_primary_key.to_a - firm = Firm.find :first, :include => :clients_using_primary_key + expected = Firm.first.clients_using_primary_key.to_a + firm = Firm.includes(:clients_using_primary_key).first assert_no_queries do assert_equal expected, firm.clients_using_primary_key end @@ -1081,9 +1047,9 @@ class EagerAssociationTest < ActiveRecord::TestCase expected = Firm.find(1).clients_using_primary_key.sort_by(&:name) # Oracle adapter truncates alias to 30 characters if current_adapter?(:OracleAdapter) - firm = Firm.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies'[0,30]+'.name' + firm = Firm.scoped(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies'[0,30]+'.name').find(1) else - firm = Firm.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name' + firm = Firm.scoped(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name').find(1) end assert_no_queries do assert_equal expected, firm.clients_using_primary_key @@ -1092,7 +1058,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preload_has_one_using_primary_key expected = accounts(:signals37) - firm = Firm.find :first, :include => :account_using_primary_key, :order => 'companies.id' + firm = Firm.scoped(:includes => :account_using_primary_key, :order => 'companies.id').first assert_no_queries do assert_equal expected, firm.account_using_primary_key end @@ -1100,7 +1066,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_include_has_one_using_primary_key expected = accounts(:signals37) - firm = Firm.find(:all, :include => :account_using_primary_key, :order => 'accounts.id').detect {|f| f.id == 1} + firm = Firm.scoped(:includes => :account_using_primary_key, :order => 'accounts.id').all.detect {|f| f.id == 1} assert_no_queries do assert_equal expected, firm.account_using_primary_key end @@ -1116,7 +1082,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preloading_empty_belongs_to_polymorphic t = Tagging.create!(:taggable_type => 'Post', :taggable_id => Post.maximum(:id) + 1, :tag => tags(:general)) - tagging = assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) { Tagging.preload(:taggable).find(t.id) } + tagging = assert_queries(2) { Tagging.preload(:taggable).find(t.id) } assert_no_queries { assert_nil tagging.taggable } end @@ -1164,9 +1130,26 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_deep_including_through_habtm - posts = Post.find(:all, :include => {:categories => :categorizations}, :order => "posts.id") + posts = Post.scoped(:includes => {:categories => :categorizations}, :order => "posts.id").all assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length } assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length } assert_no_queries { assert_equal 2, posts[1].categories[0].categorizations.length } end + + test "scoping with a circular preload" do + assert_equal Comment.find(1), Comment.preload(:post => :comments).scoping { Comment.find(1) } + end + + test "circular preload does not modify unscoped" do + expected = FirstPost.unscoped.find(2) + FirstPost.preload(:comments => :first_post).find(1) + assert_equal expected, FirstPost.unscoped.find(2) + end + + test "preload ignores the scoping" do + assert_equal( + Comment.find(1).post, + Post.where('1 = 0').scoping { Comment.preload(:post).find(1).post } + ) + end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index f457dfb9b3..ed1caa2ef5 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 @@ -123,11 +123,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert active_record.developers.include?(david) end - def test_triple_equality - assert !(Array === Developer.find(1).projects) - assert Developer.find(1).projects === Array - end - def test_adding_single jamis = Developer.find(2) jamis.projects.reload # causing the collection to load @@ -338,6 +333,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, project.developers.size end + def test_uniq_when_association_already_loaded + project = projects(:active_record) + project.developers << [ developers(:jamis), developers(:david), developers(:jamis), developers(:david) ] + assert_equal 3, Project.includes(:developers).find(project.id).developers.size + end + def test_deleting david = Developer.find(1) active_record = Project.find(1) @@ -355,7 +356,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_deleting_array david = Developer.find(1) david.projects.reload - david.projects.delete(Project.find(:all)) + david.projects.delete(Project.all) assert_equal 0, david.projects.size assert_equal 0, david.projects(true).size end @@ -375,10 +376,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase active_record.developers.reload assert_equal 3, active_record.developers_by_sql.size - active_record.developers_by_sql.delete(Developer.find(:all)) + active_record.developers_by_sql.delete(Developer.all) assert_equal 0, active_record.developers_by_sql(true).size end + def test_deleting_all_with_sql + project = Project.find(1) + project.developers_by_sql.delete_all + assert_equal 0, project.developers_by_sql.size + end + def test_deleting_all david = Developer.find(1) david.projects.reload @@ -497,7 +504,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_include_uses_array_include_after_loaded project = projects(:active_record) - project.developers.class # force load target + project.developers.load_target developer = project.developers.first @@ -548,43 +555,23 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_find_with_merged_options assert_equal 1, projects(:active_record).limited_developers.size - assert_equal 1, projects(:active_record).limited_developers.find(:all).size - assert_equal 3, projects(:active_record).limited_developers.find(:all, :limit => nil).size + assert_equal 1, projects(:active_record).limited_developers.all.size + assert_equal 3, projects(:active_record).limited_developers.limit(nil).all.size end def test_dynamic_find_should_respect_association_order # Developers are ordered 'name DESC, id DESC' high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis') - assert_equal high_id_jamis, projects(:active_record).developers.find(:first, :conditions => "name = 'Jamis'") + assert_equal high_id_jamis, projects(:active_record).developers.scoped(:where => "name = 'Jamis'").first assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis') end - def test_dynamic_find_all_should_respect_association_order - # Developers are ordered 'name DESC, id DESC' - low_id_jamis = developers(:jamis) - middle_id_jamis = developers(:poor_jamis) - high_id_jamis = projects(:active_record).developers.create(:name => 'Jamis') - - assert_equal [high_id_jamis, middle_id_jamis, low_id_jamis], projects(:active_record).developers.find(:all, :conditions => "name = 'Jamis'") - assert_equal [high_id_jamis, middle_id_jamis, low_id_jamis], projects(:active_record).developers.find_all_by_name('Jamis') - end - def test_find_should_append_to_association_order ordered_developers = projects(:active_record).developers.order('projects.id') assert_equal ['developers.name desc, developers.id desc', 'projects.id'], ordered_developers.order_values end - def test_dynamic_find_all_should_respect_association_limit - assert_equal 1, projects(:active_record).limited_developers.find(:all, :conditions => "name = 'Jamis'").length - assert_equal 1, projects(:active_record).limited_developers.find_all_by_name('Jamis').length - end - - def test_dynamic_find_all_order_should_override_association_limit - assert_equal 2, projects(:active_record).limited_developers.find(:all, :conditions => "name = 'Jamis'", :limit => 9_000).length - assert_equal 2, projects(:active_record).limited_developers.find_all_by_name('Jamis', :limit => 9_000).length - end - def test_dynamic_find_all_should_respect_readonly_access projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid?} projects(:active_record).readonly_developers.each { |d| d.readonly? } @@ -632,7 +619,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_consider_type - developer = Developer.find(:first) + developer = Developer.first special_project = SpecialProject.create("name" => "Special Project") other_project = developer.projects.first @@ -667,7 +654,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase categories(:technology).select_testing_posts(true).each do |o| assert_respond_to o, :correctness_marker end - assert_respond_to categories(:technology).select_testing_posts.find(:first), :correctness_marker + assert_respond_to categories(:technology).select_testing_posts.first, :correctness_marker end def test_habtm_selects_all_columns_by_default @@ -681,10 +668,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_join_table_alias assert_equal( 3, - Developer.find( - :all, :include => {:projects => :developers}, :references => :developers_projects_join, - :conditions => 'developers_projects_join.joined_on IS NOT NULL' - ).size + Developer.references(:developers_projects_join).scoped( + :includes => {:projects => :developers}, + :where => 'developers_projects_join.joined_on IS NOT NULL' + ).to_a.size ) end @@ -697,16 +684,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal( 3, - Developer.find( - :all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL', - :references => :developers_projects_join, :group => group.join(",") - ).size + Developer.references(:developers_projects_join).scoped( + :includes => {:projects => :developers}, :where => 'developers_projects_join.joined_on IS NOT NULL', + :group => group.join(",") + ).to_a.size ) end def test_find_grouped - all_posts_from_category1 = Post.find(:all, :conditions => "category_id = 1", :joins => :categories) - grouped_posts_of_category1 = Post.find(:all, :conditions => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories) + all_posts_from_category1 = Post.scoped(:where => "category_id = 1", :joins => :categories).all + grouped_posts_of_category1 = Post.scoped(:where => "category_id = 1", :group => "author_id", :select => 'count(posts.id) as posts_count', :joins => :categories).all assert_equal 5, all_posts_from_category1.size assert_equal 2, grouped_posts_of_category1.size end @@ -780,8 +767,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, project.developers.size assert_equal 1, developer.projects.size - assert_equal developer, project.developers.find(:first) - assert_equal project, developer.projects.find(:first) + assert_equal developer, project.developers.first + assert_equal project, developer.projects.first end def test_self_referential_habtm_without_foreign_key_set_should_raise_exception @@ -798,13 +785,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert Category.find(1).posts_with_authors_sorted_by_author_id.find_by_title('Welcome to the weblog') end - def test_counting_on_habtm_association_and_not_array - david = Developer.find(1) - # Extra parameter just to make sure we aren't falling back to - # Array#count in Ruby >=1.8.7, which would raise an ArgumentError - assert_nothing_raised { david.projects.count(:all, :conditions => '1=1') } - end - def test_count david = Developer.find(1) assert_equal 2, david.projects.count @@ -827,7 +807,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_association_proxy_transaction_method_starts_transaction_in_association_class Post.expects(:transaction) - Category.find(:first).posts.transaction do + Category.first.posts.transaction do # nothing end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 3967009c82..8b384c2513 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -130,6 +130,28 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal car.id, bulb.car_id end + def test_association_protect_foreign_key + invoice = Invoice.create + + line_item = invoice.line_items.new + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.new :invoice_id => invoice.id + 1 + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.build + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.build :invoice_id => invoice.id + 1 + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.create + assert_equal invoice.id, line_item.invoice_id + + line_item = invoice.line_items.create :invoice_id => invoice.id + 1 + assert_equal invoice.id, line_item.invoice_id + end + def test_association_conditions_bypass_attribute_protection car = Car.create(:name => 'honda') @@ -199,48 +221,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal person, person.readers.first.person end - def test_find_or_create_by_resets_cached_counters - person = Person.create! :first_name => 'tenderlove' - post = Post.first - - assert_equal [], person.readers - assert_nil person.readers.find_by_post_id(post.id) - - person.readers.find_or_create_by_post_id(post.id) - - assert_equal 1, person.readers.count - assert_equal 1, person.readers.length - assert_equal post, person.readers.first.post - assert_equal person, person.readers.first.person - end - def force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.each {|f| } end # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first def test_counting_with_counter_sql - assert_equal 2, Firm.find(:first, :order => "id").clients.count + assert_equal 2, Firm.scoped(:order => "id").first.clients.count end def test_counting - assert_equal 2, Firm.find(:first, :order => "id").plain_clients.count - end - - def test_counting_with_empty_hash_conditions - assert_equal 2, Firm.find(:first, :order => "id").plain_clients.count(:conditions => {}) - end - - def test_counting_with_single_conditions - assert_equal 1, Firm.find(:first, :order => "id").plain_clients.count(:conditions => ['name=?', "Microsoft"]) + assert_equal 2, Firm.scoped(:order => "id").first.plain_clients.count end def test_counting_with_single_hash - assert_equal 1, Firm.find(:first, :order => "id").plain_clients.count(:conditions => {:name => "Microsoft"}) + assert_equal 1, Firm.scoped(:order => "id").first.plain_clients.where(:name => "Microsoft").count end def test_counting_with_column_name_and_hash - assert_equal 2, Firm.find(:first, :order => "id").plain_clients.count(:name) + assert_equal 2, Firm.scoped(:order => "id").first.plain_clients.count(:name) end def test_counting_with_association_limit @@ -250,7 +249,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding - assert_equal 2, Firm.find(:first, :order => "id").clients.length + assert_equal 2, Firm.scoped(:order => "id").first.clients.length end def test_finding_array_compatibility @@ -259,14 +258,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_with_blank_conditions [[], {}, nil, ""].each do |blank| - assert_equal 2, Firm.find(:first, :order => "id").clients.find(:all, :conditions => blank).size + assert_equal 2, Firm.scoped(:order => "id").first.clients.where(blank).all.size end end def test_find_many_with_merged_options assert_equal 1, companies(:first_firm).limited_clients.size - assert_equal 1, companies(:first_firm).limited_clients.find(:all).size - assert_equal 2, companies(:first_firm).limited_clients.find(:all, :limit => nil).size + assert_equal 1, companies(:first_firm).limited_clients.all.size + assert_equal 2, companies(:first_firm).limited_clients.limit(nil).all.size end def test_find_should_append_to_association_order @@ -274,97 +273,65 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal ['id DESC', 'companies.id'], ordered_clients.order_values end - def test_dynamic_find_last_without_specified_order - assert_equal companies(:second_client), companies(:first_firm).unsorted_clients.find_last_by_type('Client') - end - def test_dynamic_find_should_respect_association_order - assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find(:first, :conditions => "type = 'Client'") + assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.scoped(:where => "type = 'Client'").first assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client') end - def test_dynamic_find_all_should_respect_association_order - assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find(:all, :conditions => "type = 'Client'") - assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find_all_by_type('Client') - end - - def test_dynamic_find_all_should_respect_association_limit - assert_equal 1, companies(:first_firm).limited_clients.find(:all, :conditions => "type = 'Client'").length - assert_equal 1, companies(:first_firm).limited_clients.find_all_by_type('Client').length - end - - def test_dynamic_find_all_limit_should_override_association_limit - assert_equal 2, companies(:first_firm).limited_clients.find(:all, :conditions => "type = 'Client'", :limit => 9_000).length - assert_equal 2, companies(:first_firm).limited_clients.find_all_by_type('Client', :limit => 9_000).length - end - - def test_dynamic_find_all_should_respect_readonly_access - companies(:first_firm).readonly_clients.find(:all).each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } } - companies(:first_firm).readonly_clients.find(:all).each { |c| assert c.readonly? } - end - - def test_dynamic_find_or_create_from_two_attributes_using_an_association - author = authors(:david) - number_of_posts = Post.count - another = author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") - assert_equal number_of_posts + 1, Post.count - assert_equal another, author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") - assert another.persisted? - end - def test_cant_save_has_many_readonly_association authors(:david).readonly_comments.each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } } authors(:david).readonly_comments.each { |c| assert c.readonly? } end - def test_triple_equality - # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - assert !(Array === Firm.find(:first, :order => "id").clients) - assert Firm.find(:first, :order => "id").clients === Array - end - def test_finding_default_orders - assert_equal "Summit", Firm.find(:first, :order => "id").clients.first.name + assert_equal "Summit", Firm.scoped(:order => "id").first.clients.first.name end def test_finding_with_different_class_name_and_order - assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_sorted_desc.first.name + assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_sorted_desc.first.name end def test_finding_with_foreign_key - assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_of_firm.first.name + assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_of_firm.first.name end def test_finding_with_condition - assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_like_ms.first.name + assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_like_ms.first.name end def test_finding_with_condition_hash - assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_like_ms_with_hash_conditions.first.name + assert_equal "Microsoft", Firm.scoped(:order => "id").first.clients_like_ms_with_hash_conditions.first.name end def test_finding_using_primary_key - assert_equal "Summit", Firm.find(:first, :order => "id").clients_using_primary_key.first.name + assert_equal "Summit", Firm.scoped(:order => "id").first.clients_using_primary_key.first.name end def test_finding_using_sql - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first first_client = firm.clients_using_sql.first assert_not_nil first_client assert_equal "Microsoft", first_client.name assert_equal 1, firm.clients_using_sql.size - assert_equal 1, Firm.find(:first, :order => "id").clients_using_sql.size + assert_equal 1, Firm.scoped(:order => "id").first.clients_using_sql.size + end + + def test_finding_using_sql_take_into_account_only_uniq_ids + firm = Firm.scoped(:order => "id").first + client = firm.clients_using_sql.first + assert_equal client, firm.clients_using_sql.find(client.id, client.id) + assert_equal client, firm.clients_using_sql.find(client.id, client.id.to_s) end def test_counting_using_sql - assert_equal 1, Firm.find(:first, :order => "id").clients_using_counter_sql.size - assert Firm.find(:first, :order => "id").clients_using_counter_sql.any? - assert_equal 0, Firm.find(:first, :order => "id").clients_using_zero_counter_sql.size - assert !Firm.find(:first, :order => "id").clients_using_zero_counter_sql.any? + assert_equal 1, Firm.scoped(:order => "id").first.clients_using_counter_sql.size + assert Firm.scoped(:order => "id").first.clients_using_counter_sql.any? + assert_equal 0, Firm.scoped(:order => "id").first.clients_using_zero_counter_sql.size + assert !Firm.scoped(:order => "id").first.clients_using_zero_counter_sql.any? end def test_counting_non_existant_items_using_sql - assert_equal 0, Firm.find(:first, :order => "id").no_clients_using_counter_sql.size + assert_equal 0, Firm.scoped(:order => "id").first.no_clients_using_counter_sql.size end def test_counting_using_finder_sql @@ -379,7 +346,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_ids - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find } @@ -399,7 +366,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_string_ids_when_using_finder_sql - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first client = firm.clients_using_finder_sql.find("2") assert_kind_of Client, client @@ -415,9 +382,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_all - firm = Firm.find(:first, :order => "id") - assert_equal 2, firm.clients.find(:all, :conditions => "#{QUOTED_TYPE} = 'Client'").length - assert_equal 1, firm.clients.find(:all, :conditions => "name = 'Summit'").length + firm = Firm.scoped(:order => "id").first + assert_equal 2, firm.clients.scoped(:where => "#{QUOTED_TYPE} = 'Client'").all.length + assert_equal 1, firm.clients.scoped(:where => "name = 'Summit'").all.length end def test_find_each @@ -436,7 +403,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) assert_queries(2) do - firm.clients.find_each(:batch_size => 1, :conditions => {:name => "Microsoft"}) do |c| + firm.clients.where(name: 'Microsoft').find_each(batch_size: 1) do |c| assert_equal firm.id, c.firm_id assert_equal "Microsoft", c.name end @@ -461,29 +428,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_all_sanitized # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - firm = Firm.find(:first, :order => "id") - summit = firm.clients.find(:all, :conditions => "name = 'Summit'") - assert_equal summit, firm.clients.find(:all, :conditions => ["name = ?", "Summit"]) - assert_equal summit, firm.clients.find(:all, :conditions => ["name = :name", { :name => "Summit" }]) + firm = Firm.scoped(:order => "id").first + summit = firm.clients.scoped(:where => "name = 'Summit'").all + assert_equal summit, firm.clients.scoped(:where => ["name = ?", "Summit"]).all + assert_equal summit, firm.clients.scoped(:where => ["name = :name", { :name => "Summit" }]).all end def test_find_first - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first client2 = Client.find(2) - assert_equal firm.clients.first, firm.clients.find(:first, :order => "id") - assert_equal client2, firm.clients.find(:first, :conditions => "#{QUOTED_TYPE} = 'Client'", :order => "id") + assert_equal firm.clients.first, firm.clients.scoped(:order => "id").first + assert_equal client2, firm.clients.scoped(:where => "#{QUOTED_TYPE} = 'Client'", :order => "id").first end def test_find_first_sanitized - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first client2 = Client.find(2) - assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = ?", 'Client'], :order => "id") - assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }], :order => "id") + assert_equal client2, firm.clients.scoped(:where => ["#{QUOTED_TYPE} = ?", 'Client'], :order => "id").first + assert_equal client2, firm.clients.scoped(:where => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }], :order => "id").first end def test_find_all_with_include_and_conditions assert_nothing_raised do - Developer.find(:all, :joins => :audit_logs, :conditions => {'audit_logs.message' => nil, :name => 'Smith'}) + Developer.scoped(:joins => :audit_logs, :where => {'audit_logs.message' => nil, :name => 'Smith'}).all end end @@ -493,8 +460,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_grouped - all_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1") - grouped_clients_of_firm1 = Client.find(:all, :conditions => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count') + all_clients_of_firm1 = Client.scoped(:where => "firm_id = 1").all + grouped_clients_of_firm1 = Client.scoped(:where => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count').all assert_equal 2, all_clients_of_firm1.size assert_equal 1, grouped_clients_of_firm1.size end @@ -552,7 +519,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_with_bang_on_has_many_raises_when_record_not_saved assert_raise(ActiveRecord::RecordInvalid) do - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first firm.plain_clients.create! end end @@ -717,57 +684,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !companies(:first_firm).clients_of_firm.loaded? end - def test_find_or_initialize - the_client = companies(:first_firm).clients.find_or_initialize_by_name("Yet another client") - assert_equal companies(:first_firm).id, the_client.firm_id - assert_equal "Yet another client", the_client.name - assert !the_client.persisted? - end - - def test_find_or_create_updates_size - number_of_clients = companies(:first_firm).clients.size - the_client = companies(:first_firm).clients.find_or_create_by_name("Yet another client") - assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size - assert_equal the_client, companies(:first_firm).clients.find_or_create_by_name("Yet another client") - assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size - end - - def test_find_or_initialize_updates_collection_size - number_of_clients = companies(:first_firm).clients_of_firm.size - companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client") - assert_equal number_of_clients + 1, companies(:first_firm).clients_of_firm.size - end - - def test_find_or_initialize_returns_the_instantiated_object - client = companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client") - assert_equal client, companies(:first_firm).clients_of_firm[-1] - end - - def test_find_or_initialize_only_instantiates_a_single_object - number_of_clients = Client.count - companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client").save! - companies(:first_firm).save! - assert_equal number_of_clients+1, Client.count - end - - def test_find_or_create_with_hash - post = authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody') - assert_equal post, authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody') - assert post.persisted? - end - - def test_find_or_create_with_one_attribute_followed_by_hash - post = authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody') - assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody') - assert post.persisted? - end - - def test_find_or_create_should_work_with_block - post = authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'} - assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'} - assert post.persisted? - end - def test_deleting force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first) @@ -784,7 +700,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_deleting_updates_counter_cache - topic = Topic.first(:order => "id ASC") + topic = Topic.order("id ASC").first assert_equal topic.replies.to_a.size, topic.replies_count topic.replies.delete(topic.replies.first) @@ -876,12 +792,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase client_id = firm.clients_of_firm.first.id assert_equal 1, firm.clients_of_firm.size - cleared = firm.clients_of_firm.clear + firm.clients_of_firm.clear assert_equal 0, firm.clients_of_firm.size assert_equal 0, firm.clients_of_firm(true).size assert_equal [], Client.destroyed_client_ids[firm.id] - assert_equal firm.clients_of_firm.object_id, cleared.object_id # Should not be destroyed since the association is not dependent. assert_nothing_raised do @@ -947,11 +862,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Client.create(:client_of => firm.id, :name => "BigShot Inc.") Client.create(:client_of => firm.id, :name => "SmallTime Inc.") # only one of two clients is included in the association due to the :conditions key - assert_equal 2, Client.find_all_by_client_of(firm.id).size + assert_equal 2, Client.where(client_of: firm.id).size assert_equal 1, firm.dependent_conditional_clients_of_firm.size firm.destroy # only the correctly associated client should have been deleted - assert_equal 1, Client.find_all_by_client_of(firm.id).size + assert_equal 1, Client.where(client_of: firm.id).size end def test_dependent_association_respects_optional_sanitized_conditions_on_delete @@ -959,11 +874,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Client.create(:client_of => firm.id, :name => "BigShot Inc.") Client.create(:client_of => firm.id, :name => "SmallTime Inc.") # only one of two clients is included in the association due to the :conditions key - assert_equal 2, Client.find_all_by_client_of(firm.id).size + assert_equal 2, Client.where(client_of: firm.id).size assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size firm.destroy # only the correctly associated client should have been deleted - assert_equal 1, Client.find_all_by_client_of(firm.id).size + assert_equal 1, Client.where(client_of: firm.id).size end def test_dependent_association_respects_optional_hash_conditions_on_delete @@ -971,22 +886,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Client.create(:client_of => firm.id, :name => "BigShot Inc.") Client.create(:client_of => firm.id, :name => "SmallTime Inc.") # only one of two clients is included in the association due to the :conditions key - assert_equal 2, Client.find_all_by_client_of(firm.id).size + assert_equal 2, Client.where(client_of: firm.id).size assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size firm.destroy # only the correctly associated client should have been deleted - assert_equal 1, Client.find_all_by_client_of(firm.id).size + assert_equal 1, Client.where(client_of: firm.id).size end def test_delete_all_association_with_primary_key_deletes_correct_records - firm = Firm.find(:first) + firm = Firm.first # break the vanilla firm_id foreign key assert_equal 2, firm.clients.count firm.clients.first.update_column(:firm_id, nil) assert_equal 1, firm.clients(true).count assert_equal 1, firm.clients_using_primary_key_with_delete_all.count old_record = firm.clients_using_primary_key_with_delete_all.first - firm = Firm.find(:first) + firm = Firm.first firm.destroy assert_nil Client.find_by_id(old_record.id) end @@ -1094,7 +1009,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) assert_equal 2, firm.clients.size firm.destroy - assert Client.find(:all, :conditions => "firm_id=#{firm.id}").empty? + assert Client.scoped(:where => "firm_id=#{firm.id}").all.empty? end def test_dependence_for_associations_with_hash_condition @@ -1104,7 +1019,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_destroy_dependent_when_deleted_from_association # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first assert_equal 2, firm.clients.size client = firm.clients.first @@ -1132,7 +1047,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.destroy rescue "do nothing" - assert_equal 2, Client.find(:all, :conditions => "firm_id=#{firm.id}").size + assert_equal 2, Client.scoped(:where => "firm_id=#{firm.id}").all.size end def test_dependence_on_account @@ -1195,16 +1110,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_adding_array_and_collection - assert_nothing_raised { Firm.find(:first).clients + Firm.find(:all).last.clients } - end - - def test_find_all_without_conditions - firm = companies(:first_firm) - assert_equal 2, firm.clients.find(:all).length + assert_nothing_raised { Firm.first.clients + Firm.all.last.clients } end def test_replace_with_less - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first firm.clients = [companies(:first_client)] assert firm.save, "Could not save firm" firm.reload @@ -1218,7 +1128,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_replace_with_new - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first firm.clients = [companies(:second_client), Client.new("name" => "New Client")] firm.save firm.reload @@ -1318,29 +1228,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_dynamic_find_should_respect_association_order_for_through - assert_equal Comment.find(10), authors(:david).comments_desc.find(:first, :conditions => "comments.type = 'SpecialComment'") + assert_equal Comment.find(10), authors(:david).comments_desc.scoped(:where => "comments.type = 'SpecialComment'").first assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type('SpecialComment') end - def test_dynamic_find_all_should_respect_association_order_for_through - assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find(:all, :conditions => "comments.type = 'SpecialComment'") - assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find_all_by_type('SpecialComment') - end - - def test_dynamic_find_all_should_respect_association_limit_for_through - assert_equal 1, authors(:david).limited_comments.find(:all, :conditions => "comments.type = 'SpecialComment'").length - assert_equal 1, authors(:david).limited_comments.find_all_by_type('SpecialComment').length - end - - def test_dynamic_find_all_order_should_override_association_limit_for_through - assert_equal 4, authors(:david).limited_comments.find(:all, :conditions => "comments.type = 'SpecialComment'", :limit => 9_000).length - assert_equal 4, authors(:david).limited_comments.find_all_by_type('SpecialComment', :limit => 9_000).length - end - - def test_find_all_include_over_the_same_table_for_through - assert_equal 2, people(:michael).posts.find(:all, :include => :people).length - end - def test_has_many_through_respects_hash_conditions assert_equal authors(:david).hello_posts, authors(:david).hello_posts_with_hash_conditions assert_equal authors(:david).hello_post_comments, authors(:david).hello_post_comments_with_hash_conditions @@ -1348,7 +1239,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_include_uses_array_include_after_loaded firm = companies(:first_firm) - firm.clients.class # force load target + firm.clients.load_target client = firm.clients.first @@ -1398,7 +1289,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_first_or_last_on_loaded_association_should_not_fetch_with_query firm = companies(:first_firm) - firm.clients.class # force load target + firm.clients.load_target assert firm.clients.loaded? assert_no_queries do @@ -1452,13 +1343,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, author.essays.size end - assert_equal author.essays, Essay.find_all_by_writer_id("David") + assert_equal author.essays, Essay.where(writer_id: "David") end def test_has_many_custom_primary_key david = authors(:david) - assert_equal david.essays, Essay.find_all_by_writer_id("David") + assert_equal david.essays, Essay.where(writer_id: "David") end def test_blank_custom_primary_key_on_new_record_should_not_run_queries @@ -1470,17 +1361,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - def test_calling_first_or_last_with_find_options_on_loaded_association_should_fetch_with_query - firm = companies(:first_firm) - firm.clients.class # force load target - - assert_queries 2 do - assert firm.clients.loaded? - firm.clients.first(:order => 'name') - firm.clients.last(:order => 'name') - end - end - def test_calling_first_or_last_with_integer_on_association_should_load_association firm = companies(:first_firm) @@ -1538,11 +1418,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = Namespaced::Firm.create({ :name => 'Some Company' }) firm.clients.create({ :name => 'Some Client' }) - stats = Namespaced::Firm.find(firm.id, { + stats = Namespaced::Firm.scoped( :select => "#{Namespaced::Firm.table_name}.id, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients", :joins => :clients, :group => "#{Namespaced::Firm.table_name}.id" - }) + ).find firm.id assert_equal 1, stats.num_clients.to_i ensure @@ -1551,7 +1431,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_association_proxy_transaction_method_starts_transaction_in_association_class Comment.expects(:transaction) - Post.find(:first).comments.transaction do + Post.first.comments.transaction do # nothing end end @@ -1568,7 +1448,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_creating_using_primary_key - firm = Firm.find(:first, :order => "id") + firm = Firm.scoped(:order => "id").first client = firm.clients_using_primary_key.create!(:name => 'test') assert_equal firm.name, client.firm_name end @@ -1692,6 +1572,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [bulb2], car.reload.bulbs end + def test_replace_returns_target + car = Car.create(:name => 'honda') + bulb1 = car.bulbs.create + bulb2 = car.bulbs.create + bulb3 = Bulb.create + + assert_equal [bulb1, bulb2], car.bulbs + result = car.bulbs.replace([bulb3, bulb1]) + assert_equal [bulb1, bulb3], car.bulbs + assert_equal [bulb1, bulb3], result + end + def test_building_has_many_association_with_restrict_dependency option_before = ActiveRecord::Base.dependent_restrict_raises ActiveRecord::Base.dependent_restrict_raises = true @@ -1703,4 +1595,35 @@ class HasManyAssociationsTest < ActiveRecord::TestCase ensure ActiveRecord::Base.dependent_restrict_raises = option_before end + + def test_collection_association_with_private_kernel_method + firm = companies(:first_firm) + assert_equal [accounts(:signals37)], firm.accounts.open + end + + test "first_or_initialize adds the record to the association" do + firm = Firm.create! name: 'omg' + client = firm.clients_of_firm.first_or_initialize + assert_equal [client], firm.clients_of_firm + end + + test "first_or_create adds the record to the association" do + firm = Firm.create! name: 'omg' + firm.clients_of_firm.load_target + client = firm.clients_of_firm.first_or_create name: 'lol' + assert_equal [client], firm.clients_of_firm + assert_equal [client], firm.reload.clients_of_firm + end + + test "delete_all, when not loaded, doesn't load the records" do + post = posts(:welcome) + + assert post.taggings_with_delete_all.count > 0 + assert !post.taggings_with_delete_all.loaded? + + # 2 queries: one DELETE and another to update the counter cache + assert_queries(2) do + post.taggings_with_delete_all.delete_all + end + end end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 9cc09194dc..1c06007d86 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -44,17 +44,33 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_associate_existing - posts(:thinking); people(:david) # Warm cache + post = posts(:thinking) + person = people(:david) assert_queries(1) do - posts(:thinking).people << people(:david) + post.people << person end assert_queries(1) do - assert posts(:thinking).people.include?(people(:david)) + assert post.people.include?(person) end - assert posts(:thinking).reload.people(true).include?(people(:david)) + assert post.reload.people(true).include?(person) + end + + def test_associate_existing_with_strict_mass_assignment_sanitizer + SecureReader.mass_assignment_sanitizer = :strict + + SecureReader.new + + post = posts(:thinking) + person = people(:david) + + assert_queries(1) do + post.secure_people << person + end + ensure + SecureReader.mass_assignment_sanitizer = :logger end def test_associate_existing_record_twice_should_add_to_target_twice @@ -517,7 +533,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_count_with_include_should_alias_join_table - assert_equal 2, people(:michael).posts.count(:include => :readers) + assert_equal 2, people(:michael).posts.includes(:readers).count end def test_inner_join_with_quoted_table_name @@ -552,7 +568,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_association_proxy_transaction_method_starts_transaction_in_association_class Tag.expects(:transaction) - Post.find(:first).tags.transaction do + Post.first.tags.transaction do # nothing end end @@ -635,7 +651,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_collection_singular_ids_setter company = companies(:rails_core) - dev = Developer.find(:first) + dev = Developer.first company.developer_ids = [dev.id] assert_equal [dev], company.developers @@ -655,7 +671,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set company = companies(:rails_core) - ids = [Developer.find(:first).id, -9999] + ids = [Developer.first.id, -9999] assert_raises(ActiveRecord::RecordNotFound) {company.developer_ids= ids} end @@ -688,6 +704,17 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert author.comments.include?(comment) end + def test_through_association_readonly_should_be_false + assert !people(:michael).posts.first.readonly? + assert !people(:michael).posts.all.first.readonly? + end + + def test_can_update_through_association + assert_nothing_raised do + people(:michael).posts.first.update_attributes!(:title => "Can write") + end + end + def test_has_many_through_polymorphic_with_primary_key_option assert_equal [categories(:general)], authors(:david).essay_categories diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 246877bbed..88ec65706c 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -25,13 +25,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_queries(1) { assert_nil firm.account } assert_queries(0) { assert_nil firm.account } - firms = Firm.find(:all, :include => :account) + firms = Firm.scoped(:includes => :account).all assert_queries(0) { firms.each(&:account) } end def test_with_select assert_equal Firm.find(1).account_with_select.attributes.size, 2 - assert_equal Firm.find(1, :include => :account_with_select).account_with_select.attributes.size, 2 + assert_equal Firm.scoped(:includes => :account_with_select).find(1).account_with_select.attributes.size, 2 end def test_finding_using_primary_key @@ -294,13 +294,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_dependence_with_missing_association_and_nullify Account.destroy_all - firm = DependentFirm.find(:first) + firm = DependentFirm.first assert_nil firm.account firm.destroy end def test_finding_with_interpolated_condition - firm = Firm.find(:first) + firm = Firm.first superior = firm.clients.create(:name => 'SuperiorCo') superior.rating = 10 superior.save @@ -346,14 +346,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do Firm.find(@firm.id).save! - Firm.find(@firm.id, :include => :account).save! + Firm.scoped(:includes => :account).find(@firm.id).save! end @firm.account.destroy assert_nothing_raised do Firm.find(@firm.id).save! - Firm.find(@firm.id, :include => :account).save! + Firm.scoped(:includes => :account).find(@firm.id).save! end end @@ -448,6 +448,22 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal car.id, bulb.car_id end + def test_association_protect_foreign_key + pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?") + + ship = pirate.build_ship + assert_equal pirate.id, ship.pirate_id + + ship = pirate.build_ship :pirate_id => pirate.id + 1 + assert_equal pirate.id, ship.pirate_id + + ship = pirate.create_ship + assert_equal pirate.id, ship.pirate_id + + ship = pirate.create_ship :pirate_id => pirate.id + 1 + assert_equal pirate.id, ship.pirate_id + end + def test_association_conditions_bypass_attribute_protection car = Car.create(:name => 'honda') diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 2503349c08..94b9639e57 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -73,7 +73,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_eager_loading members = assert_queries(3) do #base table, through table, clubs table - Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"]) + Member.scoped(:includes => :club, :where => ["name = ?", "Groucho Marx"]).all end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].club} @@ -81,7 +81,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_eager_loading_through_polymorphic members = assert_queries(3) do #base table, through table, clubs table - Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"]) + Member.scoped(:includes => :sponsor_club, :where => ["name = ?", "Groucho Marx"]).all end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].sponsor_club} @@ -89,14 +89,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_with_conditions_eager_loading # conditions on the through table - assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :favourite_club).favourite_club + assert_equal clubs(:moustache_club), Member.scoped(:includes => :favourite_club).find(@member.id).favourite_club memberships(:membership_of_favourite_club).update_column(:favourite, false) - assert_equal nil, Member.find(@member.id, :include => :favourite_club).reload.favourite_club + assert_equal nil, Member.scoped(:includes => :favourite_club).find(@member.id).reload.favourite_club # conditions on the source table - assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :hairy_club).hairy_club + assert_equal clubs(:moustache_club), Member.scoped(:includes => :hairy_club).find(@member.id).hairy_club clubs(:moustache_club).update_column(:name, "Association of Clean-Shaven Persons") - assert_equal nil, Member.find(@member.id, :include => :hairy_club).reload.hairy_club + assert_equal nil, Member.scoped(:includes => :hairy_club).find(@member.id).reload.hairy_club end def test_has_one_through_polymorphic_with_source_type @@ -104,14 +104,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_eager_has_one_through_polymorphic_with_source_type - clubs = Club.find(:all, :include => :sponsored_member, :conditions => ["name = ?","Moustache and Eyebrow Fancier Club"]) + clubs = Club.scoped(:includes => :sponsored_member, :where => ["name = ?","Moustache and Eyebrow Fancier Club"]).all # Only the eyebrow fanciers club has a sponsored_member assert_not_nil assert_no_queries {clubs[0].sponsored_member} end def test_has_one_through_nonpreload_eagerloading members = assert_queries(1) do - Member.find(:all, :include => :club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback + Member.scoped(:includes => :club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name').all #force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].club} @@ -119,7 +119,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eager_loading_through_polymorphic members = assert_queries(1) do - Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback + Member.scoped(:includes => :sponsor_club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name').all #force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries {members[0].sponsor_club} @@ -128,7 +128,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record Sponsor.new(:sponsor_club => clubs(:crazy_club), :sponsorable => members(:groucho)).save! members = assert_queries(1) do - Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC') #force fallback + Member.scoped(:includes => :sponsor_club, :where => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC').all #force fallback end assert_equal 1, members.size assert_not_nil assert_no_queries { members[0].sponsor_club } @@ -197,7 +197,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase @member.member_detail = @member_detail @member.organization = @organization @member_details = assert_queries(3) do - MemberDetail.find(:all, :include => :member_type) + MemberDetail.scoped(:includes => :member_type).all end @new_detail = @member_details[0] assert @new_detail.send(:association, :member_type).loaded? @@ -210,14 +210,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do Club.find(@club.id).save! - Club.find(@club.id, :include => :sponsored_member).save! + Club.scoped(:includes => :sponsored_member).find(@club.id).save! end @club.sponsor.destroy assert_nothing_raised do Club.find(@club.id).save! - Club.find(@club.id, :include => :sponsored_member).save! + Club.scoped(:includes => :sponsored_member).find(@club.id).save! end end diff --git a/activerecord/test/cases/associations/identity_map_test.rb b/activerecord/test/cases/associations/identity_map_test.rb deleted file mode 100644 index 9b8635774c..0000000000 --- a/activerecord/test/cases/associations/identity_map_test.rb +++ /dev/null @@ -1,137 +0,0 @@ -require "cases/helper" -require 'models/author' -require 'models/post' - -if ActiveRecord::IdentityMap.enabled? -class InverseHasManyIdentityMapTest < ActiveRecord::TestCase - fixtures :authors, :posts - - def test_parent_instance_should_be_shared_with_every_child_on_find - m = Author.first - is = m.posts - is.each do |i| - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to child-owned instance" - end - end - - def test_parent_instance_should_be_shared_with_eager_loaded_children - m = Author.find(:first, :include => :posts) - is = m.posts - is.each do |i| - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to child-owned instance" - end - - m = Author.find(:first, :include => :posts, :order => 'posts.id') - is = m.posts - is.each do |i| - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to child-owned instance" - end - end - - def test_parent_instance_should_be_shared_with_newly_built_child - m = Author.first - i = m.posts.build(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to just-built-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_newly_block_style_built_child - m = Author.first - i = m.posts.build {|ii| ii.title = 'Industrial Revolution Re-enactment'; ii.body = 'Lorem ipsum'} - assert_not_nil i.title, "Child attributes supplied to build via blocks should be populated" - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to just-built-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_newly_created_child - m = Author.first - i = m.posts.create(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to newly-created-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child - m = Author.first - i = m.posts.create!(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to newly-created-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_newly_block_style_created_child - m = Author.first - i = m.posts.create {|ii| ii.title = 'Industrial Revolution Re-enactment'; ii.body = 'Lorem ipsum'} - assert_not_nil i.title, "Child attributes supplied to create via blocks should be populated" - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to newly-created-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_poked_in_child - m = Author.first - i = Post.create(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - m.posts << i - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to newly-created-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_replaced_via_accessor_children - m = Author.first - i = Post.new(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - m.posts = [i] - assert_same m, i.author - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to replaced-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_replaced_via_method_children - m = Author.first - i = Post.new(:title => 'Industrial Revolution Re-enactment', :body => 'Lorem ipsum') - m.posts = [i] - assert_not_nil i.author - assert_equal m.name, i.author.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to parent instance" - i.author.name = 'Mungo' - assert_equal m.name, i.author.name, "Name of man should be the same after changes to replaced-child-owned instance" - end -end -end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 68a1e62328..1d61d5c474 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -72,17 +72,17 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_count_honors_implicit_inner_joins real_count = Author.scoped.to_a.sum{|a| a.posts.count } - assert_equal real_count, Author.count(:joins => :posts), "plain inner join count should match the number of referenced posts records" + assert_equal real_count, Author.joins(:posts).count, "plain inner join count should match the number of referenced posts records" end def test_calculate_honors_implicit_inner_joins real_count = Author.scoped.to_a.sum{|a| a.posts.count } - assert_equal real_count, Author.calculate(:count, 'authors.id', :joins => :posts), "plain inner join count should match the number of referenced posts records" + assert_equal real_count, Author.joins(:posts).calculate(:count, 'authors.id'), "plain inner join count should match the number of referenced posts records" end def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions real_count = Author.scoped.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length - authors_with_welcoming_post_titles = Author.calculate(:count, 'authors.id', :joins => :posts, :distinct => true, :conditions => "posts.title like 'Welcome%'") + authors_with_welcoming_post_titles = Author.scoped(:joins => :posts, :where => "posts.title like 'Welcome%'").calculate(:count, 'authors.id', :distinct => true) assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 76282213d8..f35ffb2994 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -96,7 +96,7 @@ class InverseHasOneTests < ActiveRecord::TestCase def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find - m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face) + m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :face).first f = m.face assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = 'Bongo' @@ -104,7 +104,7 @@ class InverseHasOneTests < ActiveRecord::TestCase f.man.name = 'Mungo' assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance" - m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face, :order => 'faces.id') + m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :face, :order => 'faces.id').first f = m.face assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = 'Bongo' @@ -114,7 +114,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_built_child - m = Man.find(:first) + m = Man.first f = m.build_face(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" @@ -125,7 +125,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_created_child - m = Man.find(:first) + m = Man.first f = m.create_face(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" @@ -136,7 +136,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method - m = Man.find(:first) + m = Man.first f = m.create_face!(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" @@ -147,7 +147,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_replaced_via_accessor_child - m = Man.find(:first) + m = Man.first f = Face.new(:description => 'haunted') m.face = f assert_not_nil f.man @@ -159,7 +159,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).dirty_face } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.dirty_face } end end @@ -179,7 +179,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_eager_loaded_children - m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests) + m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :interests).first is = m.interests is.each do |i| assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -189,7 +189,7 @@ class InverseHasManyTests < ActiveRecord::TestCase assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance" end - m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests, :order => 'interests.id') + m = Man.scoped(:where => {:name => 'Gordon'}, :includes => :interests, :order => 'interests.id').first is = m.interests is.each do |i| assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -201,7 +201,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_block_style_built_child - m = Man.find(:first) + m = Man.first i = m.interests.build {|ii| ii.topic = 'Industrial Revolution Re-enactment'} assert_not_nil i.topic, "Child attributes supplied to build via blocks should be populated" assert_not_nil i.man @@ -213,7 +213,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child - m = Man.find(:first) + m = Man.first i = m.interests.create!(:topic => 'Industrial Revolution Re-enactment') assert_not_nil i.man assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -224,7 +224,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_newly_block_style_created_child - m = Man.find(:first) + m = Man.first i = m.interests.create {|ii| ii.topic = 'Industrial Revolution Re-enactment'} assert_not_nil i.topic, "Child attributes supplied to create via blocks should be populated" assert_not_nil i.man @@ -248,7 +248,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_replaced_via_accessor_children - m = Man.find(:first) + m = Man.first i = Interest.new(:topic => 'Industrial Revolution Re-enactment') m.interests = [i] assert_not_nil i.man @@ -260,7 +260,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).secret_interests } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests } end end @@ -278,7 +278,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase end def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find - f = Face.find(:first, :include => :man, :conditions => {:description => 'trusting'}) + f = Face.scoped(:includes => :man, :where => {:description => 'trusting'}).first m = f.man assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -286,7 +286,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase m.face.description = 'pleasing' assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance" - f = Face.find(:first, :include => :man, :order => 'men.id', :conditions => {:description => 'trusting'}) + f = Face.scoped(:includes => :man, :order => 'men.id', :where => {:description => 'trusting'}).first m = f.man assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -331,7 +331,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase end def test_child_instance_should_be_shared_with_replaced_via_accessor_parent - f = Face.find(:first) + f = Face.first m = Man.new(:name => 'Charles') f.man = m assert_not_nil m.face @@ -343,7 +343,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase end def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_man } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_man } end end @@ -351,7 +351,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase fixtures :men, :faces, :interests def test_child_instance_should_be_shared_with_parent_on_find - f = Face.find(:first, :conditions => {:description => 'confused'}) + f = Face.scoped(:where => {:description => 'confused'}).first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -361,7 +361,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase end def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find - f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man) + f = Face.scoped(:where => {:description => 'confused'}, :includes => :man).first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -369,7 +369,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase m.polymorphic_face.description = 'pleasing' assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance" - f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man, :order => 'men.id') + f = Face.scoped(:where => {:description => 'confused'}, :includes => :man, :order => 'men.id').first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = 'gormless' @@ -421,19 +421,19 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error # Ideally this would, if only for symmetry's sake with other association types - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man } + assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man } end def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error # fails because no class has the correct inverse_of for horrible_polymorphic_man - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man = Man.first } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man = Man.first } end def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error # passes because Man does have the correct inverse_of - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Man.first } + assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Man.first } # fails because Interest does have the correct inverse_of - assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Interest.first } + assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end end @@ -444,7 +444,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do - i = Interest.find(:first) + i = Interest.first i.zine i.man end @@ -452,7 +452,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do - i = Interest.find(:first) + i = Interest.first i.build_zine(:title => 'Get Some in Winter! 2008') i.build_man(:name => 'Gordon') i.save! diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 301755249c..ecc676f300 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -46,16 +46,12 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert !authors(:mary).unique_categorized_posts.loaded? assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count } assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title) } - assert_queries(1) { assert_equal 0, author.unique_categorized_posts.count(:title, :conditions => "title is NULL") } + assert_queries(1) { assert_equal 0, author.unique_categorized_posts.where(title: nil).count(:title) } assert !authors(:mary).unique_categorized_posts.loaded? end def test_has_many_uniq_through_find - assert_equal 1, authors(:mary).unique_categorized_posts.find(:all).size - end - - def test_has_many_uniq_through_dynamic_find - assert_equal 1, authors(:mary).unique_categorized_posts.find_all_by_title("So I was thinking").size + assert_equal 1, authors(:mary).unique_categorized_posts.all.size end def test_polymorphic_has_many_going_through_join_model @@ -71,7 +67,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_polymorphic_has_many_going_through_join_model_with_find - assert_equal tags(:general), tag = posts(:welcome).tags.find(:first) + assert_equal tags(:general), tag = posts(:welcome).tags.first assert_no_queries do tag.tagging end @@ -85,7 +81,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_polymorphic_has_many_going_through_join_model_with_include_on_source_reflection_with_find - assert_equal tags(:general), tag = posts(:welcome).funky_tags.find(:first) + assert_equal tags(:general), tag = posts(:welcome).funky_tags.first assert_no_queries do tag.tagging end @@ -237,8 +233,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_has_many_through - posts = Post.find(:all, :order => 'posts.id') - posts_with_authors = Post.find(:all, :include => :authors, :order => 'posts.id') + posts = Post.scoped(:order => 'posts.id').all + posts_with_authors = Post.scoped(:includes => :authors, :order => 'posts.id').all assert_equal posts.length, posts_with_authors.length posts.length.times do |i| assert_equal posts[i].authors.length, assert_no_queries { posts_with_authors[i].authors.length } @@ -246,7 +242,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_one - post = Post.find_by_id(posts(:welcome).id, :include => :tagging) + post = Post.includes(:tagging).find posts(:welcome).id tagging = taggings(:welcome_general) assert_no_queries do assert_equal tagging, post.tagging @@ -254,7 +250,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_one_defined_in_abstract_parent - item = Item.find_by_id(items(:dvd).id, :include => :tagging) + item = Item.includes(:tagging).find items(:dvd).id tagging = taggings(:godfather) assert_no_queries do assert_equal tagging, item.tagging @@ -262,8 +258,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_many_through - posts = Post.find(:all, :order => 'posts.id') - posts_with_tags = Post.find(:all, :include => :tags, :order => 'posts.id') + posts = Post.scoped(:order => 'posts.id').all + posts_with_tags = Post.scoped(:includes => :tags, :order => 'posts.id').all assert_equal posts.length, posts_with_tags.length posts.length.times do |i| assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length } @@ -271,8 +267,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_polymorphic_has_many - posts = Post.find(:all, :order => 'posts.id') - posts_with_taggings = Post.find(:all, :include => :taggings, :order => 'posts.id') + posts = Post.scoped(:order => 'posts.id').all + posts_with_taggings = Post.scoped(:includes => :taggings, :order => 'posts.id').all assert_equal posts.length, posts_with_taggings.length posts.length.times do |i| assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length } @@ -280,25 +276,20 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_find_all - assert_equal [categories(:general)], authors(:david).categories.find(:all) + assert_equal [categories(:general)], authors(:david).categories.all end def test_has_many_find_first - assert_equal categories(:general), authors(:david).categories.find(:first) + assert_equal categories(:general), authors(:david).categories.first end def test_has_many_with_hash_conditions - assert_equal categories(:general), authors(:david).categories_like_general.find(:first) + assert_equal categories(:general), authors(:david).categories_like_general.first end def test_has_many_find_conditions - assert_equal categories(:general), authors(:david).categories.find(:first, :conditions => "categories.name = 'General'") - assert_nil authors(:david).categories.find(:first, :conditions => "categories.name = 'Technology'") - end - - def test_has_many_class_methods_called_by_method_missing - assert_equal categories(:general), authors(:david).categories.find_all_by_name('General').first - assert_nil authors(:david).categories.find_by_name('Technology') + assert_equal categories(:general), authors(:david).categories.scoped(:where => "categories.name = 'General'").first + assert_nil authors(:david).categories.scoped(:where => "categories.name = 'Technology'").first end def test_has_many_array_methods_called_by_method_missing @@ -327,14 +318,6 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_equal [authors(:david), authors(:bob)], posts(:thinking).authors_using_custom_pk.order('authors.id') end - def test_both_scoped_and_explicit_joins_should_be_respected - assert_nothing_raised do - Post.send(:with_scope, :find => {:joins => "left outer join comments on comments.id = posts.id"}) do - Post.find :all, :select => "comments.id, authors.id", :joins => "left outer join authors on authors.id = posts.author_id" - end - end - end - def test_belongs_to_polymorphic_with_counter_cache assert_equal 1, posts(:welcome)[:taggings_count] tagging = posts(:welcome).taggings.create(:tag => tags(:general)) @@ -362,7 +345,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end assert_raise ActiveRecord::EagerLoadPolymorphicError do - tags(:general).taggings.find(:all, :include => :taggable, :references => :bogus_table, :conditions => 'bogus_table.column = 1') + tags(:general).taggings.includes(:taggable).where('bogus_table.column = 1').references(:bogus_table).to_a end end @@ -372,7 +355,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_has_many_polymorphic_with_source_type - tag_with_include = Tag.find(tags(:general).id, :include => :tagged_posts) + tag_with_include = Tag.scoped(:includes => :tagged_posts).find(tags(:general).id) desired = posts(:welcome, :thinking) assert_no_queries do # added sort by ID as otherwise test using JRuby was failing as array elements were in different order @@ -382,20 +365,20 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_has_many_find_all - assert_equal comments(:greetings), authors(:david).comments.find(:all, :order => 'comments.id').first + assert_equal comments(:greetings), authors(:david).comments.scoped(:order => 'comments.id').all.first end def test_has_many_through_has_many_find_all_with_custom_class - assert_equal comments(:greetings), authors(:david).funky_comments.find(:all, :order => 'comments.id').first + assert_equal comments(:greetings), authors(:david).funky_comments.scoped(:order => 'comments.id').all.first end def test_has_many_through_has_many_find_first - assert_equal comments(:greetings), authors(:david).comments.find(:first, :order => 'comments.id') + assert_equal comments(:greetings), authors(:david).comments.scoped(:order => 'comments.id').first end def test_has_many_through_has_many_find_conditions - options = { :conditions => "comments.#{QUOTED_TYPE}='SpecialComment'", :order => 'comments.id' } - assert_equal comments(:does_it_hurt), authors(:david).comments.find(:first, options) + options = { :where => "comments.#{QUOTED_TYPE}='SpecialComment'", :order => 'comments.id' } + assert_equal comments(:does_it_hurt), authors(:david).comments.scoped(options).first end def test_has_many_through_has_many_find_by_id @@ -411,7 +394,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_include_has_many_through_polymorphic_has_many - author = Author.find_by_id(authors(:david).id, :include => :taggings) + author = Author.includes(:taggings).find authors(:david).id expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } @@ -419,7 +402,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_load_has_many_through_has_many - author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id' + author = Author.scoped(:where => ['name = ?', 'David'], :includes => :comments, :order => 'comments.id').first SpecialComment.new; VerySpecialComment.new assert_no_queries do assert_equal [1,2,3,5,6,7,8,9,10,12], author.comments.collect(&:id) @@ -427,7 +410,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_eager_load_has_many_through_has_many_with_conditions - post = Post.find(:first, :include => :invalid_tags) + post = Post.scoped(:includes => :invalid_tags).first assert_no_queries do post.invalid_tags end @@ -435,8 +418,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_eager_belongs_to_and_has_one_not_singularized assert_nothing_raised do - Author.find(:first, :include => :author_address) - AuthorAddress.find(:first, :include => :author) + Author.scoped(:includes => :author_address).first + AuthorAddress.scoped(:includes => :author).first end end @@ -452,7 +435,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_uses_conditions_specified_on_the_has_many_association - author = Author.find(:first) + author = Author.first assert_present author.comments assert_blank author.nonexistant_comments end @@ -622,7 +605,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_many expected = taggings(:welcome_general) - p = Post.find(posts(:welcome).id, :include => :taggings) + p = Post.scoped(:includes => :taggings).find(posts(:welcome).id) assert_no_queries {assert p.taggings.include?(expected)} assert posts(:welcome).taggings.include?(taggings(:welcome_general)) end @@ -630,18 +613,18 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_one expected = posts(:welcome) - tagging = Tagging.find(taggings(:welcome_general).id, :include => :taggable) + tagging = Tagging.scoped(:includes => :taggable).find(taggings(:welcome_general).id) assert_no_queries { assert_equal expected, tagging.taggable} end def test_polymorphic_belongs_to - p = Post.find(posts(:welcome).id, :include => {:taggings => :taggable}) + p = Post.scoped(:includes => {:taggings => :taggable}).find(posts(:welcome).id) assert_no_queries {assert_equal posts(:welcome), p.taggings.first.taggable} end def test_preload_polymorphic_has_many_through - posts = Post.find(:all, :order => 'posts.id') - posts_with_tags = Post.find(:all, :include => :tags, :order => 'posts.id') + posts = Post.scoped(:order => 'posts.id').all + posts_with_tags = Post.scoped(:includes => :tags, :order => 'posts.id').all assert_equal posts.length, posts_with_tags.length posts.length.times do |i| assert_equal posts[i].tags.length, assert_no_queries { posts_with_tags[i].tags.length } @@ -649,7 +632,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_preload_polymorph_many_types - taggings = Tagging.find :all, :include => :taggable, :conditions => ['taggable_type != ?', 'FakeModel'] + taggings = Tagging.scoped(:includes => :taggable, :where => ['taggable_type != ?', 'FakeModel']).all assert_no_queries do taggings.first.taggable.id taggings[1].taggable.id @@ -662,13 +645,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_preload_nil_polymorphic_belongs_to assert_nothing_raised do - Tagging.find(:all, :include => :taggable, :conditions => ['taggable_type IS NULL']) + Tagging.scoped(:includes => :taggable, :where => ['taggable_type IS NULL']).all end end def test_preload_polymorphic_has_many - posts = Post.find(:all, :order => 'posts.id') - posts_with_taggings = Post.find(:all, :include => :taggings, :order => 'posts.id') + posts = Post.scoped(:order => 'posts.id').all + posts_with_taggings = Post.scoped(:includes => :taggings, :order => 'posts.id').all assert_equal posts.length, posts_with_taggings.length posts.length.times do |i| assert_equal posts[i].taggings.length, assert_no_queries { posts_with_taggings[i].taggings.length } @@ -676,7 +659,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_belongs_to_shared_parent - comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 1') + comments = Comment.scoped(:includes => :post, :where => 'post_id = 1').all assert_no_queries do assert_equal comments.first.post, comments[1].post end @@ -684,7 +667,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_include_uses_array_include_after_loaded david = authors(:david) - david.categories.class # force load target + david.categories.load_target category = david.categories.first diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 017905e0ac..1d0550afaf 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -74,8 +74,8 @@ class AssociationsTest < ActiveRecord::TestCase def test_include_with_order_works - assert_nothing_raised {Account.find(:first, :order => 'id', :include => :firm)} - assert_nothing_raised {Account.find(:first, :order => :id, :include => :firm)} + assert_nothing_raised {Account.scoped(:order => 'id', :includes => :firm).first} + assert_nothing_raised {Account.scoped(:order => :id, :includes => :firm).first} end def test_bad_collection_keys @@ -214,6 +214,10 @@ class AssociationProxyTest < ActiveRecord::TestCase david = developers(:david) assert_equal david.association(:projects), david.projects.proxy_association end + + def test_scoped_allows_conditions + assert developers(:david).projects.scoped(where: 'foo').where_values.include?('foo') + end end class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 764305459d..98c38535a6 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -6,10 +6,6 @@ module ActiveRecord module AttributeMethods class ReadTest < ActiveRecord::TestCase class FakeColumn < Struct.new(:name) - def type_cast_code(var) - var - end - def type; :integer; end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 23bdb09514..1093fedea1 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -30,9 +30,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase t = Topic.new t.title = "hello there!" t.written_on = Time.now + t.author_name = "" assert t.attribute_present?("title") assert t.attribute_present?("written_on") assert !t.attribute_present?("content") + assert !t.attribute_present?("author_name") + end def test_attribute_present_with_booleans @@ -241,7 +244,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase # DB2 is not case-sensitive return true if current_adapter?(:DB2Adapter) - assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.find(:first).attributes + assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes end def test_hashes_not_mangled @@ -478,23 +481,23 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_typecast_attribute_from_select_to_false - topic = Topic.create(:title => 'Budget') + Topic.create(:title => 'Budget') # Oracle does not support boolean expressions in SELECT if current_adapter?(:OracleAdapter) - topic = Topic.find(:first, :select => "topics.*, 0 as is_test") + topic = Topic.scoped(:select => "topics.*, 0 as is_test").first else - topic = Topic.find(:first, :select => "topics.*, 1=2 as is_test") + topic = Topic.scoped(:select => "topics.*, 1=2 as is_test").first end assert !topic.is_test? end def test_typecast_attribute_from_select_to_true - topic = Topic.create(:title => 'Budget') + Topic.create(:title => 'Budget') # Oracle does not support boolean expressions in SELECT if current_adapter?(:OracleAdapter) - topic = Topic.find(:first, :select => "topics.*, 1 as is_test") + topic = Topic.scoped(:select => "topics.*, 1 as is_test").first else - topic = Topic.find(:first, :select => "topics.*, 2=2 as is_test") + topic = Topic.scoped(:select => "topics.*, 2=2 as is_test").first end assert topic.is_test? end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 1376810adf..8ef3bfef15 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -87,7 +87,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end def test_save_fails_for_invalid_has_one - firm = Firm.find(:first) + firm = Firm.first assert firm.valid? firm.build_account @@ -99,7 +99,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end def test_save_succeeds_for_invalid_has_one_with_validate_false - firm = Firm.find(:first) + firm = Firm.first assert firm.valid? firm.build_unvalidated_account @@ -155,20 +155,20 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end def test_not_resaved_when_unchanged - firm = Firm.find(:first, :include => :account) + firm = Firm.scoped(:includes => :account).first firm.name += '-changed' assert_queries(1) { firm.save! } - firm = Firm.find(:first) - firm.account = Account.find(:first) + firm = Firm.first + firm.account = Account.first assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! } - firm = Firm.find(:first).dup - firm.account = Account.find(:first) + firm = Firm.first.dup + firm.account = Account.first assert_queries(2) { firm.save! } - firm = Firm.find(:first).dup - firm.account = Account.find(:first).dup + firm = Firm.first.dup + firm.account = Account.first.dup assert_queries(2) { firm.save! } end @@ -228,7 +228,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test end def test_assignment_before_parent_saved - client = Client.find(:first) + client = Client.first apple = Firm.new("name" => "Apple") client.firm = apple assert_equal apple, client.firm @@ -342,7 +342,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test end def test_build_and_then_save_parent_should_not_reload_target - client = Client.find(:first) + client = Client.first apple = client.build_firm(:name => "Apple") client.save! assert_no_queries { assert_equal apple, client.firm } @@ -384,7 +384,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa end def test_invalid_adding_with_validate_false - firm = Firm.find(:first) + firm = Firm.first client = Client.new firm.unvalidated_clients_of_firm << client @@ -397,7 +397,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_valid_adding_with_validate_false no_of_clients = Client.count - firm = Firm.find(:first) + firm = Firm.first client = Client.new("name" => "Apple") assert firm.valid? @@ -627,7 +627,9 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_a_child_marked_for_destruction_should_not_be_destroyed_twice @pirate.ship.mark_for_destruction assert @pirate.save - @pirate.ship.expects(:destroy).never + class << @pirate.ship + def destroy; raise "Should not be called" end + end assert @pirate.save end @@ -672,7 +674,9 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_a_parent_marked_for_destruction_should_not_be_destroyed_twice @ship.pirate.mark_for_destruction assert @ship.save - @ship.pirate.expects(:destroy).never + class << @ship.pirate + def destroy; raise "Should not be called" end + end assert @ship.save end @@ -767,6 +771,16 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase assert_equal before, @pirate.reload.birds end + def test_when_new_record_a_child_marked_for_destruction_should_not_affect_other_records_from_saving + @pirate = @ship.build_pirate(:catchphrase => "Arr' now I shall keep me eye on you matey!") # new record + + 3.times { |i| @pirate.birds.build(:name => "birds_#{i}") } + @pirate.birds[1].mark_for_destruction + @pirate.save! + + assert_equal 2, @pirate.birds.reload.length + end + # Add and remove callbacks tests for association collections. %w{ method proc }.each do |callback_type| define_method("test_should_run_add_callback_#{callback_type}s_for_has_many") do @@ -858,7 +872,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") } before = @pirate.parrots.map { |c| c.mark_for_destruction ; c } - class << @pirate.parrots + class << @pirate.association(:parrots) def destroy(*args) super raise 'Oh noes!' @@ -978,10 +992,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase values = [@pirate.reload.catchphrase, @pirate.ship.name, *@pirate.ship.parts.map(&:name)] # Oracle saves empty string as NULL if current_adapter?(:OracleAdapter) - expected = ActiveRecord::IdentityMap.enabled? ? - [nil, nil, '', ''] : - [nil, nil, nil, nil] - assert_equal expected, values + assert_equal [nil, nil, nil, nil], values else assert_equal ['', '', '', ''], values end @@ -1077,8 +1088,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase @ship.save(:validate => false) # Oracle saves empty string as NULL if current_adapter?(:OracleAdapter) - expected = ActiveRecord::IdentityMap.enabled? ? [nil, ''] : [nil, nil] - assert_equal expected, [@ship.reload.name, @ship.pirate.catchphrase] + assert_equal [nil, nil], [@ship.reload.name, @ship.pirate.catchphrase] else assert_equal ['', ''], [@ship.reload.name, @ship.pirate.catchphrase] end @@ -1267,7 +1277,7 @@ module AutosaveAssociationOnACollectionAssociationTests def test_should_not_load_the_associated_models_if_they_were_not_loaded_yet assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! } - @pirate.send(@association_name).class # hack to load the target + @pirate.send(@association_name).load_target assert_queries(3) do @pirate.catchphrase = 'Yarr' diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 698c3d0cb1..619fb881fa 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -62,7 +62,11 @@ end class Weird < ActiveRecord::Base; end -class Boolean < ActiveRecord::Base; end +class Boolean < ActiveRecord::Base + def has_fun + super + end +end class LintTest < ActiveRecord::TestCase include ActiveModel::Lint::Tests @@ -121,20 +125,13 @@ class BasicsTest < ActiveRecord::TestCase unless current_adapter?(:PostgreSQLAdapter,:OracleAdapter,:SQLServerAdapter) def test_limit_with_comma - assert_nothing_raised do - Topic.limit("1,2").all - end + assert Topic.limit("1,2").all end end def test_limit_without_comma - assert_nothing_raised do - assert_equal 1, Topic.limit("1").all.length - end - - assert_nothing_raised do - assert_equal 1, Topic.limit(1).all.length - end + assert_equal 1, Topic.limit("1").all.length + assert_equal 1, Topic.limit(1).all.length end def test_invalid_limit @@ -163,7 +160,7 @@ class BasicsTest < ActiveRecord::TestCase def test_select_symbol topic_ids = Topic.select(:id).map(&:id).sort - assert_equal Topic.all.map(&:id).sort, topic_ids + assert_equal Topic.pluck(:id).sort, topic_ids end def test_table_exists @@ -188,7 +185,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_previously_changed - topic = Topic.find :first + topic = Topic.first topic.title = '<3<3<3' assert_equal({}, topic.previous_changes) @@ -198,7 +195,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_previously_changed_dup - topic = Topic.find :first + topic = Topic.first topic.title = '<3<3<3' topic.save! @@ -224,7 +221,7 @@ class BasicsTest < ActiveRecord::TestCase ) # For adapters which support microsecond resolution. - if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLiteAdapter) + if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLite3Adapter) assert_equal 11, Topic.find(1).written_on.sec assert_equal 223300, Topic.find(1).written_on.usec assert_equal 9900, Topic.find(2).written_on.usec @@ -346,13 +343,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_load - topics = Topic.find(:all, :order => 'id') + topics = Topic.scoped(:order => 'id').all assert_equal(4, topics.size) assert_equal(topics(:first).title, topics.first.title) end def test_load_with_condition - topics = Topic.find(:all, :conditions => "author_name = 'Mary'") + topics = Topic.scoped(:where => "author_name = 'Mary'").all assert_equal(1, topics.size) assert_equal(topics(:second).title, topics.first.title) @@ -474,7 +471,7 @@ class BasicsTest < ActiveRecord::TestCase if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) def test_update_all_with_order_and_limit - assert_equal 1, Topic.update_all("content = 'bulk updated!'", nil, :limit => 1, :order => 'id DESC') + assert_equal 1, Topic.limit(1).order('id DESC').update_all(:content => 'bulk updated!') end end @@ -507,7 +504,7 @@ class BasicsTest < ActiveRecord::TestCase end # Oracle, and Sybase do not have a TIME datatype. - unless current_adapter?(:OracleAdapter, :SybaseAdapter) + unless current_adapter?(:OracleAdapter, :SybaseAdapter, :SQLite3Adapter) def test_utc_as_time_zone Topic.default_timezone = :utc attributes = { "bonus_time" => "5:42:00AM" } @@ -749,6 +746,9 @@ class BasicsTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing + ActiveRecord::Base.time_zone_aware_attributes = false + ActiveRecord::Base.default_timezone = :local + Time.zone = nil attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", "written_on(5i)" => "12", "written_on(6i)" => "02" @@ -855,7 +855,7 @@ class BasicsTest < ActiveRecord::TestCase end # Oracle, and Sybase do not have a TIME datatype. - unless current_adapter?(:OracleAdapter, :SybaseAdapter) + unless current_adapter?(:OracleAdapter, :SybaseAdapter, :SQLite3Adapter) def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion ActiveRecord::Base.time_zone_aware_attributes = true ActiveRecord::Base.default_timezone = :utc @@ -876,6 +876,9 @@ class BasicsTest < ActiveRecord::TestCase end def test_multiparameter_attributes_on_time_with_empty_seconds + ActiveRecord::Base.time_zone_aware_attributes = false + ActiveRecord::Base.default_timezone = :local + Time.zone = nil attributes = { "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" @@ -931,7 +934,7 @@ class BasicsTest < ActiveRecord::TestCase def test_attributes_on_dummy_time # Oracle, and Sybase do not have a TIME datatype. - return true if current_adapter?(:OracleAdapter, :SybaseAdapter) + return true if current_adapter?(:OracleAdapter, :SybaseAdapter, :SQLite3Adapter) attributes = { "bonus_time" => "5:42:00AM" @@ -957,6 +960,16 @@ class BasicsTest < ActiveRecord::TestCase assert b_true.value? end + def test_boolean_without_questionmark + b_true = Boolean.create({ "value" => true }) + true_id = b_true.id + + subclass = Class.new(Boolean).find true_id + superclass = Boolean.find true_id + + assert_equal superclass.read_attribute(:has_fun), subclass.read_attribute(:has_fun) + end + def test_boolean_cast_from_string b_blank = Boolean.create({ "value" => "" }) blank_id = b_blank.id @@ -1139,7 +1152,7 @@ class BasicsTest < ActiveRecord::TestCase assert g.save # Reload and check that we have all the geometric attributes. - h = ActiveRecord::IdentityMap.without { Geometric.find(g.id) } + h = Geometric.find(g.id) assert_equal '(5,6.1)', h.a_point assert_equal '[(2,3),(5.5,7)]', h.a_line_segment @@ -1168,7 +1181,7 @@ class BasicsTest < ActiveRecord::TestCase assert g.save # Reload and check that we have all the geometric attributes. - h = ActiveRecord::IdentityMap.without { Geometric.find(g.id) } + h = Geometric.find(g.id) assert_equal '(5,6.1)', h.a_point assert_equal '[(2,3),(5.5,7)]', h.a_line_segment @@ -1250,10 +1263,10 @@ class BasicsTest < ActiveRecord::TestCase end def test_quoting_arrays - replies = Reply.find(:all, :conditions => [ "id IN (?)", topics(:first).replies.collect(&:id) ]) + replies = Reply.scoped(:where => [ "id IN (?)", topics(:first).replies.collect(&:id) ]).all assert_equal topics(:first).replies.size, replies.size - replies = Reply.find(:all, :conditions => [ "id IN (?)", [] ]) + replies = Reply.scoped(:where => [ "id IN (?)", [] ]).all assert_equal 0, replies.size end @@ -1479,6 +1492,40 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_clear_cash_when_setting_table_name + Joke.table_name = "cold_jokes" + before_columns = Joke.columns + before_seq = Joke.sequence_name + + Joke.table_name = "funny_jokes" + after_columns = Joke.columns + after_seq = Joke.sequence_name + + assert_not_equal before_columns, after_columns + assert_not_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil? + end + + def test_dont_clear_sequence_name_when_setting_explicitly + Joke.sequence_name = "black_jokes_seq" + Joke.table_name = "cold_jokes" + before_seq = Joke.sequence_name + + Joke.table_name = "funny_jokes" + after_seq = Joke.sequence_name + + assert_equal before_seq, after_seq unless before_seq.nil? && after_seq.nil? + end + + def test_dont_clear_inheritnce_column_when_setting_explicitly + Joke.inheritance_column = "my_type" + before_inherit = Joke.inheritance_column + + Joke.reset_column_information + after_inherit = Joke.inheritance_column + + assert_equal before_inherit, after_inherit unless before_inherit.blank? && after_inherit.blank? + end + def test_set_table_name_symbol_converted_to_string Joke.table_name = :cold_jokes assert_equal 'cold_jokes', Joke.table_name @@ -1516,22 +1563,19 @@ class BasicsTest < ActiveRecord::TestCase def test_count_with_join res = Post.count_by_sql "SELECT COUNT(*) FROM posts LEFT JOIN comments ON posts.id=comments.post_id WHERE posts.#{QUOTED_TYPE} = 'Post'" - res2 = Post.count(:conditions => "posts.#{QUOTED_TYPE} = 'Post'", :joins => "LEFT JOIN comments ON posts.id=comments.post_id") + res2 = Post.where("posts.#{QUOTED_TYPE} = 'Post'").joins("LEFT JOIN comments ON posts.id=comments.post_id").count assert_equal res, res2 res3 = nil assert_nothing_raised do - res3 = Post.count(:conditions => "posts.#{QUOTED_TYPE} = 'Post'", - :joins => "LEFT JOIN comments ON posts.id=comments.post_id") + res3 = Post.where("posts.#{QUOTED_TYPE} = 'Post'").joins("LEFT JOIN comments ON posts.id=comments.post_id").count end assert_equal res, res3 res4 = Post.count_by_sql "SELECT COUNT(p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" res5 = nil assert_nothing_raised do - res5 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", - :joins => "p, comments co", - :select => "p.id") + res5 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count end assert_equal res4, res5 @@ -1539,145 +1583,64 @@ class BasicsTest < ActiveRecord::TestCase res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" res7 = nil assert_nothing_raised do - res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", - :joins => "p, comments co", - :select => "p.id", - :distinct => true) + res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count(distinct: true) end assert_equal res6, res7 end - def test_scoped_find_conditions - scoped_developers = Developer.send(:with_scope, :find => { :conditions => 'salary > 90000' }) do - Developer.find(:all, :conditions => 'id < 5') - end - assert !scoped_developers.include?(developers(:david)) # David's salary is less than 90,000 - assert_equal 3, scoped_developers.size - end - def test_no_limit_offset assert_nothing_raised do - Developer.find(:all, :offset => 2) - end - end - - def test_scoped_find_limit_offset - scoped_developers = Developer.send(:with_scope, :find => { :limit => 3, :offset => 2 }) do - Developer.find(:all, :order => 'id') + Developer.scoped(:offset => 2).all end - assert !scoped_developers.include?(developers(:david)) - assert !scoped_developers.include?(developers(:jamis)) - assert_equal 3, scoped_developers.size - - # Test without scoped find conditions to ensure we get the whole thing - developers = Developer.find(:all, :order => 'id') - assert_equal Developer.count, developers.size - end - - def test_scoped_find_order - # Test order in scope - scoped_developers = Developer.send(:with_scope, :find => { :limit => 1, :order => 'salary DESC' }) do - Developer.find(:all) - end - assert_equal 'Jamis', scoped_developers.first.name - assert scoped_developers.include?(developers(:jamis)) - # Test scope without order and order in find - scoped_developers = Developer.send(:with_scope, :find => { :limit => 1 }) do - Developer.find(:all, :order => 'salary DESC') - end - # Test scope order + find order, order has priority - scoped_developers = Developer.send(:with_scope, :find => { :limit => 3, :order => 'id DESC' }) do - Developer.find(:all, :order => 'salary ASC') - end - assert scoped_developers.include?(developers(:poor_jamis)) - assert ! scoped_developers.include?(developers(:david)) - assert ! scoped_developers.include?(developers(:jamis)) - assert_equal 3, scoped_developers.size - - # Test without scoped find conditions to ensure we get the right thing - assert ! scoped_developers.include?(Developer.find(1)) - assert scoped_developers.include?(Developer.find(11)) - end - - def test_scoped_find_limit_offset_including_has_many_association - topics = Topic.send(:with_scope, :find => {:limit => 1, :offset => 1, :include => :replies}) do - Topic.find(:all, :order => "topics.id") - end - assert_equal 1, topics.size - assert_equal 2, topics.first.id - end - - def test_scoped_find_order_including_has_many_association - developers = Developer.send(:with_scope, :find => { :order => 'developers.salary DESC', :include => :projects }) do - Developer.find(:all) - end - assert developers.size >= 2 - (1...developers.size).each do |i| - assert developers[i-1].salary >= developers[i].salary - end - end - - def test_scoped_find_with_group_and_having - developers = Developer.send(:with_scope, :find => { :group => 'developers.salary', :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" }) do - Developer.find(:all) - end - assert_equal 3, developers.size end def test_find_last - last = Developer.find :last - assert_equal last, Developer.find(:first, :order => 'id desc') + last = Developer.last + assert_equal last, Developer.scoped(:order => 'id desc').first end def test_last - assert_equal Developer.find(:first, :order => 'id desc'), Developer.last + assert_equal Developer.scoped(:order => 'id desc').first, Developer.last end def test_all developers = Developer.all assert_kind_of Array, developers - assert_equal Developer.find(:all), developers + assert_equal Developer.all, developers end def test_all_with_conditions - assert_equal Developer.find(:all, :order => 'id desc'), Developer.order('id desc').all + assert_equal Developer.scoped(:order => 'id desc').all, Developer.order('id desc').all end def test_find_ordered_last - last = Developer.find :last, :order => 'developers.salary ASC' - assert_equal last, Developer.find(:all, :order => 'developers.salary ASC').last + last = Developer.scoped(:order => 'developers.salary ASC').last + assert_equal last, Developer.scoped(:order => 'developers.salary ASC').all.last end def test_find_reverse_ordered_last - last = Developer.find :last, :order => 'developers.salary DESC' - assert_equal last, Developer.find(:all, :order => 'developers.salary DESC').last + last = Developer.scoped(:order => 'developers.salary DESC').last + assert_equal last, Developer.scoped(:order => 'developers.salary DESC').all.last end def test_find_multiple_ordered_last - last = Developer.find :last, :order => 'developers.name, developers.salary DESC' - assert_equal last, Developer.find(:all, :order => 'developers.name, developers.salary DESC').last + last = Developer.scoped(:order => 'developers.name, developers.salary DESC').last + assert_equal last, Developer.scoped(:order => 'developers.name, developers.salary DESC').all.last end def test_find_keeps_multiple_order_values - combined = Developer.find(:all, :order => 'developers.name, developers.salary') - assert_equal combined, Developer.find(:all, :order => ['developers.name', 'developers.salary']) + combined = Developer.scoped(:order => 'developers.name, developers.salary').all + assert_equal combined, Developer.scoped(:order => ['developers.name', 'developers.salary']).all end def test_find_keeps_multiple_group_values - combined = Developer.find(:all, :group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at') - assert_equal combined, Developer.find(:all, :group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at']) + combined = Developer.scoped(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at').all + assert_equal combined, Developer.scoped(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at']).all end def test_find_symbol_ordered_last - last = Developer.find :last, :order => :salary - assert_equal last, Developer.find(:all, :order => :salary).last - end - - def test_find_scoped_ordered_last - last_developer = Developer.send(:with_scope, :find => { :order => 'developers.salary ASC' }) do - Developer.find(:last) - end - assert_equal last_developer, Developer.find(:all, :order => 'developers.salary ASC').last + last = Developer.scoped(:order => :salary).last + assert_equal last, Developer.scoped(:order => :salary).all.last end def test_abstract_class @@ -1752,7 +1715,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_to_param_should_return_string - assert_kind_of String, Client.find(:first).to_param + assert_kind_of String, Client.first.to_param end def test_inspect_class @@ -1771,8 +1734,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_inspect_limited_select_instance - assert_equal %(#<Topic id: 1>), Topic.find(:first, :select => 'id', :conditions => 'id = 1').inspect - assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.find(:first, :select => 'id, title', :conditions => 'id = 1').inspect + assert_equal %(#<Topic id: 1>), Topic.scoped(:select => 'id', :where => 'id = 1').first.inspect + assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.scoped(:select => 'id, title', :where => 'id = 1').first.inspect end def test_inspect_class_without_table @@ -1920,8 +1883,17 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 1, post.comments.length end + def test_marshalling_new_record_round_trip_with_associations + post = Post.new + post.comments.build + + post = Marshal.load(Marshal.dump(post)) + + assert post.new_record?, "should be a new record" + end + def test_attribute_names - assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id"], + assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"], Company.attribute_names end @@ -1949,7 +1921,7 @@ class BasicsTest < ActiveRecord::TestCase def test_cache_key_format_for_existing_record_with_updated_at dev = Developer.first - assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:number)}", dev.cache_key + assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key end def test_cache_key_format_for_existing_record_with_nil_updated_at @@ -2000,4 +1972,41 @@ class BasicsTest < ActiveRecord::TestCase def test_typecasting_aliases assert_equal 10, Topic.select('10 as tenderlove').first.tenderlove end + + def test_slice + company = Company.new(:rating => 1, :name => "37signals", :firm_name => "37signals") + hash = company.slice(:name, :rating, "arbitrary_method") + assert_equal hash[:name], company.name + assert_equal hash['name'], company.name + assert_equal hash[:rating], company.rating + assert_equal hash['arbitrary_method'], company.arbitrary_method + assert_equal hash[:arbitrary_method], company.arbitrary_method + assert_nil hash[:firm_name] + assert_nil hash['firm_name'] + end + + def test_default_values_are_deeply_dupped + company = Company.new + company.description << "foo" + assert_equal "", Company.new.description + end + + ["find_by", "find_by!"].each do |meth| + test "#{meth} delegates to scoped" do + record = stub + + scope = mock + scope.expects(meth).with(:foo, :bar).returns(record) + + klass = Class.new(ActiveRecord::Base) + klass.stubs(:scoped => scope) + + assert_equal record, klass.public_send(meth, :foo, :bar) + end + end + + test "scoped can take a values hash" do + klass = Class.new(ActiveRecord::Base) + assert_equal ['foo'], klass.scoped(select: 'foo').select_values + end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 660098b9ad..cdd4b49042 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -27,30 +27,18 @@ class EachTest < ActiveRecord::TestCase def test_each_should_raise_if_select_is_set_without_id assert_raise(RuntimeError) do - Post.find_each(:select => :title, :batch_size => 1) { |post| post } + Post.select(:title).find_each(:batch_size => 1) { |post| post } end end def test_each_should_execute_if_id_is_in_select assert_queries(6) do - Post.find_each(:select => "id, title, type", :batch_size => 2) do |post| + Post.select("id, title, type").find_each(:batch_size => 2) do |post| assert_kind_of Post, post end end end - def test_each_should_raise_if_the_order_is_set - assert_raise(RuntimeError) do - Post.find_each(:order => "title") { |post| post } - end - end - - def test_each_should_raise_if_the_limit_is_set - assert_raise(RuntimeError) do - Post.find_each(:limit => 1) { |post| post } - end - end - def test_warn_if_limit_scope_is_set ActiveRecord::Base.logger.expects(:warn) Post.limit(1).find_each { |post| post } diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 3652255c38..03aa9fdb27 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -23,6 +23,8 @@ module ActiveRecord @listener = LogListener.new @pk = Topic.columns.find { |c| c.primary } ActiveSupport::Notifications.subscribe('sql.active_record', @listener) + + skip_if_prepared_statement_caching_is_not_supported end def teardown @@ -30,9 +32,6 @@ module ActiveRecord end def test_binds_are_logged - # FIXME: use skip with minitest - return unless @connection.supports_statement_cache? - sub = @connection.substitute_at(@pk, 0) binds = [[@pk, 1]] sql = "select * from topics where id = #{sub}" @@ -44,9 +43,6 @@ module ActiveRecord end def test_find_one_uses_binds - # FIXME: use skip with minitest - return unless @connection.supports_statement_cache? - Topic.find(1) binds = [[@pk, 1]] message = @listener.calls.find { |args| args[4][:binds] == binds } @@ -54,9 +50,6 @@ module ActiveRecord end def test_logs_bind_vars - # FIXME: use skip with minitest - return unless @connection.supports_statement_cache? - pk = Topic.columns.find { |x| x.primary } payload = { @@ -86,5 +79,11 @@ module ActiveRecord logger.sql event assert_match([[pk.name, 10]].inspect, logger.debugs.first) end + + private + + def skip_if_prepared_statement_caching_is_not_supported + skip('prepared statement caching is not supported') unless @connection.supports_statement_cache? + end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 0391319a00..041f8ffb7c 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -49,13 +49,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_get_maximum_of_field_with_include - assert_equal 55, Account.maximum(:credit_limit, :include => :firm, :references => :companies, :conditions => "companies.name != 'Summit'") - end - - def test_should_get_maximum_of_field_with_scoped_include - Account.send :with_scope, :find => { :include => :firm, :references => :companies, :conditions => "companies.name != 'Summit'" } do - assert_equal 55, Account.maximum(:credit_limit) - end + assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(:credit_limit) end def test_should_get_minimum_of_field @@ -63,12 +57,12 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_field - c = Account.sum(:credit_limit, :group => :firm_id) + c = Account.group(:firm_id).sum(:credit_limit) [1,6,2].each { |firm_id| assert c.keys.include?(firm_id) } end def test_should_group_by_multiple_fields - c = Account.count(:all, :group => ['firm_id', :credit_limit]) + c = Account.group('firm_id', :credit_limit).count(:all) [ [nil, 50], [1, 50], [6, 50], [6, 55], [9, 53], [2, 60] ].each { |firm_and_limit| assert c.keys.include?(firm_and_limit) } end @@ -81,32 +75,32 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field - c = Account.sum(:credit_limit, :group => :firm_id) + c = Account.group(:firm_id).sum(:credit_limit) assert_equal 50, c[1] assert_equal 105, c[6] assert_equal 60, c[2] end def test_should_order_by_grouped_field - c = Account.sum(:credit_limit, :group => :firm_id, :order => "firm_id") + c = Account.scoped(:group => :firm_id, :order => "firm_id").sum(:credit_limit) assert_equal [1, 2, 6, 9], c.keys.compact end def test_should_order_by_calculation - c = Account.sum(:credit_limit, :group => :firm_id, :order => "sum_credit_limit desc, firm_id") + c = Account.scoped(:group => :firm_id, :order => "sum_credit_limit desc, firm_id").sum(:credit_limit) assert_equal [105, 60, 53, 50, 50], c.keys.collect { |k| c[k] } assert_equal [6, 2, 9, 1], c.keys.compact end def test_should_limit_calculation - c = Account.sum(:credit_limit, :conditions => "firm_id IS NOT NULL", - :group => :firm_id, :order => "firm_id", :limit => 2) + c = Account.scoped(:where => "firm_id IS NOT NULL", + :group => :firm_id, :order => "firm_id", :limit => 2).sum(:credit_limit) assert_equal [1, 2], c.keys.compact end def test_should_limit_calculation_with_offset - c = Account.sum(:credit_limit, :conditions => "firm_id IS NOT NULL", - :group => :firm_id, :order => "firm_id", :limit => 2, :offset => 1) + c = Account.scoped(:where => "firm_id IS NOT NULL", :group => :firm_id, + :order => "firm_id", :limit => 2, :offset => 1).sum(:credit_limit) assert_equal [2, 6], c.keys.compact end @@ -156,16 +150,8 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_having_condition - c = Account.sum(:credit_limit, :group => :firm_id, - :having => 'sum(credit_limit) > 50') - assert_nil c[1] - assert_equal 105, c[6] - assert_equal 60, c[2] - end - - def test_should_group_by_summed_field_having_sanitized_condition - c = Account.sum(:credit_limit, :group => :firm_id, - :having => ['sum(credit_limit) > ?', 50]) + c = Account.scoped(:group => :firm_id, + :having => 'sum(credit_limit) > 50').sum(:credit_limit) assert_nil c[1] assert_equal 105, c[6] assert_equal 60, c[2] @@ -179,19 +165,19 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_association - c = Account.sum(:credit_limit, :group => :firm) + c = Account.group(:firm).sum(:credit_limit) assert_equal 50, c[companies(:first_firm)] assert_equal 105, c[companies(:rails_core)] assert_equal 60, c[companies(:first_client)] end def test_should_sum_field_with_conditions - assert_equal 105, Account.sum(:credit_limit, :conditions => 'firm_id = 6') + assert_equal 105, Account.where('firm_id = 6').sum(:credit_limit) end def test_should_return_zero_if_sum_conditions_return_nothing - assert_equal 0, Account.sum(:credit_limit, :conditions => '1 = 2') - assert_equal 0, companies(:rails_core).companies.sum(:id, :conditions => '1 = 2') + assert_equal 0, Account.where('1 = 2').sum(:credit_limit) + assert_equal 0, companies(:rails_core).companies.where('1 = 2').sum(:id) end def test_sum_should_return_valid_values_for_decimals @@ -200,24 +186,24 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_with_conditions - c = Account.sum(:credit_limit, :conditions => 'firm_id > 1', - :group => :firm_id) + c = Account.scoped(:where => 'firm_id > 1', + :group => :firm_id).sum(:credit_limit) assert_nil c[1] assert_equal 105, c[6] assert_equal 60, c[2] end def test_should_group_by_summed_field_with_conditions_and_having - c = Account.sum(:credit_limit, :conditions => 'firm_id > 1', - :group => :firm_id, - :having => 'sum(credit_limit) > 60') + c = Account.scoped(:where => 'firm_id > 1', + :group => :firm_id, + :having => 'sum(credit_limit) > 60').sum(:credit_limit) assert_nil c[1] assert_equal 105, c[6] assert_nil c[2] end def test_should_group_by_fields_with_table_alias - c = Account.sum(:credit_limit, :group => 'accounts.firm_id') + c = Account.group('accounts.firm_id').sum(:credit_limit) assert_equal 50, c[1] assert_equal 105, c[6] assert_equal 60, c[2] @@ -229,14 +215,14 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_calculate_grouped_with_invalid_field - c = Account.count(:all, :group => 'accounts.firm_id') + c = Account.group('accounts.firm_id').count(:all) assert_equal 1, c[1] assert_equal 2, c[6] assert_equal 1, c[2] end def test_should_calculate_grouped_association_with_invalid_field - c = Account.count(:all, :group => :firm) + c = Account.group(:firm).count(:all) assert_equal 1, c[companies(:first_firm)] assert_equal 2, c[companies(:rails_core)] assert_equal 1, c[companies(:first_client)] @@ -255,7 +241,7 @@ class CalculationsTest < ActiveRecord::TestCase column.expects(:type_cast).with("ABC").returns("ABC") Account.expects(:columns).at_least_once.returns([column]) - c = Account.count(:all, :group => :firm) + c = Account.group(:firm).count(:all) first_key = c.keys.first assert_equal Firm, first_key.class assert_equal 1, c[first_key] @@ -263,22 +249,14 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_calculate_grouped_association_with_foreign_key_option Account.belongs_to :another_firm, :class_name => 'Firm', :foreign_key => 'firm_id' - c = Account.count(:all, :group => :another_firm) + c = Account.group(:another_firm).count(:all) assert_equal 1, c[companies(:first_firm)] assert_equal 2, c[companies(:rails_core)] assert_equal 1, c[companies(:first_client)] end - def test_should_not_modify_options_when_using_includes - options = {:conditions => 'companies.id > 1', :include => :firm, :references => :companies} - options_copy = options.dup - - Account.count(:all, options) - assert_equal options_copy, options - end - def test_should_calculate_grouped_by_function - c = Company.count(:all, :group => "UPPER(#{QUOTED_TYPE})") + c = Company.group("UPPER(#{QUOTED_TYPE})").count(:all) assert_equal 2, c[nil] assert_equal 1, c['DEPENDENTFIRM'] assert_equal 4, c['CLIENT'] @@ -286,7 +264,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_calculate_grouped_by_function_with_table_alias - c = Company.count(:all, :group => "UPPER(companies.#{QUOTED_TYPE})") + c = Company.group("UPPER(companies.#{QUOTED_TYPE})").count(:all) assert_equal 2, c[nil] assert_equal 1, c['DEPENDENTFIRM'] assert_equal 4, c['CLIENT'] @@ -306,25 +284,24 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_sum_scoped_field_with_conditions - assert_equal 8, companies(:rails_core).companies.sum(:id, :conditions => 'id > 7') + assert_equal 8, companies(:rails_core).companies.where('id > 7').sum(:id) end def test_should_group_by_scoped_field - c = companies(:rails_core).companies.sum(:id, :group => :name) + c = companies(:rails_core).companies.group(:name).sum(:id) assert_equal 7, c['Leetsoft'] assert_equal 8, c['Jadedpixel'] end def test_should_group_by_summed_field_through_association_and_having - c = companies(:rails_core).companies.sum(:id, :group => :name, - :having => 'sum(id) > 7') + c = companies(:rails_core).companies.group(:name).having('sum(id) > 7').sum(:id) assert_nil c['Leetsoft'] assert_equal 8, c['Jadedpixel'] end def test_should_count_selected_field_with_include - assert_equal 6, Account.count(:distinct => true, :include => :firm) - assert_equal 4, Account.count(:distinct => true, :include => :firm, :select => :credit_limit) + assert_equal 6, Account.includes(:firm).count(:distinct => true) + assert_equal 4, Account.includes(:firm).select(:credit_limit).count(:distinct => true) end def test_should_not_perform_joined_include_by_default @@ -348,11 +325,11 @@ class CalculationsTest < ActiveRecord::TestCase Account.last.update_column('credit_limit', 49) Account.first.update_column('credit_limit', 51) - assert_equal 1, Account.scoped(:select => "credit_limit").count(:conditions => ['credit_limit >= 50']) + assert_equal 1, Account.scoped(:select => "credit_limit").where('credit_limit >= 50').count end def test_should_count_manual_select_with_include - assert_equal 6, Account.count(:select => "DISTINCT accounts.id", :include => :firm) + assert_equal 6, Account.scoped(:select => "DISTINCT accounts.id", :includes => :firm).count end def test_count_with_column_parameter @@ -360,16 +337,16 @@ class CalculationsTest < ActiveRecord::TestCase end def test_count_with_column_and_options_parameter - assert_equal 2, Account.count(:firm_id, :conditions => "credit_limit = 50 AND firm_id IS NOT NULL") + assert_equal 2, Account.where("credit_limit = 50 AND firm_id IS NOT NULL").count(:firm_id) end def test_should_count_field_in_joined_table - assert_equal 5, Account.count('companies.id', :joins => :firm) - assert_equal 4, Account.count('companies.id', :joins => :firm, :distinct => true) + assert_equal 5, Account.joins(:firm).count('companies.id') + assert_equal 4, Account.joins(:firm).count('companies.id', :distinct => true) end def test_should_count_field_in_joined_table_with_group_by - c = Account.count('companies.id', :group => 'accounts.firm_id', :joins => :firm) + c = Account.scoped(:group => 'accounts.firm_id', :joins => :firm).count('companies.id') [1,6,2,9].each { |firm_id| assert c.keys.include?(firm_id) } end @@ -392,17 +369,17 @@ class CalculationsTest < ActiveRecord::TestCase end def test_count_with_from_option - assert_equal Company.count(:all), Company.count(:all, :from => 'companies') - assert_equal Account.count(:all, :conditions => "credit_limit = 50"), - Account.count(:all, :from => 'accounts', :conditions => "credit_limit = 50") - assert_equal Company.count(:type, :conditions => {:type => "Firm"}), - Company.count(:type, :conditions => {:type => "Firm"}, :from => 'companies') + assert_equal Company.count(:all), Company.from('companies').count(:all) + assert_equal Account.where("credit_limit = 50").count(:all), + Account.from('accounts').where("credit_limit = 50").count(:all) + assert_equal Company.where(:type => "Firm").count(:type), + Company.where(:type => "Firm").from('companies').count(:type) end def test_sum_with_from_option - assert_equal Account.sum(:credit_limit), Account.sum(:credit_limit, :from => 'accounts') - assert_equal Account.sum(:credit_limit, :conditions => "credit_limit > 50"), - Account.sum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.sum(:credit_limit), Account.from('accounts').sum(:credit_limit) + assert_equal Account.where("credit_limit > 50").sum(:credit_limit), + Account.where("credit_limit > 50").from('accounts').sum(:credit_limit) end def test_sum_array_compatibility @@ -410,33 +387,33 @@ class CalculationsTest < ActiveRecord::TestCase end def test_average_with_from_option - assert_equal Account.average(:credit_limit), Account.average(:credit_limit, :from => 'accounts') - assert_equal Account.average(:credit_limit, :conditions => "credit_limit > 50"), - Account.average(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.average(:credit_limit), Account.from('accounts').average(:credit_limit) + assert_equal Account.where("credit_limit > 50").average(:credit_limit), + Account.where("credit_limit > 50").from('accounts').average(:credit_limit) end def test_minimum_with_from_option - assert_equal Account.minimum(:credit_limit), Account.minimum(:credit_limit, :from => 'accounts') - assert_equal Account.minimum(:credit_limit, :conditions => "credit_limit > 50"), - Account.minimum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.minimum(:credit_limit), Account.from('accounts').minimum(:credit_limit) + assert_equal Account.where("credit_limit > 50").minimum(:credit_limit), + Account.where("credit_limit > 50").from('accounts').minimum(:credit_limit) end def test_maximum_with_from_option - assert_equal Account.maximum(:credit_limit), Account.maximum(:credit_limit, :from => 'accounts') - assert_equal Account.maximum(:credit_limit, :conditions => "credit_limit > 50"), - Account.maximum(:credit_limit, :from => 'accounts', :conditions => "credit_limit > 50") + assert_equal Account.maximum(:credit_limit), Account.from('accounts').maximum(:credit_limit) + assert_equal Account.where("credit_limit > 50").maximum(:credit_limit), + Account.where("credit_limit > 50").from('accounts').maximum(:credit_limit) end def test_from_option_with_specified_index if Edge.connection.adapter_name == 'MySQL' or Edge.connection.adapter_name == 'Mysql2' - assert_equal Edge.count(:all), Edge.count(:all, :from => 'edges USE INDEX(unique_edge_index)') - assert_equal Edge.count(:all, :conditions => 'sink_id < 5'), - Edge.count(:all, :from => 'edges USE INDEX(unique_edge_index)', :conditions => 'sink_id < 5') + assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all) + assert_equal Edge.where('sink_id < 5').count(:all), + Edge.from('edges USE INDEX(unique_edge_index)').where('sink_id < 5').count(:all) end end def test_from_option_with_table_different_than_class - assert_equal Account.count(:all), Company.count(:all, :from => 'accounts') + assert_equal Account.count(:all), Company.from('accounts').count(:all) end def test_distinct_is_honored_when_used_with_count_operation_after_group @@ -488,4 +465,23 @@ class CalculationsTest < ActiveRecord::TestCase Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)]) assert_equal [7], Company.joins(:contracts).pluck(:developer_id) end + + def test_pluck_with_selection_clause + assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT credit_limit').sort + assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT accounts.credit_limit').sort + assert_equal [50, 53, 55, 60], Account.pluck('DISTINCT(credit_limit)').sort + + # MySQL returns "SUM(DISTINCT(credit_limit))" as the column name unless + # an alias is provided. Without the alias, the column cannot be found + # and properly typecast. + assert_equal [50 + 53 + 55 + 60], Account.pluck('SUM(DISTINCT(credit_limit)) as credit_limit') + end + + def test_pluck_expects_a_single_selection + assert_raise(ArgumentError) { Account.pluck 'id, credit_limit' } + end + + def test_plucks_with_ids + assert_equal Company.all.map(&:id).sort, Company.ids.sort + end end diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb index c7dcc21809..b874adc081 100644 --- a/activerecord/test/cases/coders/yaml_column_test.rb +++ b/activerecord/test/cases/coders/yaml_column_test.rb @@ -9,6 +9,13 @@ module ActiveRecord assert_equal Object, coder.object_class end + def test_type_mismatch_on_different_classes_on_dump + coder = YAMLColumn.new(Array) + assert_raises(SerializationTypeMismatch) do + coder.dump("a") + end + end + def test_type_mismatch_on_different_classes coder = YAMLColumn.new(Array) assert_raises(SerializationTypeMismatch) do diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb index ccc57cb876..4111a5f808 100644 --- a/activerecord/test/cases/column_test.rb +++ b/activerecord/test/cases/column_test.rb @@ -24,6 +24,58 @@ module ActiveRecord assert !column.type_cast('off') assert !column.type_cast('OFF') end + + def test_type_cast_integer + column = Column.new("field", nil, "integer") + assert_equal 1, column.type_cast(1) + assert_equal 1, column.type_cast('1') + assert_equal 1, column.type_cast('1ignore') + assert_equal 0, column.type_cast('bad1') + assert_equal 0, column.type_cast('bad') + assert_equal 1, column.type_cast(1.7) + assert_nil column.type_cast(nil) + end + + def test_type_cast_non_integer_to_integer + column = Column.new("field", nil, "integer") + assert_raises(NoMethodError) do + column.type_cast([]) + end + assert_raises(NoMethodError) do + column.type_cast(true) + end + assert_raises(NoMethodError) do + column.type_cast(false) + end + end + + def test_type_cast_time + column = Column.new("field", nil, "time") + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, column.type_cast(time_string).strftime("%T") + end + + def test_type_cast_datetime_and_timestamp + [Column.new("field", nil, "datetime"), Column.new("field", nil, "timestamp")].each do |column| + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T") + end + end + + def test_type_cast_date + column = Column.new("field", nil, "date") + assert_equal nil, column.type_cast('') + assert_equal nil, column.type_cast(' ') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, column.type_cast(date_string).strftime("%F") + end end end end diff --git a/activerecord/test/cases/connection_adapters/quoting_test.rb b/activerecord/test/cases/connection_adapters/quoting_test.rb new file mode 100644 index 0000000000..59dcb96ebc --- /dev/null +++ b/activerecord/test/cases/connection_adapters/quoting_test.rb @@ -0,0 +1,13 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + module Quoting + class QuotingTest < ActiveRecord::TestCase + def test_quoting_classes + assert_equal "'Object'", AbstractAdapter.new(nil).quote(Object) + end + end + end + end +end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 42e39d534c..541e983758 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -39,6 +39,21 @@ module ActiveRecord assert_equal 0, @cache.tables.size assert_equal 0, @cache.primary_keys.size end + + def test_dump_and_load + @cache.columns['posts'] + @cache.columns_hash['posts'] + @cache.tables['posts'] + @cache.primary_keys['posts'] + + @cache = Marshal.load(Marshal.dump(@cache)) + + assert_equal 12, @cache.columns['posts'].size + assert_equal 12, @cache.columns_hash['posts'].size + assert @cache.tables['posts'] + assert_equal 'id', @cache.primary_keys['posts'] + end + end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 2c69bfde5b..bba7815d73 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -3,6 +3,8 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class ConnectionPoolTest < ActiveRecord::TestCase + attr_reader :pool + def setup super @@ -25,6 +27,67 @@ module ActiveRecord @pool.disconnect! end + def active_connections(pool) + pool.connections.find_all(&:in_use?) + end + + def test_checkout_after_close + connection = pool.connection + assert connection.in_use? + + connection.close + assert !connection.in_use? + + assert pool.connection.in_use? + end + + def test_released_connection_moves_between_threads + thread_conn = nil + + Thread.new { + pool.with_connection do |conn| + thread_conn = conn + end + }.join + + assert thread_conn + + Thread.new { + pool.with_connection do |conn| + assert_equal thread_conn, conn + end + }.join + end + + def test_with_connection + assert_equal 0, active_connections(pool).size + + main_thread = pool.connection + assert_equal 1, active_connections(pool).size + + Thread.new { + pool.with_connection do |conn| + assert conn + assert_equal 2, active_connections(pool).size + end + assert_equal 1, active_connections(pool).size + }.join + + main_thread.close + assert_equal 0, active_connections(pool).size + end + + def test_active_connection_in_use + assert !pool.active_connection? + main_thread = pool.connection + + assert pool.active_connection? + + main_thread.close + + assert !pool.active_connection? + end + def test_full_pool_exception assert_raises(PoolFullError) do (@pool.size + 1).times do @@ -33,11 +96,35 @@ module ActiveRecord end end + def test_full_pool_blocks + cs = @pool.size.times.map { @pool.checkout } + t = Thread.new { @pool.checkout } + + # make sure our thread is in the timeout section + Thread.pass until t.status == "sleep" + + connection = cs.first + connection.close + assert_equal connection, t.join.value + end + + def test_removing_releases_latch + cs = @pool.size.times.map { @pool.checkout } + t = Thread.new { @pool.checkout } + + # make sure our thread is in the timeout section + Thread.pass until t.status == "sleep" + + connection = cs.first + @pool.remove connection + assert_respond_to t.join.value, :execute + end + def test_reap_and_active @pool.checkout @pool.checkout @pool.checkout - @pool.timeout = 0 + @pool.dead_connection_timeout = 0 connections = @pool.connections.dup @@ -50,7 +137,7 @@ module ActiveRecord @pool.checkout @pool.checkout @pool.checkout - @pool.timeout = 0 + @pool.dead_connection_timeout = 0 connections = @pool.connections.dup connections.each do |conn| diff --git a/activerecord/test/cases/custom_locking_test.rb b/activerecord/test/cases/custom_locking_test.rb index d63ecdbcc5..42ef51ef3e 100644 --- a/activerecord/test/cases/custom_locking_test.rb +++ b/activerecord/test/cases/custom_locking_test.rb @@ -9,7 +9,7 @@ module ActiveRecord if current_adapter?(:MysqlAdapter, :Mysql2Adapter) assert_match 'SHARE MODE', Person.lock('LOCK IN SHARE MODE').to_sql assert_sql(/LOCK IN SHARE MODE/) do - Person.find(1, :lock => 'LOCK IN SHARE MODE') + Person.scoped(:lock => 'LOCK IN SHARE MODE').find(1) end end end diff --git a/activerecord/test/cases/deprecated_dynamic_methods_test.rb b/activerecord/test/cases/deprecated_dynamic_methods_test.rb new file mode 100644 index 0000000000..77a609f49a --- /dev/null +++ b/activerecord/test/cases/deprecated_dynamic_methods_test.rb @@ -0,0 +1,607 @@ +# This file should be deleted when active_record_deprecated_finders is removed as +# a dependency. +# +# It is kept for now as there is some fairly nuanced behaviour in the dynamic +# finders so it is useful to keep this around to guard against regressions if +# we need to change the code. + +require 'cases/helper' +require 'models/topic' +require 'models/reply' +require 'models/customer' +require 'models/post' +require 'models/company' +require 'models/author' +require 'models/category' +require 'models/comment' +require 'models/person' +require 'models/reader' + +class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase + fixtures :topics, :customers, :companies, :accounts, :posts, :categories, :categories_posts, :authors, :people, :comments, :readers + + def setup + @deprecation_behavior = ActiveSupport::Deprecation.behavior + ActiveSupport::Deprecation.behavior = :silence + end + + def teardown + ActiveSupport::Deprecation.behavior = @deprecation_behavior + end + + def test_find_all_by_one_attribute + topics = Topic.find_all_by_content("Have a nice day") + assert_equal 2, topics.size + assert topics.include?(topics(:first)) + + assert_equal [], Topic.find_all_by_title("The First Topic!!") + end + + def test_find_all_by_one_attribute_which_is_a_symbol + topics = Topic.find_all_by_content("Have a nice day".to_sym) + assert_equal 2, topics.size + assert topics.include?(topics(:first)) + + assert_equal [], Topic.find_all_by_title("The First Topic!!") + end + + def test_find_all_by_one_attribute_that_is_an_aggregate + balance = customers(:david).balance + assert_kind_of Money, balance + found_customers = Customer.find_all_by_balance(balance) + assert_equal 1, found_customers.size + assert_equal customers(:david), found_customers.first + end + + def test_find_all_by_two_attributes_that_are_both_aggregates + balance = customers(:david).balance + address = customers(:david).address + assert_kind_of Money, balance + assert_kind_of Address, address + found_customers = Customer.find_all_by_balance_and_address(balance, address) + assert_equal 1, found_customers.size + assert_equal customers(:david), found_customers.first + end + + def test_find_all_by_two_attributes_with_one_being_an_aggregate + balance = customers(:david).balance + assert_kind_of Money, balance + found_customers = Customer.find_all_by_balance_and_name(balance, customers(:david).name) + assert_equal 1, found_customers.size + assert_equal customers(:david), found_customers.first + end + + def test_find_all_by_one_attribute_with_options + topics = Topic.find_all_by_content("Have a nice day", :order => "id DESC") + assert_equal topics(:first), topics.last + + topics = Topic.find_all_by_content("Have a nice day", :order => "id") + assert_equal topics(:first), topics.first + end + + def test_find_all_by_array_attribute + assert_equal 2, Topic.find_all_by_title(["The First Topic", "The Second Topic of the day"]).size + end + + def test_find_all_by_boolean_attribute + topics = Topic.find_all_by_approved(false) + assert_equal 1, topics.size + assert topics.include?(topics(:first)) + + topics = Topic.find_all_by_approved(true) + assert_equal 3, topics.size + assert topics.include?(topics(:second)) + end + + def test_find_all_by_nil_and_not_nil_attributes + topics = Topic.find_all_by_last_read_and_author_name nil, "Mary" + assert_equal 1, topics.size + assert_equal "Mary", topics[0].author_name + end + + def test_find_or_create_from_one_attribute + number_of_companies = Company.count + sig38 = Company.find_or_create_by_name("38signals") + assert_equal number_of_companies + 1, Company.count + assert_equal sig38, Company.find_or_create_by_name("38signals") + assert sig38.persisted? + end + + def test_find_or_create_from_two_attributes + number_of_topics = Topic.count + another = Topic.find_or_create_by_title_and_author_name("Another topic","John") + assert_equal number_of_topics + 1, Topic.count + assert_equal another, Topic.find_or_create_by_title_and_author_name("Another topic", "John") + assert another.persisted? + end + + def test_find_or_create_from_one_attribute_bang + number_of_companies = Company.count + assert_raises(ActiveRecord::RecordInvalid) { Company.find_or_create_by_name!("") } + assert_equal number_of_companies, Company.count + sig38 = Company.find_or_create_by_name!("38signals") + assert_equal number_of_companies + 1, Company.count + assert_equal sig38, Company.find_or_create_by_name!("38signals") + assert sig38.persisted? + end + + def test_find_or_create_from_two_attributes_bang + number_of_companies = Company.count + assert_raises(ActiveRecord::RecordInvalid) { Company.find_or_create_by_name_and_firm_id!("", 17) } + assert_equal number_of_companies, Company.count + sig38 = Company.find_or_create_by_name_and_firm_id!("38signals", 17) + assert_equal number_of_companies + 1, Company.count + assert_equal sig38, Company.find_or_create_by_name_and_firm_id!("38signals", 17) + assert sig38.persisted? + assert_equal "38signals", sig38.name + assert_equal 17, sig38.firm_id + end + + def test_find_or_create_from_two_attributes_with_one_being_an_aggregate + number_of_customers = Customer.count + created_customer = Customer.find_or_create_by_balance_and_name(Money.new(123), "Elizabeth") + assert_equal number_of_customers + 1, Customer.count + assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123), "Elizabeth") + assert created_customer.persisted? + end + + def test_find_or_create_from_one_attribute_and_hash + number_of_companies = Company.count + sig38 = Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) + assert_equal number_of_companies + 1, Company.count + assert_equal sig38, Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) + assert sig38.persisted? + assert_equal "38signals", sig38.name + assert_equal 17, sig38.firm_id + assert_equal 23, sig38.client_of + end + + def test_find_or_create_from_two_attributes_and_hash + number_of_companies = Company.count + sig38 = Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23}) + assert_equal number_of_companies + 1, Company.count + assert_equal sig38, Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23}) + assert sig38.persisted? + assert_equal "38signals", sig38.name + assert_equal 17, sig38.firm_id + assert_equal 23, sig38.client_of + end + + def test_find_or_create_from_one_aggregate_attribute + number_of_customers = Customer.count + created_customer = Customer.find_or_create_by_balance(Money.new(123)) + assert_equal number_of_customers + 1, Customer.count + assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123)) + assert created_customer.persisted? + end + + def test_find_or_create_from_one_aggregate_attribute_and_hash + number_of_customers = Customer.count + balance = Money.new(123) + name = "Elizabeth" + created_customer = Customer.find_or_create_by_balance({:balance => balance, :name => name}) + assert_equal number_of_customers + 1, Customer.count + assert_equal created_customer, Customer.find_or_create_by_balance({:balance => balance, :name => name}) + assert created_customer.persisted? + assert_equal balance, created_customer.balance + assert_equal name, created_customer.name + end + + def test_find_or_initialize_from_one_attribute + sig38 = Company.find_or_initialize_by_name("38signals") + assert_equal "38signals", sig38.name + assert !sig38.persisted? + end + + def test_find_or_initialize_from_one_aggregate_attribute + new_customer = Customer.find_or_initialize_by_balance(Money.new(123)) + assert_equal 123, new_customer.balance.amount + assert !new_customer.persisted? + end + + def test_find_or_initialize_from_one_attribute_should_not_set_attribute_even_when_protected + c = Company.find_or_initialize_by_name({:name => "Fortune 1000", :rating => 1000}) + assert_equal "Fortune 1000", c.name + assert_not_equal 1000, c.rating + assert c.valid? + assert !c.persisted? + end + + def test_find_or_create_from_one_attribute_should_not_set_attribute_even_when_protected + c = Company.find_or_create_by_name({:name => "Fortune 1000", :rating => 1000}) + assert_equal "Fortune 1000", c.name + assert_not_equal 1000, c.rating + assert c.valid? + assert c.persisted? + end + + def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected + c = Company.find_or_initialize_by_name_and_rating("Fortune 1000", 1000) + assert_equal "Fortune 1000", c.name + assert_equal 1000, c.rating + assert c.valid? + assert !c.persisted? + end + + def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected + c = Company.find_or_create_by_name_and_rating("Fortune 1000", 1000) + assert_equal "Fortune 1000", c.name + assert_equal 1000, c.rating + assert c.valid? + assert c.persisted? + end + + def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash + c = Company.find_or_initialize_by_rating(1000, {:name => "Fortune 1000"}) + assert_equal "Fortune 1000", c.name + assert_equal 1000, c.rating + assert c.valid? + assert !c.persisted? + end + + def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash + c = Company.find_or_create_by_rating(1000, {:name => "Fortune 1000"}) + assert_equal "Fortune 1000", c.name + assert_equal 1000, c.rating + assert c.valid? + assert c.persisted? + end + + def test_find_or_initialize_should_set_protected_attributes_if_given_as_block + c = Company.find_or_initialize_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } + assert_equal "Fortune 1000", c.name + assert_equal 1000.to_f, c.rating.to_f + assert c.valid? + assert !c.persisted? + end + + def test_find_or_create_should_set_protected_attributes_if_given_as_block + c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } + assert_equal "Fortune 1000", c.name + assert_equal 1000.to_f, c.rating.to_f + assert c.valid? + assert c.persisted? + end + + def test_find_or_create_should_work_with_block_on_first_call + class << Company + undef_method(:find_or_create_by_name) if method_defined?(:find_or_create_by_name) + end + c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } + assert_equal "Fortune 1000", c.name + assert_equal 1000.to_f, c.rating.to_f + assert c.valid? + assert c.persisted? + end + + def test_find_or_initialize_from_two_attributes + another = Topic.find_or_initialize_by_title_and_author_name("Another topic","John") + assert_equal "Another topic", another.title + assert_equal "John", another.author_name + assert !another.persisted? + end + + def test_find_or_initialize_from_two_attributes_but_passing_only_one + assert_raise(ArgumentError) { Topic.find_or_initialize_by_title_and_author_name("Another topic") } + end + + def test_find_or_initialize_from_one_aggregate_attribute_and_one_not + new_customer = Customer.find_or_initialize_by_balance_and_name(Money.new(123), "Elizabeth") + assert_equal 123, new_customer.balance.amount + assert_equal "Elizabeth", new_customer.name + assert !new_customer.persisted? + end + + def test_find_or_initialize_from_one_attribute_and_hash + sig38 = Company.find_or_initialize_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) + assert_equal "38signals", sig38.name + assert_equal 17, sig38.firm_id + assert_equal 23, sig38.client_of + assert !sig38.persisted? + end + + def test_find_or_initialize_from_one_aggregate_attribute_and_hash + balance = Money.new(123) + name = "Elizabeth" + new_customer = Customer.find_or_initialize_by_balance({:balance => balance, :name => name}) + assert_equal balance, new_customer.balance + assert_equal name, new_customer.name + assert !new_customer.persisted? + end + + def test_find_last_by_one_attribute + assert_equal Topic.last, Topic.find_last_by_title(Topic.last.title) + assert_nil Topic.find_last_by_title("A title with no matches") + end + + def test_find_last_by_invalid_method_syntax + assert_raise(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") } + assert_raise(NoMethodError) { Topic.find_last_by_title?("The First Topic") } + end + + def test_find_last_by_one_attribute_with_several_options + assert_equal accounts(:signals37), Account.order('id DESC').where('id != ?', 3).find_last_by_credit_limit(50) + end + + def test_find_last_by_one_missing_attribute + assert_raise(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") } + end + + def test_find_last_by_two_attributes + topic = Topic.last + assert_equal topic, Topic.find_last_by_title_and_author_name(topic.title, topic.author_name) + assert_nil Topic.find_last_by_title_and_author_name(topic.title, "Anonymous") + end + + def test_find_last_with_limit_gives_same_result_when_loaded_and_unloaded + scope = Topic.limit(2) + unloaded_last = scope.last + loaded_last = scope.all.last + assert_equal loaded_last, unloaded_last + end + + def test_find_last_with_limit_and_offset_gives_same_result_when_loaded_and_unloaded + scope = Topic.offset(2).limit(2) + unloaded_last = scope.last + loaded_last = scope.all.last + assert_equal loaded_last, unloaded_last + end + + def test_find_last_with_offset_gives_same_result_when_loaded_and_unloaded + scope = Topic.offset(3) + unloaded_last = scope.last + loaded_last = scope.all.last + assert_equal loaded_last, unloaded_last + end + + def test_find_all_by_nil_attribute + topics = Topic.find_all_by_last_read nil + assert_equal 3, topics.size + assert topics.collect(&:last_read).all?(&:nil?) + end + + def test_forwarding_to_dynamic_finders + welcome = Post.find(1) + assert_equal 4, Category.find_all_by_type('SpecialCategory').size + assert_equal 0, welcome.categories.find_all_by_type('SpecialCategory').size + assert_equal 2, welcome.categories.find_all_by_type('Category').size + end + + def test_dynamic_find_all_should_respect_association_order + assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.scoped(:where => "type = 'Client'").all + assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find_all_by_type('Client') + end + + def test_dynamic_find_all_should_respect_association_limit + assert_equal 1, companies(:first_firm).limited_clients.scoped(:where => "type = 'Client'").all.length + assert_equal 1, companies(:first_firm).limited_clients.find_all_by_type('Client').length + end + + def test_dynamic_find_all_limit_should_override_association_limit + assert_equal 2, companies(:first_firm).limited_clients.scoped(:where => "type = 'Client'", :limit => 9_000).all.length + assert_equal 2, companies(:first_firm).limited_clients.find_all_by_type('Client', :limit => 9_000).length + end + + def test_dynamic_find_last_without_specified_order + assert_equal companies(:second_client), companies(:first_firm).unsorted_clients.find_last_by_type('Client') + end + + def test_dynamic_find_or_create_from_two_attributes_using_an_association + author = authors(:david) + number_of_posts = Post.count + another = author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") + assert_equal number_of_posts + 1, Post.count + assert_equal another, author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body") + assert another.persisted? + end + + def test_dynamic_find_all_should_respect_association_order_for_through + assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.scoped(:where => "comments.type = 'SpecialComment'").all + assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find_all_by_type('SpecialComment') + end + + def test_dynamic_find_all_should_respect_association_limit_for_through + assert_equal 1, authors(:david).limited_comments.scoped(:where => "comments.type = 'SpecialComment'").all.length + assert_equal 1, authors(:david).limited_comments.find_all_by_type('SpecialComment').length + end + + def test_dynamic_find_all_order_should_override_association_limit_for_through + assert_equal 4, authors(:david).limited_comments.scoped(:where => "comments.type = 'SpecialComment'", :limit => 9_000).all.length + assert_equal 4, authors(:david).limited_comments.find_all_by_type('SpecialComment', :limit => 9_000).length + end + + def test_find_all_include_over_the_same_table_for_through + assert_equal 2, people(:michael).posts.scoped(:includes => :people).all.length + end + + def test_find_or_create_by_resets_cached_counters + person = Person.create! :first_name => 'tenderlove' + post = Post.first + + assert_equal [], person.readers + assert_nil person.readers.find_by_post_id(post.id) + + person.readers.find_or_create_by_post_id(post.id) + + assert_equal 1, person.readers.count + assert_equal 1, person.readers.length + assert_equal post, person.readers.first.post + assert_equal person, person.readers.first.person + end + + def test_find_or_initialize + the_client = companies(:first_firm).clients.find_or_initialize_by_name("Yet another client") + assert_equal companies(:first_firm).id, the_client.firm_id + assert_equal "Yet another client", the_client.name + assert !the_client.persisted? + end + + def test_find_or_create_updates_size + number_of_clients = companies(:first_firm).clients.size + the_client = companies(:first_firm).clients.find_or_create_by_name("Yet another client") + assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size + assert_equal the_client, companies(:first_firm).clients.find_or_create_by_name("Yet another client") + assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size + end + + def test_find_or_initialize_updates_collection_size + number_of_clients = companies(:first_firm).clients_of_firm.size + companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client") + assert_equal number_of_clients + 1, companies(:first_firm).clients_of_firm.size + end + + def test_find_or_initialize_returns_the_instantiated_object + client = companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client") + assert_equal client, companies(:first_firm).clients_of_firm[-1] + end + + def test_find_or_initialize_only_instantiates_a_single_object + number_of_clients = Client.count + companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client").save! + companies(:first_firm).save! + assert_equal number_of_clients+1, Client.count + end + + def test_find_or_create_with_hash + post = authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody') + assert_equal post, authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody') + assert post.persisted? + end + + def test_find_or_create_with_one_attribute_followed_by_hash + post = authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody') + assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody') + assert post.persisted? + end + + def test_find_or_create_should_work_with_block + post = authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'} + assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'} + assert post.persisted? + end + + def test_forwarding_to_dynamic_finders_2 + welcome = Post.find(1) + assert_equal 4, Comment.find_all_by_type('Comment').size + assert_equal 2, welcome.comments.find_all_by_type('Comment').size + end + + def test_dynamic_find_all_by_attributes + authors = Author.scoped + + davids = authors.find_all_by_name('David') + assert_kind_of Array, davids + assert_equal [authors(:david)], davids + end + + def test_dynamic_find_or_initialize_by_attributes + authors = Author.scoped + + lifo = authors.find_or_initialize_by_name('Lifo') + assert_equal "Lifo", lifo.name + assert !lifo.persisted? + + assert_equal authors(:david), authors.find_or_initialize_by_name(:name => 'David') + end + + def test_dynamic_find_or_create_by_attributes + authors = Author.scoped + + lifo = authors.find_or_create_by_name('Lifo') + assert_equal "Lifo", lifo.name + assert lifo.persisted? + + assert_equal authors(:david), authors.find_or_create_by_name(:name => 'David') + end + + def test_dynamic_find_or_create_by_attributes_bang + authors = Author.scoped + + assert_raises(ActiveRecord::RecordInvalid) { authors.find_or_create_by_name!('') } + + lifo = authors.find_or_create_by_name!('Lifo') + assert_equal "Lifo", lifo.name + assert lifo.persisted? + + assert_equal authors(:david), authors.find_or_create_by_name!(:name => 'David') + end + + def test_finder_block + t = Topic.first + found = nil + Topic.find_by_id(t.id) { |f| found = f } + assert_equal t, found + end + + def test_finder_block_nothing_found + bad_id = Topic.maximum(:id) + 1 + assert_nil Topic.find_by_id(bad_id) { |f| raise } + end + + def test_find_returns_block_value + t = Topic.first + x = Topic.find_by_id(t.id) { |f| "hi mom!" } + assert_equal "hi mom!", x + end + + def test_dynamic_finder_with_invalid_params + assert_raise(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" } + end + + def test_find_by_one_attribute_with_order_option + assert_equal accounts(:signals37), Account.find_by_credit_limit(50, :order => 'id') + assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :order => 'id DESC') + end + + def test_dynamic_find_by_attributes_should_yield_found_object + david = authors(:david) + yielded_value = nil + Author.find_by_name(david.name) do |author| + yielded_value = author + end + assert_equal david, yielded_value + end +end + +class DynamicScopeTest < ActiveRecord::TestCase + fixtures :posts + + def setup + @test_klass = Class.new(Post) do + def self.name; "Post"; end + end + @deprecation_behavior = ActiveSupport::Deprecation.behavior + ActiveSupport::Deprecation.behavior = :silence + end + + def teardown + ActiveSupport::Deprecation.behavior = @deprecation_behavior + end + + def test_dynamic_scope + assert_equal @test_klass.scoped_by_author_id(1).find(1), @test_klass.find(1) + assert_equal @test_klass.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, @test_klass.scoped(:where => { :author_id => 1, :title => "Welcome to the weblog"}).first + end + + def test_dynamic_scope_should_create_methods_after_hitting_method_missing + assert_blank @test_klass.methods.grep(/scoped_by_type/) + @test_klass.scoped_by_type(nil) + assert_present @test_klass.methods.grep(/scoped_by_type/) + end + + def test_dynamic_scope_with_less_number_of_arguments + assert_raise(ArgumentError){ @test_klass.scoped_by_author_id_and_title(1) } + end +end + +class DynamicScopeMatchTest < ActiveRecord::TestCase + def test_scoped_by_no_match + assert_nil ActiveRecord::DynamicMatchers::Method.match(nil, "not_scoped_at_all") + end + + def test_scoped_by + match = ActiveRecord::DynamicMatchers::Method.match(nil, "scoped_by_age_and_sex_and_location") + assert_not_nil match + assert_equal %w(age sex location), match.attribute_names + end +end diff --git a/activerecord/test/cases/deprecated_finder_test.rb b/activerecord/test/cases/deprecated_finder_test.rb deleted file mode 100644 index 2afc91b769..0000000000 --- a/activerecord/test/cases/deprecated_finder_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "cases/helper" -require 'models/entrant' - -class DeprecatedFinderTest < ActiveRecord::TestCase - fixtures :entrants - - def test_deprecated_find_all_was_removed - assert_raise(NoMethodError) { Entrant.find_all } - end - - def test_deprecated_find_first_was_removed - assert_raise(NoMethodError) { Entrant.find_first } - end - - def test_deprecated_find_on_conditions_was_removed - assert_raise(NoMethodError) { Entrant.find_on_conditions } - end - - def test_count - assert_equal(0, Entrant.count(:conditions => "id > 3")) - assert_equal(1, Entrant.count(:conditions => ["id > ?", 2])) - assert_equal(2, Entrant.count(:conditions => ["id > ?", 1])) - end - - def test_count_by_sql - assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3")) - assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2])) - assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1])) - end -end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 54e0b40b4f..2650040a80 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -288,7 +288,7 @@ class DirtyTest < ActiveRecord::TestCase with_partial_updates Pirate, false do assert_queries(2) { 2.times { pirate.save! } } - Pirate.update_all({ :updated_on => old_updated_on }, :id => pirate.id) + Pirate.where(id: pirate.id).update_all(:updated_on => old_updated_on) end with_partial_updates Pirate, true do @@ -306,7 +306,7 @@ class DirtyTest < ActiveRecord::TestCase with_partial_updates Person, false do assert_queries(2) { 2.times { person.save! } } - Person.update_all({ :first_name => 'baz' }, :id => person.id) + Person.where(id: person.id).update_all(:first_name => 'baz') end with_partial_updates Person, true do diff --git a/activerecord/test/cases/dynamic_finder_match_test.rb b/activerecord/test/cases/dynamic_finder_match_test.rb deleted file mode 100644 index e576870317..0000000000 --- a/activerecord/test/cases/dynamic_finder_match_test.rb +++ /dev/null @@ -1,98 +0,0 @@ -require "cases/helper" - -module ActiveRecord - class DynamicFinderMatchTest < ActiveRecord::TestCase - def test_find_or_create_by - match = DynamicFinderMatch.match("find_or_create_by_age_and_sex_and_location") - assert_not_nil match - assert !match.finder? - assert match.instantiator? - assert_equal :first, match.finder - assert_equal :create, match.instantiator - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_or_initialize_by - match = DynamicFinderMatch.match("find_or_initialize_by_age_and_sex_and_location") - assert_not_nil match - assert !match.finder? - assert match.instantiator? - assert_equal :first, match.finder - assert_equal :new, match.instantiator - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_no_match - assert_nil DynamicFinderMatch.match("not_a_finder") - end - - def find_by_bang - match = DynamicFinderMatch.match("find_by_age_and_sex_and_location!") - assert_not_nil match - assert match.finder? - assert match.bang? - assert_equal :first, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_by - match = DynamicFinderMatch.match("find_by_age_and_sex_and_location") - assert_not_nil match - assert match.finder? - assert_equal :first, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_by_with_symbol - m = DynamicFinderMatch.match(:find_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_all_by_with_symbol - m = DynamicFinderMatch.match(:find_all_by_foo) - assert_equal :all, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_all_by - match = DynamicFinderMatch.match("find_all_by_age_and_sex_and_location") - assert_not_nil match - assert match.finder? - assert_equal :all, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_last_by - m = DynamicFinderMatch.match(:find_last_by_foo) - assert_equal :last, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_by! - m = DynamicFinderMatch.match(:find_by_foo!) - assert_equal :first, m.finder - assert m.bang?, 'should be banging' - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_or_create - m = DynamicFinderMatch.match(:find_or_create_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - assert_equal :create, m.instantiator - end - - def test_find_or_initialize - m = DynamicFinderMatch.match(:find_or_initialize_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - assert_equal :new, m.instantiator - end - - def test_garbage - assert !DynamicFinderMatch.match(:fooo), 'should be false' - assert !DynamicFinderMatch.match(:find_by), 'should be false' - end - end -end diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 83c9b6e107..cb7781f8e7 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -28,7 +28,9 @@ if ActiveRecord::Base.connection.supports_explain? original = base.logger base.logger = nil - base.logger.expects(:warn).never + class << base.logger + def warn; raise "Should not be called" end + end with_threshold(0) do car = Car.where(:name => 'honda').first diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 235805a67c..810c1500cc 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -56,6 +56,16 @@ class FinderRespondToTest < ActiveRecord::TestCase assert_respond_to Topic, :find_or_create_by_title_and_author_name end + def test_should_respond_to_find_or_create_from_one_attribute_bang + ensure_topic_method_is_not_cached(:find_or_create_by_title!) + assert_respond_to Topic, :find_or_create_by_title! + end + + def test_should_respond_to_find_or_create_from_two_attributes_bang + ensure_topic_method_is_not_cached(:find_or_create_by_title_and_author_name!) + assert_respond_to Topic, :find_or_create_by_title_and_author_name! + end + def test_should_not_respond_to_find_by_one_missing_attribute assert !Topic.respond_to?(:find_by_undertitle) end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 76c041397a..f7ecab28ce 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -32,6 +32,7 @@ class FinderTest < ActiveRecord::TestCase assert Topic.exists?(:author_name => "Mary", :approved => true) assert Topic.exists?(["parent_id = ?", 1]) assert !Topic.exists?(45) + assert !Topic.exists?(Topic.new) begin assert !Topic.exists?("foo") @@ -82,12 +83,6 @@ class FinderTest < ActiveRecord::TestCase Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) end - def test_exists_with_scoped_include - Developer.send(:with_scope, :find => { :include => :projects, :order => "projects.name" }) do - assert Developer.exists? - end - end - def test_exists_does_not_instantiate_records Developer.expects(:instantiate).never Developer.exists? @@ -104,14 +99,14 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_ids_with_limit_and_offset - assert_equal 2, Entrant.find([1,3,2], :limit => 2).size - assert_equal 1, Entrant.find([1,3,2], :limit => 3, :offset => 2).size + assert_equal 2, Entrant.scoped(:limit => 2).find([1,3,2]).size + assert_equal 1, Entrant.scoped(:limit => 3, :offset => 2).find([1,3,2]).size # Also test an edge case: If you have 11 results, and you set a # limit of 3 and offset of 9, then you should find that there # will be only 2 results, regardless of the limit. - devs = Developer.find :all - last_devs = Developer.find devs.map(&:id), :limit => 3, :offset => 9 + devs = Developer.all + last_devs = Developer.scoped(:limit => 3, :offset => 9).find devs.map(&:id) assert_equal 2, last_devs.size end @@ -119,58 +114,12 @@ class FinderTest < ActiveRecord::TestCase assert_equal [], Topic.find([]) end - def test_find_by_ids_missing_one - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) } - end - - def test_find_all_with_limit - assert_equal(2, Entrant.find(:all, :limit => 2).size) - assert_equal(0, Entrant.find(:all, :limit => 0).size) + def test_find_doesnt_have_implicit_ordering + assert_sql(/^((?!ORDER).)*$/) { Topic.find(1) } end - def test_find_all_with_prepared_limit_and_offset - entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 1) - - assert_equal(2, entrants.size) - assert_equal(entrants(:second).name, entrants.first.name) - - assert_equal 3, Entrant.count - entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 2) - assert_equal(1, entrants.size) - assert_equal(entrants(:third).name, entrants.first.name) - end - - def test_find_all_with_limit_and_offset_and_multiple_order_clauses - first_three_posts = Post.find :all, :order => 'author_id, id', :limit => 3, :offset => 0 - second_three_posts = Post.find :all, :order => ' author_id,id ', :limit => 3, :offset => 3 - third_three_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6 - last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 9 - - assert_equal [[0,3],[1,1],[1,2]], first_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[2,7],[2,9],[2,11]], third_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[3,8],[3,10]], last_posts.map { |p| [p.author_id, p.id] } - end - - - def test_find_with_group - developers = Developer.find(:all, :group => "salary", :select => "salary") - assert_equal 4, developers.size - assert_equal 4, developers.map(&:salary).uniq.size - end - - def test_find_with_group_and_having - developers = Developer.find(:all, :group => "salary", :having => "sum(salary) > 10000", :select => "salary") - assert_equal 3, developers.size - assert_equal 3, developers.map(&:salary).uniq.size - assert developers.all? { |developer| developer.salary > 10000 } - end - - def test_find_with_group_and_sanitized_having - developers = Developer.find(:all, :group => "salary", :having => ["sum(salary) > ?", 10000], :select => "salary") - assert_equal 3, developers.size - assert_equal 3, developers.map(&:salary).uniq.size - assert developers.all? { |developer| developer.salary > 10000 } + def test_find_by_ids_missing_one + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) } end def test_find_with_group_and_sanitized_having_method @@ -199,14 +148,24 @@ class FinderTest < ActiveRecord::TestCase assert_equal [Account], accounts.collect(&:class).uniq end - def test_find_first - first = Topic.find(:first, :conditions => "title = 'The First Topic'") - assert_equal(topics(:first).title, first.title) + def test_take + assert_equal topics(:first), Topic.take + end + + def test_take_failing + assert_nil Topic.where("title = 'This title does not exist'").take end - def test_find_first_failing - first = Topic.find(:first, :conditions => "title = 'The First Topic!'") - assert_nil(first) + def test_take_bang_present + assert_nothing_raised do + assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").take! + end + end + + def test_take_bang_missing + assert_raises ActiveRecord::RecordNotFound do + Topic.where("title = 'This title does not exist'").take! + end end def test_first @@ -229,6 +188,12 @@ class FinderTest < ActiveRecord::TestCase end end + def test_first_have_primary_key_order_by_default + expected = topics(:first) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.first + end + def test_model_class_responds_to_first_bang assert Topic.first! Topic.delete_all @@ -257,7 +222,8 @@ class FinderTest < ActiveRecord::TestCase end end - def test_first_and_last_with_integer_should_use_sql_limit + def test_take_and_first_and_last_with_integer_should_use_sql_limit + assert_sql(/LIMIT 3|ROWNUM <= 3/) { Topic.take(3).entries } assert_sql(/LIMIT 2|ROWNUM <= 2/) { Topic.first(2).entries } assert_sql(/LIMIT 5|ROWNUM <= 5/) { Topic.last(5).entries } end @@ -278,7 +244,8 @@ class FinderTest < ActiveRecord::TestCase assert_no_match(/LIMIT/, query.first) end - def test_first_and_last_with_integer_should_return_an_array + def test_take_and_first_and_last_with_integer_should_return_an_array + assert_kind_of Array, Topic.take(5) assert_kind_of Array, Topic.first(5) assert_kind_of Array, Topic.last(5) end @@ -292,7 +259,7 @@ class FinderTest < ActiveRecord::TestCase end def test_find_only_some_columns - topic = Topic.find(1, :select => "author_name") + topic = Topic.scoped(:select => "author_name").find(1) assert_raise(ActiveModel::MissingAttributeError) {topic.title} assert_nil topic.read_attribute("title") assert_equal "David", topic.author_name @@ -302,36 +269,24 @@ class FinderTest < ActiveRecord::TestCase assert_respond_to topic, "author_name" end - def test_find_on_blank_conditions - [nil, " ", [], {}].each do |blank| - assert_nothing_raised { Topic.find(:first, :conditions => blank) } - end - end - - def test_find_on_blank_bind_conditions - [ [""], ["",{}] ].each do |blank| - assert_nothing_raised { Topic.find(:first, :conditions => blank) } - end - end - def test_find_on_array_conditions - assert Topic.find(1, :conditions => ["approved = ?", false]) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) } + assert Topic.scoped(:where => ["approved = ?", false]).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => ["approved = ?", true]).find(1) } end def test_find_on_hash_conditions - assert Topic.find(1, :conditions => { :approved => false }) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) } + assert Topic.scoped(:where => { :approved => false }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :approved => true }).find(1) } end def test_find_on_hash_conditions_with_explicit_table_name - assert Topic.find(1, :conditions => { 'topics.approved' => false }) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) } + assert Topic.scoped(:where => { 'topics.approved' => false }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { 'topics.approved' => true }).find(1) } end def test_find_on_hash_conditions_with_hashed_table_name - assert Topic.find(1, :conditions => {:topics => { :approved => false }}) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) } + assert Topic.scoped(:where => {:topics => { :approved => false }}).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => {:topics => { :approved => true }}).find(1) } end def test_find_with_hash_conditions_on_joined_table @@ -341,89 +296,89 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_hash_conditions_on_joined_table_and_with_range - firms = DependentFirm.all :joins => :account, :conditions => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }} + firms = DependentFirm.scoped :joins => :account, :where => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }} assert_equal 1, firms.size assert_equal companies(:rails_core), firms.first end def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate david = customers(:david) - assert Customer.find(david.id, :conditions => { 'customers.name' => david.name, :address => david.address }) + assert Customer.scoped(:where => { 'customers.name' => david.name, :address => david.address }).find(david.id) assert_raise(ActiveRecord::RecordNotFound) { - Customer.find(david.id, :conditions => { 'customers.name' => david.name + "1", :address => david.address }) + Customer.scoped(:where => { 'customers.name' => david.name + "1", :address => david.address }).find(david.id) } end def test_find_on_association_proxy_conditions - assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10, 12], Comment.find_all_by_post_id(authors(:david).posts).map(&:id).sort + assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10, 12], Comment.where(post_id: authors(:david).posts).map(&:id).sort end def test_find_on_hash_conditions_with_range - assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1..2 }).map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :id => 2..3 }) } + assert_equal [1,2], Topic.scoped(:where => { :id => 1..2 }).all.map(&:id).sort + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :id => 2..3 }).find(1) } end def test_find_on_hash_conditions_with_end_exclusive_range - assert_equal [1,2,3], Topic.find(:all, :conditions => { :id => 1..3 }).map(&:id).sort - assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1...3 }).map(&:id).sort - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(3, :conditions => { :id => 2...3 }) } + assert_equal [1,2,3], Topic.scoped(:where => { :id => 1..3 }).all.map(&:id).sort + assert_equal [1,2], Topic.scoped(:where => { :id => 1...3 }).all.map(&:id).sort + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :id => 2...3 }).find(3) } end def test_find_on_hash_conditions_with_multiple_ranges - assert_equal [1,2,3], Comment.find(:all, :conditions => { :id => 1..3, :post_id => 1..2 }).map(&:id).sort - assert_equal [1], Comment.find(:all, :conditions => { :id => 1..1, :post_id => 1..10 }).map(&:id).sort + assert_equal [1,2,3], Comment.scoped(:where => { :id => 1..3, :post_id => 1..2 }).all.map(&:id).sort + assert_equal [1], Comment.scoped(:where => { :id => 1..1, :post_id => 1..10 }).all.map(&:id).sort end def test_find_on_hash_conditions_with_array_of_integers_and_ranges - assert_equal [1,2,3,5,6,7,8,9], Comment.find(:all, :conditions => {:id => [1..2, 3, 5, 6..8, 9]}).map(&:id).sort + assert_equal [1,2,3,5,6,7,8,9], Comment.scoped(:where => {:id => [1..2, 3, 5, 6..8, 9]}).all.map(&:id).sort end def test_find_on_multiple_hash_conditions - assert Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }) - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) } + assert Topic.scoped(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.scoped(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) } end def test_condition_interpolation - assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).written_on + assert_kind_of Firm, Company.where("name = '%s'", "37signals").first + assert_nil Company.scoped(:where => ["name = '%s'", "37signals!"]).first + assert_nil Company.scoped(:where => ["name = '%s'", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.scoped(:where => ["id = %d", 1]).first.written_on end def test_condition_array_interpolation - assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"]) - assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).written_on + assert_kind_of Firm, Company.scoped(:where => ["name = '%s'", "37signals"]).first + assert_nil Company.scoped(:where => ["name = '%s'", "37signals!"]).first + assert_nil Company.scoped(:where => ["name = '%s'", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.scoped(:where => ["id = %d", 1]).first.written_on end def test_condition_hash_interpolation - assert_kind_of Firm, Company.find(:first, :conditions => { :name => "37signals"}) - assert_nil Company.find(:first, :conditions => { :name => "37signals!"}) - assert_kind_of Time, Topic.find(:first, :conditions => {:id => 1}).written_on + assert_kind_of Firm, Company.scoped(:where => { :name => "37signals"}).first + assert_nil Company.scoped(:where => { :name => "37signals!"}).first + assert_kind_of Time, Topic.scoped(:where => {:id => 1}).first.written_on end def test_hash_condition_find_malformed assert_raise(ActiveRecord::StatementInvalid) { - Company.find(:first, :conditions => { :id => 2, :dhh => true }) + Company.scoped(:where => { :id => 2, :dhh => true }).first } end def test_hash_condition_find_with_escaped_characters Company.create("name" => "Ain't noth'n like' \#stuff") - assert Company.find(:first, :conditions => { :name => "Ain't noth'n like' \#stuff" }) + assert Company.scoped(:where => { :name => "Ain't noth'n like' \#stuff" }).first end def test_hash_condition_find_with_array - p1, p2 = Post.find(:all, :limit => 2, :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => { :id => [p1, p2] }, :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => { :id => [p1, p2.id] }, :order => 'id asc') + p1, p2 = Post.scoped(:limit => 2, :order => 'id asc').all + assert_equal [p1, p2], Post.scoped(:where => { :id => [p1, p2] }, :order => 'id asc').all + assert_equal [p1, p2], Post.scoped(:where => { :id => [p1, p2.id] }, :order => 'id asc').all end def test_hash_condition_find_with_nil - topic = Topic.find(:first, :conditions => { :last_read => nil } ) + topic = Topic.scoped(:where => { :last_read => nil } ).first assert_not_nil topic assert_nil topic.last_read end @@ -431,42 +386,42 @@ class FinderTest < ActiveRecord::TestCase def test_hash_condition_find_with_aggregate_having_one_mapping balance = customers(:david).balance assert_kind_of Money, balance - found_customer = Customer.find(:first, :conditions => {:balance => balance}) + found_customer = Customer.scoped(:where => {:balance => balance}).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate gps_location = customers(:david).gps_location assert_kind_of GpsLocation, gps_location - found_customer = Customer.find(:first, :conditions => {:gps_location => gps_location}) + found_customer = Customer.scoped(:where => {:gps_location => gps_location}).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_having_one_mapping_and_key_value_being_attribute_value balance = customers(:david).balance assert_kind_of Money, balance - found_customer = Customer.find(:first, :conditions => {:balance => balance.amount}) + found_customer = Customer.scoped(:where => {:balance => balance.amount}).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_attribute_value gps_location = customers(:david).gps_location assert_kind_of GpsLocation, gps_location - found_customer = Customer.find(:first, :conditions => {:gps_location => gps_location.gps_location}) + found_customer = Customer.scoped(:where => {:gps_location => gps_location.gps_location}).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_aggregate_having_three_mappings address = customers(:david).address assert_kind_of Address, address - found_customer = Customer.find(:first, :conditions => {:address => address}) + found_customer = Customer.scoped(:where => {:address => address}).first assert_equal customers(:david), found_customer end def test_hash_condition_find_with_one_condition_being_aggregate_and_another_not address = customers(:david).address assert_kind_of Address, address - found_customer = Customer.find(:first, :conditions => {:address => address, :name => customers(:david).name}) + found_customer = Customer.scoped(:where => {:address => address, :name => customers(:david).name}).first assert_equal customers(:david), found_customer end @@ -474,7 +429,7 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :local do topic = Topic.first - assert_equal topic, Topic.find(:first, :conditions => ['written_on = ?', topic.written_on.getutc]) + assert_equal topic, Topic.scoped(:where => ['written_on = ?', topic.written_on.getutc]).first end end end @@ -483,7 +438,7 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :local do topic = Topic.first - assert_equal topic, Topic.find(:first, :conditions => {:written_on => topic.written_on.getutc}) + assert_equal topic, Topic.scoped(:where => {:written_on => topic.written_on.getutc}).first end end end @@ -492,7 +447,7 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :utc do topic = Topic.first - assert_equal topic, Topic.find(:first, :conditions => ['written_on = ?', topic.written_on.getlocal]) + assert_equal topic, Topic.scoped(:where => ['written_on = ?', topic.written_on.getlocal]).first end end end @@ -501,32 +456,32 @@ class FinderTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do with_active_record_default_timezone :utc do topic = Topic.first - assert_equal topic, Topic.find(:first, :conditions => {:written_on => topic.written_on.getlocal}) + assert_equal topic, Topic.scoped(:where => {:written_on => topic.written_on.getlocal}).first end end end def test_bind_variables - assert_kind_of Firm, Company.find(:first, :conditions => ["name = ?", "37signals"]) - assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"]) - assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!' OR 1=1"]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = ?", 1]).written_on + assert_kind_of Firm, Company.scoped(:where => ["name = ?", "37signals"]).first + assert_nil Company.scoped(:where => ["name = ?", "37signals!"]).first + assert_nil Company.scoped(:where => ["name = ?", "37signals!' OR 1=1"]).first + assert_kind_of Time, Topic.scoped(:where => ["id = ?", 1]).first.written_on assert_raise(ActiveRecord::PreparedStatementInvalid) { - Company.find(:first, :conditions => ["id=? AND name = ?", 2]) + Company.scoped(:where => ["id=? AND name = ?", 2]).first } assert_raise(ActiveRecord::PreparedStatementInvalid) { - Company.find(:first, :conditions => ["id=?", 2, 3, 4]) + Company.scoped(:where => ["id=?", 2, 3, 4]).first } end def test_bind_variables_with_quotes Company.create("name" => "37signals' go'es agains") - assert Company.find(:first, :conditions => ["name = ?", "37signals' go'es agains"]) + assert Company.scoped(:where => ["name = ?", "37signals' go'es agains"]).first end def test_named_bind_variables_with_quotes Company.create("name" => "37signals' go'es agains") - assert Company.find(:first, :conditions => ["name = :name", {:name => "37signals' go'es agains"}]) + assert Company.scoped(:where => ["name = :name", {:name => "37signals' go'es agains"}]).first end def test_bind_arity @@ -544,10 +499,10 @@ class FinderTest < ActiveRecord::TestCase assert_nothing_raised { bind("'+00:00'", :foo => "bar") } - assert_kind_of Firm, Company.find(:first, :conditions => ["name = :name", { :name => "37signals" }]) - assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!" }]) - assert_nil Company.find(:first, :conditions => ["name = :name", { :name => "37signals!' OR 1=1" }]) - assert_kind_of Time, Topic.find(:first, :conditions => ["id = :id", { :id => 1 }]).written_on + assert_kind_of Firm, Company.scoped(:where => ["name = :name", { :name => "37signals" }]).first + assert_nil Company.scoped(:where => ["name = :name", { :name => "37signals!" }]).first + assert_nil Company.scoped(:where => ["name = :name", { :name => "37signals!' OR 1=1" }]).first + assert_kind_of Time, Topic.scoped(:where => ["id = :id", { :id => 1 }]).first.written_on end class SimpleEnumerable @@ -618,12 +573,6 @@ class FinderTest < ActiveRecord::TestCase assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table") end - def test_count - assert_equal(0, Entrant.count(:conditions => "id > 3")) - assert_equal(1, Entrant.count(:conditions => ["id > ?", 2])) - assert_equal(2, Entrant.count(:conditions => ["id > ?", 1])) - end - def test_count_by_sql assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3")) assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2])) @@ -640,13 +589,8 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") } end - def test_find_by_one_attribute_with_order_option - assert_equal accounts(:signals37), Account.find_by_credit_limit(50, :order => 'id') - assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :order => 'id DESC') - end - def test_find_by_one_attribute_with_conditions - assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) + assert_equal accounts(:rails_core_account), Account.where('firm_id = ?', 6).find_by_credit_limit(50) end def test_find_by_one_attribute_that_is_an_aggregate @@ -686,12 +630,12 @@ class FinderTest < ActiveRecord::TestCase def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching # ensure this test can run independently of order class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' } - a = Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) - assert_equal a, Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6]) # find_by_credit_limit has been cached + a = Account.where('firm_id = ?', 6).find_by_credit_limit(50) + assert_equal a, Account.where('firm_id = ?', 6).find_by_credit_limit(50) # find_by_credit_limit has been cached end def test_find_by_one_attribute_with_several_options - assert_equal accounts(:unknown), Account.find_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3]) + assert_equal accounts(:unknown), Account.order('id DESC').where('id != ?', 3).find_by_credit_limit(50) end def test_find_by_one_missing_attribute @@ -714,345 +658,26 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") } end - def test_find_last_by_one_attribute - assert_equal Topic.last, Topic.find_last_by_title(Topic.last.title) - assert_nil Topic.find_last_by_title("A title with no matches") - end - - def test_find_last_by_invalid_method_syntax - assert_raise(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") } - assert_raise(NoMethodError) { Topic.find_last_by_title?("The First Topic") } - end - - def test_find_last_by_one_attribute_with_several_options - assert_equal accounts(:signals37), Account.find_last_by_credit_limit(50, :order => 'id DESC', :conditions => ['id != ?', 3]) - end - - def test_find_last_by_one_missing_attribute - assert_raise(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") } - end - - def test_find_last_by_two_attributes - topic = Topic.last - assert_equal topic, Topic.find_last_by_title_and_author_name(topic.title, topic.author_name) - assert_nil Topic.find_last_by_title_and_author_name(topic.title, "Anonymous") - end - - def test_find_last_with_limit_gives_same_result_when_loaded_and_unloaded - scope = Topic.limit(2) - unloaded_last = scope.last - loaded_last = scope.all.last - assert_equal loaded_last, unloaded_last - end - - def test_find_last_with_limit_and_offset_gives_same_result_when_loaded_and_unloaded - scope = Topic.offset(2).limit(2) - unloaded_last = scope.last - loaded_last = scope.all.last - assert_equal loaded_last, unloaded_last - end - - def test_find_last_with_offset_gives_same_result_when_loaded_and_unloaded - scope = Topic.offset(3) - unloaded_last = scope.last - loaded_last = scope.all.last - assert_equal loaded_last, unloaded_last - end - - def test_find_all_by_one_attribute - topics = Topic.find_all_by_content("Have a nice day") - assert_equal 2, topics.size - assert topics.include?(topics(:first)) - - assert_equal [], Topic.find_all_by_title("The First Topic!!") - end - - def test_find_all_by_one_attribute_which_is_a_symbol - topics = Topic.find_all_by_content("Have a nice day".to_sym) - assert_equal 2, topics.size - assert topics.include?(topics(:first)) - - assert_equal [], Topic.find_all_by_title("The First Topic!!") - end - - def test_find_all_by_one_attribute_that_is_an_aggregate - balance = customers(:david).balance - assert_kind_of Money, balance - found_customers = Customer.find_all_by_balance(balance) - assert_equal 1, found_customers.size - assert_equal customers(:david), found_customers.first - end - - def test_find_all_by_two_attributes_that_are_both_aggregates - balance = customers(:david).balance - address = customers(:david).address - assert_kind_of Money, balance - assert_kind_of Address, address - found_customers = Customer.find_all_by_balance_and_address(balance, address) - assert_equal 1, found_customers.size - assert_equal customers(:david), found_customers.first - end - - def test_find_all_by_two_attributes_with_one_being_an_aggregate - balance = customers(:david).balance - assert_kind_of Money, balance - found_customers = Customer.find_all_by_balance_and_name(balance, customers(:david).name) - assert_equal 1, found_customers.size - assert_equal customers(:david), found_customers.first - end - - def test_find_all_by_one_attribute_with_options - topics = Topic.find_all_by_content("Have a nice day", :order => "id DESC") - assert_equal topics(:first), topics.last - - topics = Topic.find_all_by_content("Have a nice day", :order => "id") - assert_equal topics(:first), topics.first - end - - def test_find_all_by_array_attribute - assert_equal 2, Topic.find_all_by_title(["The First Topic", "The Second Topic of the day"]).size - end - - def test_find_all_by_boolean_attribute - topics = Topic.find_all_by_approved(false) - assert_equal 1, topics.size - assert topics.include?(topics(:first)) - - topics = Topic.find_all_by_approved(true) - assert_equal 3, topics.size - assert topics.include?(topics(:second)) - end - def test_find_by_nil_attribute topic = Topic.find_by_last_read nil assert_not_nil topic assert_nil topic.last_read end - def test_find_all_by_nil_attribute - topics = Topic.find_all_by_last_read nil - assert_equal 3, topics.size - assert topics.collect(&:last_read).all?(&:nil?) - end - def test_find_by_nil_and_not_nil_attributes topic = Topic.find_by_last_read_and_author_name nil, "Mary" assert_equal "Mary", topic.author_name end - def test_find_all_by_nil_and_not_nil_attributes - topics = Topic.find_all_by_last_read_and_author_name nil, "Mary" - assert_equal 1, topics.size - assert_equal "Mary", topics[0].author_name - end - - def test_find_or_create_from_one_attribute - number_of_companies = Company.count - sig38 = Company.find_or_create_by_name("38signals") - assert_equal number_of_companies + 1, Company.count - assert_equal sig38, Company.find_or_create_by_name("38signals") - assert sig38.persisted? - end - - def test_find_or_create_from_two_attributes - number_of_topics = Topic.count - another = Topic.find_or_create_by_title_and_author_name("Another topic","John") - assert_equal number_of_topics + 1, Topic.count - assert_equal another, Topic.find_or_create_by_title_and_author_name("Another topic", "John") - assert another.persisted? - end - - def test_find_or_create_from_two_attributes_with_one_being_an_aggregate - number_of_customers = Customer.count - created_customer = Customer.find_or_create_by_balance_and_name(Money.new(123), "Elizabeth") - assert_equal number_of_customers + 1, Customer.count - assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123), "Elizabeth") - assert created_customer.persisted? - end - - def test_find_or_create_from_one_attribute_and_hash - number_of_companies = Company.count - sig38 = Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) - assert_equal number_of_companies + 1, Company.count - assert_equal sig38, Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) - assert sig38.persisted? - assert_equal "38signals", sig38.name - assert_equal 17, sig38.firm_id - assert_equal 23, sig38.client_of - end - - def test_find_or_create_from_two_attributes_and_hash - number_of_companies = Company.count - sig38 = Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23}) - assert_equal number_of_companies + 1, Company.count - assert_equal sig38, Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23}) - assert sig38.persisted? - assert_equal "38signals", sig38.name - assert_equal 17, sig38.firm_id - assert_equal 23, sig38.client_of - end - - def test_find_or_create_from_one_aggregate_attribute - number_of_customers = Customer.count - created_customer = Customer.find_or_create_by_balance(Money.new(123)) - assert_equal number_of_customers + 1, Customer.count - assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123)) - assert created_customer.persisted? - end - - def test_find_or_create_from_one_aggregate_attribute_and_hash - number_of_customers = Customer.count - balance = Money.new(123) - name = "Elizabeth" - created_customer = Customer.find_or_create_by_balance({:balance => balance, :name => name}) - assert_equal number_of_customers + 1, Customer.count - assert_equal created_customer, Customer.find_or_create_by_balance({:balance => balance, :name => name}) - assert created_customer.persisted? - assert_equal balance, created_customer.balance - assert_equal name, created_customer.name - end - - def test_find_or_initialize_from_one_attribute - sig38 = Company.find_or_initialize_by_name("38signals") - assert_equal "38signals", sig38.name - assert !sig38.persisted? - end - - def test_find_or_initialize_from_one_aggregate_attribute - new_customer = Customer.find_or_initialize_by_balance(Money.new(123)) - assert_equal 123, new_customer.balance.amount - assert !new_customer.persisted? - end - - def test_find_or_initialize_from_one_attribute_should_not_set_attribute_even_when_protected - c = Company.find_or_initialize_by_name({:name => "Fortune 1000", :rating => 1000}) - assert_equal "Fortune 1000", c.name - assert_not_equal 1000, c.rating - assert c.valid? - assert !c.persisted? - end - - def test_find_or_create_from_one_attribute_should_not_set_attribute_even_when_protected - c = Company.find_or_create_by_name({:name => "Fortune 1000", :rating => 1000}) - assert_equal "Fortune 1000", c.name - assert_not_equal 1000, c.rating - assert c.valid? - assert c.persisted? - end - - def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected - c = Company.find_or_initialize_by_name_and_rating("Fortune 1000", 1000) - assert_equal "Fortune 1000", c.name - assert_equal 1000, c.rating - assert c.valid? - assert !c.persisted? - end - - def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected - c = Company.find_or_create_by_name_and_rating("Fortune 1000", 1000) - assert_equal "Fortune 1000", c.name - assert_equal 1000, c.rating - assert c.valid? - assert c.persisted? - end - - def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash - c = Company.find_or_initialize_by_rating(1000, {:name => "Fortune 1000"}) - assert_equal "Fortune 1000", c.name - assert_equal 1000, c.rating - assert c.valid? - assert !c.persisted? - end - - def test_find_or_create_from_one_attribute_should_set_attribute_even_when_protected_and_also_set_the_hash - c = Company.find_or_create_by_rating(1000, {:name => "Fortune 1000"}) - assert_equal "Fortune 1000", c.name - assert_equal 1000, c.rating - assert c.valid? - assert c.persisted? - end - - def test_find_or_initialize_should_set_protected_attributes_if_given_as_block - c = Company.find_or_initialize_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } - assert_equal "Fortune 1000", c.name - assert_equal 1000.to_f, c.rating.to_f - assert c.valid? - assert !c.persisted? - end - - def test_find_or_create_should_set_protected_attributes_if_given_as_block - c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } - assert_equal "Fortune 1000", c.name - assert_equal 1000.to_f, c.rating.to_f - assert c.valid? - assert c.persisted? - end - - def test_find_or_create_should_work_with_block_on_first_call - class << Company - undef_method(:find_or_create_by_name) if method_defined?(:find_or_create_by_name) - end - c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 } - assert_equal "Fortune 1000", c.name - assert_equal 1000.to_f, c.rating.to_f - assert c.valid? - assert c.persisted? - end - - def test_find_or_initialize_from_two_attributes - another = Topic.find_or_initialize_by_title_and_author_name("Another topic","John") - assert_equal "Another topic", another.title - assert_equal "John", another.author_name - assert !another.persisted? - end - - def test_find_or_initialize_from_two_attributes_but_passing_only_one - assert_raise(ArgumentError) { Topic.find_or_initialize_by_title_and_author_name("Another topic") } - end - - def test_find_or_initialize_from_one_aggregate_attribute_and_one_not - new_customer = Customer.find_or_initialize_by_balance_and_name(Money.new(123), "Elizabeth") - assert_equal 123, new_customer.balance.amount - assert_equal "Elizabeth", new_customer.name - assert !new_customer.persisted? - end - - def test_find_or_initialize_from_one_attribute_and_hash - sig38 = Company.find_or_initialize_by_name({:name => "38signals", :firm_id => 17, :client_of => 23}) - assert_equal "38signals", sig38.name - assert_equal 17, sig38.firm_id - assert_equal 23, sig38.client_of - assert !sig38.persisted? - end - - def test_find_or_initialize_from_one_aggregate_attribute_and_hash - balance = Money.new(123) - name = "Elizabeth" - new_customer = Customer.find_or_initialize_by_balance({:balance => balance, :name => name}) - assert_equal balance, new_customer.balance - assert_equal name, new_customer.name - assert !new_customer.persisted? - end - def test_find_with_bad_sql assert_raise(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" } end - def test_find_with_invalid_params - assert_raise(ArgumentError) { Topic.find :first, :join => "It should be `joins'" } - assert_raise(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" } - end - - def test_dynamic_finder_with_invalid_params - assert_raise(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" } - end - def test_find_all_with_join - developers_on_project_one = Developer.find( - :all, + developers_on_project_one = Developer.scoped( :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', - :conditions => 'project_id=1' - ) + :where => 'project_id=1' + ).all assert_equal 3, developers_on_project_one.length developer_names = developers_on_project_one.map { |d| d.name } assert developer_names.include?('David') @@ -1060,17 +685,15 @@ class FinderTest < ActiveRecord::TestCase end def test_joins_dont_clobber_id - first = Firm.find( - :first, + first = Firm.scoped( :joins => 'INNER JOIN companies clients ON clients.firm_id = companies.id', - :conditions => 'companies.id = 1' - ) + :where => 'companies.id = 1' + ).first assert_equal 1, first.id end def test_joins_with_string_array - person_with_reader_and_post = Post.find( - :all, + person_with_reader_and_post = Post.scoped( :joins => [ "INNER JOIN categorizations ON categorizations.post_id = posts.id", "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'" @@ -1081,15 +704,14 @@ class FinderTest < ActiveRecord::TestCase def test_find_by_id_with_conditions_with_or assert_nothing_raised do - Post.find([1,2,3], - :conditions => "posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'") + Post.where("posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'").find([1,2,3]) end end # http://dev.rubyonrails.org/ticket/6778 def test_find_ignores_previously_inserted_record Post.create!(:title => 'test', :body => 'it out') - assert_equal [], Post.find_all_by_id(nil) + assert_equal [], Post.where(id: nil) end def test_find_by_empty_ids @@ -1097,13 +719,13 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_empty_in_condition - assert_equal [], Post.find(:all, :conditions => ['id in (?)', []]) + assert_equal [], Post.where('id in (?)', []) end def test_find_by_records - p1, p2 = Post.find(:all, :limit => 2, :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => ['id in (?)', [p1, p2]], :order => 'id asc') - assert_equal [p1, p2], Post.find(:all, :conditions => ['id in (?)', [p1, p2.id]], :order => 'id asc') + p1, p2 = Post.scoped(:limit => 2, :order => 'id asc').all + assert_equal [p1, p2], Post.scoped(:where => ['id in (?)', [p1, p2]], :order => 'id asc') + assert_equal [p1, p2], Post.scoped(:where => ['id in (?)', [p1, p2.id]], :order => 'id asc') end def test_select_value @@ -1130,16 +752,15 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct - assert_equal 2, Post.find(:all, :include => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).size + assert_equal 2, Post.scoped(:includes => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).all.size - assert_equal 3, Post.find(:all, :include => { :author => :author_address, :authors => :author_address}, - :order => 'author_addresses_authors.id DESC ', :limit => 3).size + assert_equal 3, Post.scoped(:includes => { :author => :author_address, :authors => :author_address}, + :order => 'author_addresses_authors.id DESC ', :limit => 3).all.size end def test_find_with_nil_inside_set_passed_for_one_attribute - client_of = Company.find( - :all, - :conditions => { + client_of = Company.scoped( + :where => { :client_of => [2, 1, nil], :name => ['37signals', 'Summit', 'Microsoft'] }, :order => 'client_of DESC' @@ -1150,9 +771,8 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_nil_inside_set_passed_for_attribute - client_of = Company.find( - :all, - :conditions => { :client_of => [nil] }, + client_of = Company.scoped( + :where => { :client_of => [nil] }, :order => 'client_of DESC' ).map { |x| x.client_of } @@ -1160,22 +780,14 @@ class FinderTest < ActiveRecord::TestCase end def test_with_limiting_with_custom_select - posts = Post.find( - :all, :include => :author, :select => ' posts.*, authors.id as "author_id"', - :references => :authors, :limit => 3, :order => 'posts.id' - ) + posts = Post.references(:authors).scoped( + :includes => :author, :select => ' posts.*, authors.id as "author_id"', + :limit => 3, :order => 'posts.id' + ).all assert_equal 3, posts.size assert_equal [0, 1, 1], posts.map(&:author_id).sort end - def test_finder_with_scoped_from - all_topics = Topic.find(:all) - - Topic.send(:with_scope, :find => { :from => 'fake_topics' }) do - assert_equal all_topics, Topic.from('topics').to_a - end - end - def test_find_one_message_with_custom_primary_key Toy.primary_key = :name begin @@ -1185,6 +797,10 @@ class FinderTest < ActiveRecord::TestCase end end + def test_finder_with_offset_string + assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.scoped(:offset => "3").all } + end + protected def bind(statement, *vars) if vars.first.is_a?(Hash) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 562b370c97..c28f8de682 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -514,7 +514,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase self.use_transactional_fixtures = false def test_raises_error - assert_raise FixtureClassNotFound do + assert_raise ActiveRecord::FixtureClassNotFound do funny_jokes(:a_joke) end end @@ -716,7 +716,7 @@ class FoxyFixturesTest < ActiveRecord::TestCase end def test_only_generates_a_pk_if_necessary - m = Matey.find(:first) + m = Matey.first m.pirate = pirates(:blackbeard) m.target = pirates(:redbeard) end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 9f5f012073..37fa13f771 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -2,6 +2,7 @@ require File.expand_path('../../../../load_paths', __FILE__) require 'config' +gem 'minitest' require 'minitest/autorun' require 'stringio' require 'mocha' @@ -19,9 +20,6 @@ require 'support/connection' # Show backtraces for deprecated behavior for quicker cleanup. ActiveSupport::Deprecation.debug = true -# Enable Identity Map only when ENV['IM'] is set to "true" -ActiveRecord::IdentityMap.enabled = (ENV['IM'] == "true") - # Avoid deprecation warning setting dependent_restrict_raises to false. The default is true ActiveRecord::Base.dependent_restrict_raises = false @@ -39,7 +37,7 @@ def current_adapter?(*types) end def in_memory_db? - current_adapter?(:SQLiteAdapter) && + current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:" end @@ -83,8 +81,8 @@ class ActiveSupport::TestCase self.use_instantiated_fixtures = false self.use_transactional_fixtures = true - def create_fixtures(*table_names, &block) - ActiveRecord::Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, table_names, fixture_class_names, &block) + def create_fixtures(*fixture_set_names, &block) + ActiveRecord::Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block) end end diff --git a/activerecord/test/cases/identity_map/middleware_test.rb b/activerecord/test/cases/identity_map/middleware_test.rb deleted file mode 100644 index 5b32a1c6d2..0000000000 --- a/activerecord/test/cases/identity_map/middleware_test.rb +++ /dev/null @@ -1,74 +0,0 @@ -require "cases/helper" -require "rack" - -module ActiveRecord - module IdentityMap - class MiddlewareTest < ActiveRecord::TestCase - def setup - super - @enabled = IdentityMap.enabled - IdentityMap.enabled = false - end - - def teardown - super - IdentityMap.enabled = @enabled - IdentityMap.clear - end - - def test_delegates - called = false - mw = Middleware.new lambda { |env| - called = true - [200, {}, nil] - } - mw.call({}) - assert called, 'middleware delegated' - end - - def test_im_enabled_during_delegation - mw = Middleware.new lambda { |env| - assert IdentityMap.enabled?, 'identity map should be enabled' - [200, {}, nil] - } - mw.call({}) - end - - class Enum < Struct.new(:iter) - def each(&b) - iter.call(&b) - end - end - - def test_im_enabled_during_body_each - mw = Middleware.new lambda { |env| - [200, {}, Enum.new(lambda { |&b| - assert IdentityMap.enabled?, 'identity map should be enabled' - b.call "hello" - })] - } - body = mw.call({}).last - body.each { |x| assert_equal 'hello', x } - end - - def test_im_disabled_after_body_close - mw = Middleware.new lambda { |env| [200, {}, []] } - body = mw.call({}).last - assert IdentityMap.enabled?, 'identity map should be enabled' - body.close - assert !IdentityMap.enabled?, 'identity map should be disabled' - end - - def test_im_cleared_after_body_close - mw = Middleware.new lambda { |env| [200, {}, []] } - body = mw.call({}).last - - IdentityMap.repository['hello'] = 'world' - assert !IdentityMap.repository.empty?, 'repo should not be empty' - - body.close - assert IdentityMap.repository.empty?, 'repo should be empty' - end - end - end -end diff --git a/activerecord/test/cases/identity_map_test.rb b/activerecord/test/cases/identity_map_test.rb deleted file mode 100644 index 3efc8bf559..0000000000 --- a/activerecord/test/cases/identity_map_test.rb +++ /dev/null @@ -1,439 +0,0 @@ -require "cases/helper" - -require 'models/developer' -require 'models/project' -require 'models/company' -require 'models/topic' -require 'models/reply' -require 'models/computer' -require 'models/customer' -require 'models/order' -require 'models/post' -require 'models/author' -require 'models/tag' -require 'models/tagging' -require 'models/comment' -require 'models/sponsor' -require 'models/member' -require 'models/essay' -require 'models/subscriber' -require "models/pirate" -require "models/bird" -require "models/parrot" - -if ActiveRecord::IdentityMap.enabled? -class IdentityMapTest < ActiveRecord::TestCase - fixtures :accounts, :companies, :developers, :projects, :topics, - :developers_projects, :computers, :authors, :author_addresses, - :posts, :tags, :taggings, :comments, :subscribers - - ############################################################################## - # Basic tests checking if IM is functioning properly on basic find operations# - ############################################################################## - - def test_find_id - assert_same(Client.find(3), Client.find(3)) - end - - def test_find_id_without_identity_map - ActiveRecord::IdentityMap.without do - assert_not_same(Client.find(3), Client.find(3)) - end - end - - def test_find_id_use_identity_map - ActiveRecord::IdentityMap.enabled = false - ActiveRecord::IdentityMap.use do - assert_same(Client.find(3), Client.find(3)) - end - ActiveRecord::IdentityMap.enabled = true - end - - def test_find_pkey - assert_same( - Subscriber.find('swistak'), - Subscriber.find('swistak') - ) - end - - def test_find_by_id - assert_same( - Client.find_by_id(3), - Client.find_by_id(3) - ) - end - - def test_find_by_string_and_numeric_id - assert_same( - Client.find_by_id("3"), - Client.find_by_id(3) - ) - end - - def test_find_by_pkey - assert_same( - Subscriber.find_by_nick('swistak'), - Subscriber.find_by_nick('swistak') - ) - end - - def test_find_first_id - assert_same( - Client.find(:first, :conditions => {:id => 1}), - Client.find(:first, :conditions => {:id => 1}) - ) - end - - def test_find_first_pkey - assert_same( - Subscriber.find(:first, :conditions => {:nick => 'swistak'}), - Subscriber.find(:first, :conditions => {:nick => 'swistak'}) - ) - end - - def test_queries_are_not_executed_when_finding_by_id - Post.find 1 - assert_no_queries do - Post.find 1 - end - end - - ############################################################################## - # Tests checking if IM is functioning properly on more advanced finds # - # and associations # - ############################################################################## - - def test_owner_object_is_associated_from_identity_map - post = Post.find(1) - comment = post.comments.first - - assert_no_queries do - comment.post - end - assert_same post, comment.post - end - - def test_associated_object_are_assigned_from_identity_map - post = Post.find(1) - - post.comments.each do |comment| - assert_same post, comment.post - assert_equal post.object_id, comment.post.object_id - end - end - - def test_creation - t1 = Topic.create("title" => "t1") - t2 = Topic.find(t1.id) - assert_same(t1, t2) - end - - ############################################################################## - # Tests checking if IM is functioning properly on classes with multiple # - # types of inheritance # - ############################################################################## - - def test_inherited_without_type_attribute_without_identity_map - ActiveRecord::IdentityMap.without do - p1 = DestructivePirate.create!(:catchphrase => "I'm not a regular Pirate") - p2 = Pirate.find(p1.id) - assert_not_same(p1, p2) - end - end - - def test_inherited_with_type_attribute_without_identity_map - ActiveRecord::IdentityMap.without do - c = comments(:sub_special_comment) - c1 = SubSpecialComment.find(c.id) - c2 = Comment.find(c.id) - assert_same(c1.class, c2.class) - end - end - - def test_inherited_without_type_attribute - p1 = DestructivePirate.create!(:catchphrase => "I'm not a regular Pirate") - p2 = Pirate.find(p1.id) - assert_not_same(p1, p2) - end - - def test_inherited_with_type_attribute - c = comments(:sub_special_comment) - c1 = SubSpecialComment.find(c.id) - c2 = Comment.find(c.id) - assert_same(c1, c2) - end - - ############################################################################## - # Tests checking dirty attribute behavior with IM # - ############################################################################## - - def test_loading_new_instance_should_not_update_dirty_attributes - swistak = Subscriber.find(:first, :conditions => {:nick => 'swistak'}) - swistak.name = "Swistak Sreberkowiec" - assert_equal(["name"], swistak.changed) - assert_equal({"name" => ["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) - - assert swistak.name_changed? - assert_equal("Swistak Sreberkowiec", swistak.name) - end - - def test_loading_new_instance_should_change_dirty_attribute_original_value - swistak = Subscriber.find(:first, :conditions => {:nick => 'swistak'}) - swistak.name = "Swistak Sreberkowiec" - - Subscriber.update_all({:name => "Raczkowski Marcin"}, {:name => "Marcin Raczkowski"}) - - assert_equal({"name"=>["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) - assert_equal("Swistak Sreberkowiec", swistak.name) - end - - def test_loading_new_instance_should_remove_dirt - swistak = Subscriber.find(:first, :conditions => {:nick => 'swistak'}) - swistak.name = "Swistak Sreberkowiec" - - assert_equal({"name" => ["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) - - Subscriber.update_all({:name => "Swistak Sreberkowiec"}, {:name => "Marcin Raczkowski"}) - - assert_equal("Swistak Sreberkowiec", swistak.name) - assert_equal({"name"=>["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) - assert swistak.name_changed? - end - - def test_has_many_associations - pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?") - pirate.birds.create!(:name => 'Posideons Killer') - pirate.birds.create!(:name => 'Killer bandita Dionne') - - posideons, _ = pirate.birds - - pirate.reload - - pirate.birds_attributes = [{ :id => posideons.id, :name => 'Grace OMalley' }] - assert_equal 'Grace OMalley', pirate.birds.to_a.find { |r| r.id == posideons.id }.name - end - - def test_changing_associations - post1 = Post.create("title" => "One post", "body" => "Posting...") - post2 = Post.create("title" => "Another post", "body" => "Posting... Again...") - comment = Comment.new("body" => "comment") - - comment.post = post1 - assert comment.save - - assert_same(post1.comments.first, comment) - - comment.post = post2 - assert comment.save - - assert_same(post2.comments.first, comment) - assert_equal(0, post1.comments.size) - end - - def test_im_with_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins - tag = posts(:welcome).tags.first - tag_with_joins_and_select = posts(:welcome).tags.add_joins_and_select.first - assert_same(tag, tag_with_joins_and_select) - assert_nothing_raised(NoMethodError, "Joins/select was not loaded") { tag.author_id } - end - - ############################################################################## - # Tests checking Identity Map behavior with preloaded associations, joins, # - # includes etc. # - ############################################################################## - - def test_find_with_preloaded_associations - assert_queries(2) do - posts = Post.preload(:comments).order('posts.id') - assert posts.first.comments.first - end - - # With IM we'll retrieve post object from previous query, it'll have comments - # already preloaded from first call - assert_queries(1) do - posts = Post.preload(:comments).order('posts.id') - assert posts.first.comments.first - end - - assert_queries(2) do - posts = Post.preload(:author).order('posts.id') - assert posts.first.author - end - - # With IM we'll retrieve post object from previous query, it'll have comments - # already preloaded from first call - assert_queries(1) do - posts = Post.preload(:author).order('posts.id') - assert posts.first.author - end - - assert_queries(1) do - posts = Post.preload(:author, :comments).order('posts.id') - assert posts.first.author - assert posts.first.comments.first - end - end - - def test_find_with_included_associations - assert_queries(2) do - posts = Post.includes(:comments).order('posts.id') - assert posts.first.comments.first - end - - assert_queries(1) do - posts = Post.scoped.includes(:comments).order('posts.id') - assert posts.first.comments.first - end - - assert_queries(2) do - posts = Post.includes(:author).order('posts.id') - assert posts.first.author - end - - assert_queries(1) do - posts = Post.includes(:author, :comments).order('posts.id') - assert posts.first.author - assert posts.first.comments.first - end - end - - def test_eager_loading_with_conditions_on_joined_table_preloads - posts = Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} - assert_same posts.first.author, Author.first - - posts = Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => [:comments], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} - assert_same posts.first.author, Author.first - - posts = Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'", :order => 'posts.id') - assert_equal posts(:welcome, :thinking), posts - assert_same posts.first.author, Author.first - - posts = Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2", :order => 'posts.id') - assert_equal posts(:welcome, :thinking), posts - assert_same posts.first.author, Author.first - end - - def test_eager_loading_with_conditions_on_string_joined_table_preloads - posts = assert_queries(2) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => "INNER JOIN comments on comments.post_id = posts.id", :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') - end - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} - - posts = assert_queries(1) do - Post.find(:all, :select => 'distinct posts.*', :include => :author, :joins => ["INNER JOIN comments on comments.post_id = posts.id"], :conditions => "comments.body like 'Thank you%'", :order => 'posts.id') - end - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} - end - - ############################################################################## - # Behaviour related to saving failures - ############################################################################## - - def test_reload_object_if_save_failed - developer = Developer.first - developer.salary = 0 - - assert !developer.save - - same_developer = Developer.first - - assert_not_same developer, same_developer - assert_not_equal 0, same_developer.salary - assert_not_equal developer.salary, same_developer.salary - end - - def test_reload_object_if_forced_save_failed - developer = Developer.first - developer.salary = 0 - - assert_raise(ActiveRecord::RecordInvalid) { developer.save! } - - same_developer = Developer.first - - assert_not_same developer, same_developer - assert_not_equal 0, same_developer.salary - assert_not_equal developer.salary, same_developer.salary - end - - def test_reload_object_if_update_attributes_fails - developer = Developer.first - developer.salary = 0 - - assert !developer.update_attributes(:salary => 0) - - same_developer = Developer.first - - assert_not_same developer, same_developer - assert_not_equal 0, same_developer.salary - assert_not_equal developer.salary, same_developer.salary - end - - ############################################################################## - # Behaviour of readonly, frozen, destroyed - ############################################################################## - - def test_find_using_identity_map_respects_readonly_when_loading_associated_object_first - author = Author.first - readonly_comment = author.readonly_comments.first - - comment = Comment.first - assert !comment.readonly? - - assert readonly_comment.readonly? - - assert_raise(ActiveRecord::ReadOnlyRecord) {readonly_comment.save} - assert comment.save - end - - def test_find_using_identity_map_respects_readonly - comment = Comment.first - assert !comment.readonly? - - author = Author.first - readonly_comment = author.readonly_comments.first - - assert readonly_comment.readonly? - - assert_raise(ActiveRecord::ReadOnlyRecord) {readonly_comment.save} - assert comment.save - end - - def test_find_using_select_and_identity_map - author_id, author = Author.select('id').first, Author.first - - assert_equal author_id, author - assert_same author_id, author - assert_not_nil author.name - - post, post_id = Post.first, Post.select('id').first - - assert_equal post_id, post - assert_same post_id, post - assert_not_nil post.title - end - -# Currently AR is not allowing changing primary key (see Persistence#update) -# So we ignore it. If this changes, this test needs to be uncommented. -# def test_updating_of_pkey -# assert client = Client.find(3), -# client.update_attribute(:id, 666) -# -# assert Client.find(666) -# assert_same(client, Client.find(666)) -# -# s = Subscriber.find_by_nick('swistak') -# assert s.update_attribute(:nick, 'swistakTheJester') -# assert_equal('swistakTheJester', s.nick) -# -# assert stj = Subscriber.find_by_nick('swistakTheJester') -# assert_same(s, stj) -# end - -end -end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 02df464469..06de51f5cd 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -15,7 +15,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_class_with_blank_sti_name - company = Company.find(:first) + company = Company.first company = company.dup company.extend(Module.new { def read_attribute(name) @@ -24,7 +24,7 @@ class InheritanceTest < ActiveRecord::TestCase end }) company.save! - company = Company.find(:all).find { |x| x.id == company.id } + company = Company.all.find { |x| x.id == company.id } assert_equal ' ', company.type end @@ -98,7 +98,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_inheritance_find_all - companies = Company.find(:all, :order => 'id') + companies = Company.scoped(:order => 'id').all assert_kind_of Firm, companies[0], "37signals should be a firm" assert_kind_of Client, companies[1], "Summit should be a client" end @@ -149,9 +149,9 @@ class InheritanceTest < ActiveRecord::TestCase def test_update_all_within_inheritance Client.update_all "name = 'I am a client'" - assert_equal "I am a client", Client.find(:all).first.name + assert_equal "I am a client", Client.all.first.name # Order by added as otherwise Oracle tests were failing because of different order of results - assert_equal "37signals", Firm.find(:all, :order => "id").first.name + assert_equal "37signals", Firm.scoped(:order => "id").all.first.name end def test_alt_update_all_within_inheritance @@ -173,9 +173,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_find_first_within_inheritance - assert_kind_of Firm, Company.find(:first, :conditions => "name = '37signals'") - assert_kind_of Firm, Firm.find(:first, :conditions => "name = '37signals'") - assert_nil Client.find(:first, :conditions => "name = '37signals'") + assert_kind_of Firm, Company.scoped(:where => "name = '37signals'").first + assert_kind_of Firm, Firm.scoped(:where => "name = '37signals'").first + assert_nil Client.scoped(:where => "name = '37signals'").first end def test_alt_find_first_within_inheritance @@ -187,10 +187,10 @@ class InheritanceTest < ActiveRecord::TestCase def test_complex_inheritance very_special_client = VerySpecialClient.create("name" => "veryspecial") assert_equal very_special_client, VerySpecialClient.where("name = 'veryspecial'").first - assert_equal very_special_client, SpecialClient.find(:first, :conditions => "name = 'veryspecial'") - assert_equal very_special_client, Company.find(:first, :conditions => "name = 'veryspecial'") - assert_equal very_special_client, Client.find(:first, :conditions => "name = 'veryspecial'") - assert_equal 1, Client.find(:all, :conditions => "name = 'Summit'").size + assert_equal very_special_client, SpecialClient.scoped(:where => "name = 'veryspecial'").first + assert_equal very_special_client, Company.scoped(:where => "name = 'veryspecial'").first + assert_equal very_special_client, Client.scoped(:where => "name = 'veryspecial'").first + assert_equal 1, Client.scoped(:where => "name = 'Summit'").all.size assert_equal very_special_client, Client.find(very_special_client.id) end @@ -201,14 +201,14 @@ class InheritanceTest < ActiveRecord::TestCase end def test_eager_load_belongs_to_something_inherited - account = Account.find(1, :include => :firm) + account = Account.scoped(:includes => :firm).find(1) assert account.association_cache.key?(:firm), "nil proves eager load failed" end def test_eager_load_belongs_to_primary_key_quoting con = Account.connection assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do - Account.find(1, :include => :firm) + Account.scoped(:includes => :firm).find(1) end end @@ -230,7 +230,7 @@ class InheritanceTest < ActiveRecord::TestCase private def switch_to_alt_inheritance_column # we don't want misleading test results, so get rid of the values in the type column - Company.find(:all, :order => 'id').each do |c| + Company.scoped(:order => 'id').all.each do |c| c['type'] = nil c.save end @@ -259,7 +259,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase def test_instantiation_doesnt_try_to_require_corresponding_file ActiveRecord::Base.store_full_sti_class = false - foo = Firm.find(:first).clone + foo = Firm.first.clone foo.ruby_type = foo.type = 'FirmOnTheFly' foo.save! diff --git a/activerecord/test/cases/invalid_date_test.rb b/activerecord/test/cases/invalid_date_test.rb index 98cda010ae..426a350379 100644 --- a/activerecord/test/cases/invalid_date_test.rb +++ b/activerecord/test/cases/invalid_date_test.rb @@ -7,8 +7,6 @@ class InvalidDateTest < ActiveRecord::TestCase invalid_dates = [[2007, 11, 31], [1993, 2, 29], [2007, 2, 29]] - topic = Topic.new - valid_dates.each do |date_src| topic = Topic.new("last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s) # Oracle DATE columns are datetime columns and Oracle adapter returns Time value diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb index 75e5dfa49b..0b78f2e46b 100644 --- a/activerecord/test/cases/lifecycle_test.rb +++ b/activerecord/test/cases/lifecycle_test.rb @@ -137,7 +137,7 @@ class LifecycleTest < ActiveRecord::TestCase def test_auto_observer topic_observer = TopicaAuditor.instance assert_nil TopicaAuditor.observed_class - assert_equal [Topic], TopicaAuditor.instance.observed_classes.to_a + assert_equal [Topic], TopicaAuditor.observed_classes.to_a topic = Topic.find(1) assert_equal topic.title, topic_observer.topic.title diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 807274ca67..afb0bd6fd9 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -9,6 +9,7 @@ require 'models/string_key_object' require 'models/car' require 'models/engine' require 'models/wheel' +require 'models/treasure' class LockWithoutDefault < ActiveRecord::Base; end @@ -22,7 +23,7 @@ class ReadonlyFirstNamePerson < Person end class OptimisticLockingTest < ActiveRecord::TestCase - fixtures :people, :legacy_things, :references, :string_key_objects + fixtures :people, :legacy_things, :references, :string_key_objects, :peoples_treasures def test_non_integer_lock_existing s1 = StringKeyObject.find("record1") @@ -230,15 +231,24 @@ class OptimisticLockingTest < ActiveRecord::TestCase def test_polymorphic_destroy_with_dependencies_and_lock_version car = Car.create! - + assert_difference 'car.wheels.count' do car.wheels << Wheel.create! - end + end assert_difference 'car.wheels.count', -1 do car.destroy end assert car.destroyed? end + + def test_removing_has_and_belongs_to_many_associations_upon_destroy + p = RichPerson.create! first_name: 'Jon' + p.treasures.create! + assert !p.treasures.empty? + p.destroy + assert p.treasures.empty? + assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty? + end end class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase @@ -313,7 +323,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase def counter_test(model, expected_count) add_counter_column_to(model) - object = model.find(:first) + object = model.first assert_equal 0, object.test_count assert_equal 0, object.send(model.locking_column) yield object.id @@ -348,18 +358,7 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? def test_sane_find_with_lock assert_nothing_raised do Person.transaction do - Person.find 1, :lock => true - end - end - end - - # Test scoped lock. - def test_sane_find_with_scoped_lock - assert_nothing_raised do - Person.transaction do - Person.send(:with_scope, :find => { :lock => true }) do - Person.find 1 - end + Person.lock.find(1) end end end @@ -370,7 +369,7 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? def test_eager_find_with_lock assert_nothing_raised do Person.transaction do - Person.find 1, :include => :readers, :lock => true + Person.includes(:readers).lock.find(1) end end end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index d1f0ace184..acd2fcdad4 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -11,8 +11,6 @@ class LogSubscriberTest < ActiveRecord::TestCase def setup @old_logger = ActiveRecord::Base.logger - @using_identity_map = ActiveRecord::IdentityMap.enabled? - ActiveRecord::IdentityMap.enabled = false Developer.primary_key super ActiveRecord::LogSubscriber.attach_to(:active_record) @@ -22,7 +20,6 @@ class LogSubscriberTest < ActiveRecord::TestCase super ActiveRecord::LogSubscriber.log_subscribers.pop ActiveRecord::Base.logger = @old_logger - ActiveRecord::IdentityMap.enabled = @using_identity_map end def set_logger(logger) @@ -103,13 +100,4 @@ class LogSubscriberTest < ActiveRecord::TestCase def test_initializes_runtime Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join end - - def test_log - ActiveRecord::IdentityMap.use do - Post.find 1 - Post.find 1 - end - wait - assert_match(/From Identity Map/, @logger.logged(:debug).last) - end end diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb index 8122857f52..2f98d3c646 100644 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -247,6 +247,23 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase assert !Task.new.respond_to?("#{method}=") end end +end + + +# This class should be deleted when we removed active_record_deprecated_finders as a +# dependency. +class MassAssignmentSecurityDeprecatedFindersTest < ActiveRecord::TestCase + include MassAssignmentTestHelpers + + def setup + super + @deprecation_behavior = ActiveSupport::Deprecation.behavior + ActiveSupport::Deprecation.behavior = :silence + end + + def teardown + ActiveSupport::Deprecation.behavior = @deprecation_behavior + end def test_find_or_initialize_by_with_attr_accessible_attributes p = TightPerson.find_or_initialize_by_first_name('Josh', attributes_hash) diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb deleted file mode 100644 index ebf6e26385..0000000000 --- a/activerecord/test/cases/method_scoping_test.rb +++ /dev/null @@ -1,558 +0,0 @@ -# This file can be removed once with_exclusive_scope and with_scope are removed. -# All the tests were already ported to relation_scoping_test.rb when the new -# relation scoping API was added. - -require "cases/helper" -require 'models/post' -require 'models/author' -require 'models/developer' -require 'models/project' -require 'models/comment' - -class MethodScopingTest < ActiveRecord::TestCase - fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects - - def test_set_conditions - Developer.send(:with_scope, :find => { :conditions => 'just a test...' }) do - assert_match '(just a test...)', Developer.scoped.to_sql - end - end - - def test_scoped_find - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - assert_nothing_raised { Developer.find(1) } - end - end - - def test_scoped_find_first - Developer.send(:with_scope, :find => { :conditions => "salary = 100000" }) do - assert_equal Developer.find(10), Developer.find(:first, :order => 'name') - end - end - - def test_scoped_find_last - highest_salary = Developer.find(:first, :order => "salary DESC") - - Developer.send(:with_scope, :find => { :order => "salary" }) do - assert_equal highest_salary, Developer.last - end - end - - def test_scoped_find_last_preserves_scope - lowest_salary = Developer.find(:first, :order => "salary ASC") - highest_salary = Developer.find(:first, :order => "salary DESC") - - Developer.send(:with_scope, :find => { :order => "salary" }) do - assert_equal highest_salary, Developer.last - assert_equal lowest_salary, Developer.first - end - end - - def test_scoped_find_combines_conditions - Developer.send(:with_scope, :find => { :conditions => "salary = 9000" }) do - assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => "name = 'Jamis'") - end - end - - def test_scoped_find_sanitizes_conditions - Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do - assert_equal developers(:poor_jamis), Developer.find(:first) - end - end - - def test_scoped_find_combines_and_sanitizes_conditions - Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do - assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis']) - end - end - - def test_scoped_find_all - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - assert_equal [developers(:david)], Developer.all - end - end - - def test_scoped_find_select - Developer.send(:with_scope, :find => { :select => "id, name" }) do - developer = Developer.find(:first, :conditions => "name = 'David'") - assert_equal "David", developer.name - assert !developer.has_attribute?(:salary) - end - end - - def test_scope_select_concatenates - Developer.send(:with_scope, :find => { :select => "name" }) do - developer = Developer.find(:first, :select => 'id, salary', :conditions => "name = 'David'") - assert_equal 80000, developer.salary - assert developer.has_attribute?(:id) - assert developer.has_attribute?(:name) - assert developer.has_attribute?(:salary) - end - end - - def test_scoped_count - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - assert_equal 1, Developer.count - end - - Developer.send(:with_scope, :find => { :conditions => 'salary = 100000' }) do - assert_equal 8, Developer.count - assert_equal 1, Developer.count(:conditions => "name LIKE 'fixture_1%'") - end - end - - def test_scoped_find_include - # with the include, will retrieve only developers for the given project - scoped_developers = Developer.send(:with_scope, :find => { :include => :projects }) do - Developer.find(:all, :conditions => { 'projects.id' => 2 }) - end - assert scoped_developers.include?(developers(:david)) - assert !scoped_developers.include?(developers(:jamis)) - assert_equal 1, scoped_developers.size - end - - def test_scoped_find_joins - scoped_developers = Developer.send(:with_scope, :find => { :joins => 'JOIN developers_projects ON id = developer_id' } ) do - Developer.find(:all, :conditions => 'developers_projects.project_id = 2') - end - assert scoped_developers.include?(developers(:david)) - assert !scoped_developers.include?(developers(:jamis)) - assert_equal 1, scoped_developers.size - assert_equal developers(:david).attributes, scoped_developers.first.attributes - end - - def test_scoped_find_using_new_style_joins - scoped_developers = Developer.send(:with_scope, :find => { :joins => :projects }) do - Developer.find(:all, :conditions => 'projects.id = 2') - end - assert scoped_developers.include?(developers(:david)) - assert !scoped_developers.include?(developers(:jamis)) - assert_equal 1, scoped_developers.size - assert_equal developers(:david).attributes, scoped_developers.first.attributes - end - - def test_scoped_find_merges_old_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id ' }) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'INNER JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_merges_new_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => :comments, :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_merges_new_and_old_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_merges_string_array_style_and_string_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => ["INNER JOIN posts ON posts.author_id = authors.id"]}) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'INNER JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_merges_string_array_style_and_hash_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => :posts}) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => ['INNER JOIN comments ON posts.id = comments.post_id'], :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_merges_joins_and_eliminates_duplicate_string_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => 'INNER JOIN posts ON posts.author_id = authors.id'}) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => ["INNER JOIN posts ON posts.author_id = authors.id", "INNER JOIN comments ON posts.id = comments.post_id"], :conditions => 'comments.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_find_strips_spaces_from_string_joins_and_eliminates_duplicate_string_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => ' INNER JOIN posts ON posts.author_id = authors.id '}) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => ['INNER JOIN posts ON posts.author_id = authors.id'], :conditions => 'posts.id = 1') - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_scoped_count_include - # with the include, will retrieve only developers for the given project - Developer.send(:with_scope, :find => { :include => :projects }) do - assert_equal 1, Developer.count(:conditions => { 'projects.id' => 2 }) - end - end - - def test_scope_for_create_only_uses_equal - table = VerySpecialComment.arel_table - relation = VerySpecialComment.scoped - relation.where_values << table[:id].not_eq(1) - assert_equal({:type => "VerySpecialComment"}, relation.send(:scope_for_create)) - end - - def test_scoped_create - new_comment = nil - - VerySpecialComment.send(:with_scope, :create => { :post_id => 1 }) do - assert_equal({:post_id => 1, :type => 'VerySpecialComment' }, VerySpecialComment.scoped.send(:scope_for_create)) - new_comment = VerySpecialComment.create :body => "Wonderful world" - end - - assert Post.find(1).comments.include?(new_comment) - end - - def test_scoped_create_with_join_and_merge - Comment.where(:body => "but Who's Buying?").joins(:post).merge(Post.where(:body => 'Peace Sells...')).with_scope do - assert_equal({:body => "but Who's Buying?"}, Comment.scoped.scope_for_create) - end - end - - def test_immutable_scope - options = { :conditions => "name = 'David'" } - Developer.send(:with_scope, :find => options) do - assert_equal %w(David), Developer.all.map(&:name) - options[:conditions] = "name != 'David'" - assert_equal %w(David), Developer.all.map(&:name) - end - - scope = { :find => { :conditions => "name = 'David'" }} - Developer.send(:with_scope, scope) do - assert_equal %w(David), Developer.all.map(&:name) - scope[:find][:conditions] = "name != 'David'" - assert_equal %w(David), Developer.all.map(&:name) - end - end - - def test_scoped_with_duck_typing - scoping = Struct.new(:current_scope).new(:find => { :conditions => ["name = ?", 'David'] }) - Developer.send(:with_scope, scoping) do - assert_equal %w(David), Developer.all.map(&:name) - end - end - - def test_ensure_that_method_scoping_is_correctly_restored - begin - Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do - raise "an exception" - end - rescue - end - - assert !Developer.scoped.where_values.include?("name = 'Jamis'") - end -end - -class NestedScopingTest < ActiveRecord::TestCase - fixtures :authors, :developers, :projects, :comments, :posts - - def test_merge_options - Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do - Developer.send(:with_scope, :find => { :limit => 10 }) do - devs = Developer.scoped - assert_match '(salary = 80000)', devs.to_sql - assert_equal 10, devs.taken - end - end - end - - def test_merge_inner_scope_has_priority - Developer.send(:with_scope, :find => { :limit => 5 }) do - Developer.send(:with_scope, :find => { :limit => 10 }) do - assert_equal 10, Developer.scoped.taken - end - end - end - - def test_replace_options - Developer.send(:with_scope, :find => { :conditions => {:name => 'David'} }) do - Developer.send(:with_exclusive_scope, :find => { :conditions => {:name => 'Jamis'} }) do - assert_equal 'Jamis', Developer.scoped.send(:scope_for_create)[:name] - end - - assert_equal 'David', Developer.scoped.send(:scope_for_create)[:name] - end - end - - def test_with_exclusive_scope_with_relation - assert_raise(ArgumentError) do - Developer.all_johns - end - end - - def test_append_conditions - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do - devs = Developer.scoped - assert_match "(name = 'David') AND (salary = 80000)", devs.to_sql - assert_equal(1, Developer.count) - end - Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do - assert_equal(0, Developer.count) - end - end - end - - def test_merge_and_append_options - Developer.send(:with_scope, :find => { :conditions => 'salary = 80000', :limit => 10 }) do - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - devs = Developer.scoped - assert_match "(salary = 80000) AND (name = 'David')", devs.to_sql - assert_equal 10, devs.taken - end - end - end - - def test_nested_scoped_find - Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do - Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'David'" }) do - assert_nothing_raised { Developer.find(1) } - assert_equal('David', Developer.find(:first).name) - end - assert_equal('Jamis', Developer.find(:first).name) - end - end - - def test_nested_scoped_find_include - Developer.send(:with_scope, :find => { :include => :projects }) do - Developer.send(:with_scope, :find => { :conditions => { 'projects.id' => 2 } }) do - assert_nothing_raised { Developer.find(1) } - assert_equal('David', Developer.find(:first).name) - end - end - end - - def test_nested_scoped_find_merged_include - # :include's remain unique and don't "double up" when merging - Developer.send(:with_scope, :find => { :include => :projects, :conditions => { 'projects.id' => 2 } }) do - Developer.send(:with_scope, :find => { :include => :projects }) do - assert_equal 1, Developer.scoped.includes_values.uniq.length - assert_equal 'David', Developer.find(:first).name - end - end - - # the nested scope doesn't remove the first :include - Developer.send(:with_scope, :find => { :include => :projects, :conditions => { 'projects.id' => 2 } }) do - Developer.send(:with_scope, :find => { :include => [] }) do - assert_equal 1, Developer.scoped.includes_values.uniq.length - assert_equal('David', Developer.find(:first).name) - end - end - - # mixing array and symbol include's will merge correctly - Developer.send(:with_scope, :find => { :include => [:projects], :conditions => { 'projects.id' => 2 } }) do - Developer.send(:with_scope, :find => { :include => :projects }) do - assert_equal 1, Developer.scoped.includes_values.uniq.length - assert_equal('David', Developer.find(:first).name) - end - end - end - - def test_nested_scoped_find_replace_include - Developer.send(:with_scope, :find => { :include => :projects }) do - Developer.send(:with_exclusive_scope, :find => { :include => [] }) do - assert_equal 0, Developer.scoped.includes_values.length - end - end - end - - def test_three_level_nested_exclusive_scoped_find - Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do - assert_equal('Jamis', Developer.find(:first).name) - - Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'David'" }) do - assert_equal('David', Developer.find(:first).name) - - Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'Maiha'" }) do - assert_equal(nil, Developer.find(:first)) - end - - # ensure that scoping is restored - assert_equal('David', Developer.find(:first).name) - end - - # ensure that scoping is restored - assert_equal('Jamis', Developer.find(:first).name) - end - end - - def test_merged_scoped_find - poor_jamis = developers(:poor_jamis) - Developer.send(:with_scope, :find => { :conditions => "salary < 100000" }) do - Developer.send(:with_scope, :find => { :offset => 1, :order => 'id asc' }) do - # Oracle adapter does not generated space after asc therefore trailing space removed from regex - assert_sql(/ORDER BY\s+id asc/) do - assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc')) - end - end - end - end - - def test_merged_scoped_find_sanitizes_conditions - Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do - Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do - assert_raise(ActiveRecord::RecordNotFound) { developers(:poor_jamis) } - end - end - end - - def test_nested_scoped_find_combines_and_sanitizes_conditions - Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do - Developer.send(:with_exclusive_scope, :find => { :conditions => ['salary = ?', 9000] }) do - assert_equal developers(:poor_jamis), Developer.find(:first) - assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis']) - end - end - end - - def test_merged_scoped_find_combines_and_sanitizes_conditions - Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do - Developer.send(:with_scope, :find => { :conditions => ['salary > ?', 9000] }) do - assert_equal %w(David), Developer.all.map(&:name) - end - end - end - - def test_nested_scoped_create - comment = nil - Comment.send(:with_scope, :create => { :post_id => 1}) do - Comment.send(:with_scope, :create => { :post_id => 2}) do - assert_equal({:post_id => 2}, Comment.scoped.send(:scope_for_create)) - comment = Comment.create :body => "Hey guys, nested scopes are broken. Please fix!" - end - end - assert_equal 2, comment.post_id - end - - def test_nested_exclusive_scope_for_create - comment = nil - - Comment.send(:with_scope, :create => { :body => "Hey guys, nested scopes are broken. Please fix!" }) do - Comment.send(:with_exclusive_scope, :create => { :post_id => 1 }) do - assert_equal({:post_id => 1}, Comment.scoped.send(:scope_for_create)) - assert_blank Comment.new.body - comment = Comment.create :body => "Hey guys" - end - end - assert_equal 1, comment.post_id - assert_equal 'Hey guys', comment.body - end - - def test_merged_scoped_find_on_blank_conditions - [nil, " ", [], {}].each do |blank| - Developer.send(:with_scope, :find => {:conditions => blank}) do - Developer.send(:with_scope, :find => {:conditions => blank}) do - assert_nothing_raised { Developer.find(:first) } - end - end - end - end - - def test_merged_scoped_find_on_blank_bind_conditions - [ [""], ["",{}] ].each do |blank| - Developer.send(:with_scope, :find => {:conditions => blank}) do - Developer.send(:with_scope, :find => {:conditions => blank}) do - assert_nothing_raised { Developer.find(:first) } - end - end - end - end - - def test_immutable_nested_scope - options1 = { :conditions => "name = 'Jamis'" } - options2 = { :conditions => "name = 'David'" } - Developer.send(:with_scope, :find => options1) do - Developer.send(:with_exclusive_scope, :find => options2) do - assert_equal %w(David), Developer.all.map(&:name) - options1[:conditions] = options2[:conditions] = nil - assert_equal %w(David), Developer.all.map(&:name) - end - end - end - - def test_immutable_merged_scope - options1 = { :conditions => "name = 'Jamis'" } - options2 = { :conditions => "salary > 10000" } - Developer.send(:with_scope, :find => options1) do - Developer.send(:with_scope, :find => options2) do - assert_equal %w(Jamis), Developer.all.map(&:name) - options1[:conditions] = options2[:conditions] = nil - assert_equal %w(Jamis), Developer.all.map(&:name) - end - end - end - - def test_ensure_that_method_scoping_is_correctly_restored - Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - begin - Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do - raise "an exception" - end - rescue - end - - assert Developer.scoped.where_values.include?("name = 'David'") - assert !Developer.scoped.where_values.include?("name = 'Maiha'") - end - end - - def test_nested_scoped_find_merges_old_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id' }) do - Author.send(:with_scope, :find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do - Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1') - end - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_nested_scoped_find_merges_new_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do - Author.send(:with_scope, :find => { :joins => :comments }) do - Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1') - end - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end - - def test_nested_scoped_find_merges_new_and_old_style_joins - scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do - Author.send(:with_scope, :find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do - Author.find(:all, :select => 'DISTINCT authors.*', :joins => '', :conditions => 'comments.id = 1') - end - end - assert scoped_authors.include?(authors(:david)) - assert !scoped_authors.include?(authors(:mary)) - assert_equal 1, scoped_authors.size - assert_equal authors(:david).attributes, scoped_authors.first.attributes - end -end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index f0b1f74bd3..ab61a4dcef 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -83,7 +83,6 @@ module ActiveRecord t.column :one_int, :integer, :limit => 1 t.column :four_int, :integer, :limit => 4 t.column :eight_int, :integer, :limit => 8 - t.column :eleven_int, :integer, :limit => 11 end columns = connection.columns(:testings) @@ -94,20 +93,17 @@ module ActiveRecord one = columns.detect { |c| c.name == "one_int" } four = columns.detect { |c| c.name == "four_int" } eight = columns.detect { |c| c.name == "eight_int" } - eleven = columns.detect { |c| c.name == "eleven_int" } if current_adapter?(:PostgreSQLAdapter) assert_equal 'integer', default.sql_type assert_equal 'smallint', one.sql_type assert_equal 'integer', four.sql_type assert_equal 'bigint', eight.sql_type - assert_equal 'integer', eleven.sql_type elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) assert_match 'int(11)', default.sql_type assert_match 'tinyint', one.sql_type assert_match 'int', four.sql_type assert_match 'bigint', eight.sql_type - assert_match 'int(11)', eleven.sql_type elsif current_adapter?(:OracleAdapter) assert_equal 'NUMBER(38)', default.sql_type assert_equal 'NUMBER(1)', one.sql_type diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb new file mode 100644 index 0000000000..063209389f --- /dev/null +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -0,0 +1,221 @@ +require "cases/migration/helper" + +module ActiveRecord + class Migration + class TableTest < ActiveRecord::TestCase + class MockConnection < MiniTest::Mock + def native_database_types + { + :string => 'varchar(255)', + :integer => 'integer', + } + end + + def type_to_sql(type, limit, precision, scale) + native_database_types[type] + end + end + + def setup + @connection = MockConnection.new + end + + def teardown + assert @connection.verify + end + + def with_change_table + yield ConnectionAdapters::Table.new(:delete_me, @connection) + end + + def test_references_column_type_adds_id + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, 'customer_id', :integer, {}] + t.references :customer + end + end + + def test_remove_references_column_type_removes_id + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, 'customer_id'] + t.remove_references :customer + end + end + + def test_add_belongs_to_works_like_add_references + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, 'customer_id', :integer, {}] + t.belongs_to :customer + end + end + + def test_remove_belongs_to_works_like_remove_references + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, 'customer_id'] + t.remove_belongs_to :customer + end + end + + def test_references_column_type_with_polymorphic_adds_type + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, 'taggable_id', :integer, {}] + @connection.expect :add_column, nil, [:delete_me, 'taggable_type', :string, {}] + t.references :taggable, :polymorphic => true + end + end + + def test_remove_references_column_type_with_polymorphic_removes_type + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, 'taggable_id'] + @connection.expect :remove_column, nil, [:delete_me, 'taggable_type'] + t.remove_references :taggable, :polymorphic => true + end + end + + def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, 'taggable_id', :integer, {:null => false}] + @connection.expect :add_column, nil, [:delete_me, 'taggable_type', :string, {:null => false}] + t.references :taggable, :polymorphic => true, :null => false + end + end + + def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, 'taggable_id'] + @connection.expect :remove_column, nil, [:delete_me, 'taggable_type'] + t.remove_references :taggable, :polymorphic => true, :null => false + end + end + + def test_timestamps_creates_updated_at_and_created_at + with_change_table do |t| + @connection.expect :add_timestamps, nil, [:delete_me] + t.timestamps + end + end + + def test_remove_timestamps_creates_updated_at_and_created_at + with_change_table do |t| + @connection.expect :remove_timestamps, nil, [:delete_me] + t.remove_timestamps + end + end + + def string_column + @connection.native_database_types[:string] + end + + def integer_column + @connection.native_database_types[:integer] + end + + def test_integer_creates_integer_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, integer_column, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, integer_column, {}] + t.integer :foo, :bar + end + end + + def test_string_creates_string_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, string_column, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, string_column, {}] + t.string :foo, :bar + end + end + + def test_column_creates_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}] + t.column :bar, :integer + end + end + + def test_column_creates_column_with_options + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {:null => false}] + t.column :bar, :integer, :null => false + end + end + + def test_index_creates_index + with_change_table do |t| + @connection.expect :add_index, nil, [:delete_me, :bar, {}] + t.index :bar + end + end + + def test_index_creates_index_with_options + with_change_table do |t| + @connection.expect :add_index, nil, [:delete_me, :bar, {:unique => true}] + t.index :bar, :unique => true + end + end + + def test_index_exists + with_change_table do |t| + @connection.expect :index_exists?, nil, [:delete_me, :bar, {}] + t.index_exists?(:bar) + end + end + + def test_index_exists_with_options + with_change_table do |t| + @connection.expect :index_exists?, nil, [:delete_me, :bar, {:unique => true}] + t.index_exists?(:bar, :unique => true) + end + end + + def test_change_changes_column + with_change_table do |t| + @connection.expect :change_column, nil, [:delete_me, :bar, :string, {}] + t.change :bar, :string + end + end + + def test_change_changes_column_with_options + with_change_table do |t| + @connection.expect :change_column, nil, [:delete_me, :bar, :string, {:null => true}] + t.change :bar, :string, :null => true + end + end + + def test_change_default_changes_column + with_change_table do |t| + @connection.expect :change_column_default, nil, [:delete_me, :bar, :string] + t.change_default :bar, :string + end + end + + def test_remove_drops_single_column + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, :bar] + t.remove :bar + end + end + + def test_remove_drops_multiple_columns + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, :bar, :baz] + t.remove :bar, :baz + end + end + + def test_remove_index_removes_index_with_options + with_change_table do |t| + @connection.expect :remove_index, nil, [:delete_me, {:unique => true}] + t.remove_index :unique => true + end + end + + def test_rename_renames_column + with_change_table do |t| + @connection.expect :rename_column, nil, [:delete_me, :bar, :baz] + t.rename :bar, :baz + end + end + end + end +end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 040445ef12..18f8d82bfe 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -64,7 +64,7 @@ module ActiveRecord end # SELECT - row = TestModel.find(:first) + row = TestModel.first assert_kind_of BigDecimal, row.wealth # If this assert fails, that means the SELECT is broken! @@ -79,7 +79,7 @@ module ActiveRecord TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") # SELECT - row = TestModel.find(:first) + row = TestModel.first assert_kind_of BigDecimal, row.wealth # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! @@ -132,7 +132,7 @@ module ActiveRecord :birthday => 18.years.ago, :favorite_day => 10.days.ago, :moment_of_truth => "1782-10-10 21:40:18", :male => true - bob = TestModel.find(:first) + bob = TestModel.first assert_equal 'bob', bob.first_name assert_equal 'bobsen', bob.last_name assert_equal "I was born ....", bob.bio @@ -183,6 +183,16 @@ module ActiveRecord assert_instance_of TrueClass, bob.male? assert_kind_of BigDecimal, bob.wealth end + + def test_out_of_range_limit_should_raise + skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + + assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 } + + unless current_adapter?(:PostgreSQLAdapter) + assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff } + end + end end end end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb new file mode 100644 index 0000000000..8ab1c59724 --- /dev/null +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -0,0 +1,99 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class ReferencesIndexTest < ActiveRecord::TestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + def teardown + super + connection.drop_table :testings rescue nil + end + + def test_creates_index + connection.create_table table_name do |t| + t.references :foo, :index => true + end + + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_does_not_create_index + connection.create_table table_name do |t| + t.references :foo + end + + refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_does_not_create_index_explicit + connection.create_table table_name do |t| + t.references :foo, :index => false + end + + refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_creates_index_with_options + connection.create_table table_name do |t| + t.references :foo, :index => {:name => :index_testings_on_yo_momma} + t.references :bar, :index => {:unique => true} + end + + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_yo_momma) + assert connection.index_exists?(table_name, :bar_id, :name => :index_testings_on_bar_id, :unique => true) + end + + def test_creates_polymorphic_index + connection.create_table table_name do |t| + t.references :foo, :polymorphic => true, :index => true + end + + assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + end + + def test_creates_index_for_existing_table + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo, :index => true + end + + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_does_not_create_index_for_existing_table + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo + end + + refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_does_not_create_index_for_existing_table_explicit + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo, :index => false + end + + refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + end + + def test_creates_polymorphic_index_for_existing_table + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo, :polymorphic => true, :index => true + end + + assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + end + + end + end +end diff --git a/activerecord/test/cases/migration/rename_column_test.rb b/activerecord/test/cases/migration/rename_column_test.rb index 16e09fd80e..d1a85ee5e4 100644 --- a/activerecord/test/cases/migration/rename_column_test.rb +++ b/activerecord/test/cases/migration/rename_column_test.rb @@ -18,7 +18,7 @@ module ActiveRecord rename_column "test_models", "girlfriend", "exgirlfriend" TestModel.reset_column_information - bob = TestModel.find(:first) + bob = TestModel.first assert_equal "bobette", bob.exgirlfriend end @@ -33,7 +33,7 @@ module ActiveRecord rename_column :test_models, :first_name, :nick_name TestModel.reset_column_information assert TestModel.column_names.include?("nick_name") - assert_equal ['foo'], TestModel.find(:all).map(&:nick_name) + assert_equal ['foo'], TestModel.all.map(&:nick_name) end # FIXME: another integration test. We should decouple this from the @@ -46,7 +46,7 @@ module ActiveRecord rename_column "test_models", "first_name", "nick_name" TestModel.reset_column_information assert TestModel.column_names.include?("nick_name") - assert_equal ['foo'], TestModel.find(:all).map(&:nick_name) + assert_equal ['foo'], TestModel.all.map(&:nick_name) end def test_rename_column_preserves_default_value_not_null diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 92dc150104..5d1bad0d54 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -36,7 +36,7 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Base.connection.initialize_schema_migrations_table ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}" - %w(things awesome_things prefix_things_suffix prefix_awesome_things_suffix).each do |table| + %w(things awesome_things prefix_things_suffix p_awesome_things_s ).each do |table| Thing.connection.drop_table(table) rescue nil end Thing.reset_column_information @@ -109,7 +109,7 @@ class MigrationTest < ActiveRecord::TestCase :value_of_e => BigDecimal("2.7182818284590452353602875") ) - b = BigNumber.find(:first) + b = BigNumber.first assert_not_nil b assert_not_nil b.bank_balance @@ -153,7 +153,7 @@ class MigrationTest < ActiveRecord::TestCase end GiveMeBigNumbers.down - assert_raise(ActiveRecord::StatementInvalid) { BigNumber.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { BigNumber.first } end def test_filtering_migrations @@ -165,13 +165,13 @@ class MigrationTest < ActiveRecord::TestCase Person.reset_column_information assert Person.column_methods_hash.include?(:last_name) - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter) Person.reset_column_information assert !Person.column_methods_hash.include?(:last_name) - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } end class MockMigration < ActiveRecord::Migration @@ -278,19 +278,19 @@ class MigrationTest < ActiveRecord::TestCase def test_rename_table_with_prefix_and_suffix assert !Thing.table_exists? - ActiveRecord::Base.table_name_prefix = 'prefix_' - ActiveRecord::Base.table_name_suffix = '_suffix' + ActiveRecord::Base.table_name_prefix = 'p_' + ActiveRecord::Base.table_name_suffix = '_s' Thing.reset_table_name Thing.reset_sequence_name WeNeedThings.up assert Thing.create("content" => "hello world") - assert_equal "hello world", Thing.find(:first).content + assert_equal "hello world", Thing.first.content RenameThings.up - Thing.table_name = "prefix_awesome_things_suffix" + Thing.table_name = "p_awesome_things_s" - assert_equal "hello world", Thing.find(:first).content + assert_equal "hello world", Thing.first.content ensure ActiveRecord::Base.table_name_prefix = '' ActiveRecord::Base.table_name_suffix = '' @@ -306,10 +306,10 @@ class MigrationTest < ActiveRecord::TestCase Reminder.reset_sequence_name WeNeedReminders.up assert Reminder.create("content" => "hello world", "remind_at" => Time.now) - assert_equal "hello world", Reminder.find(:first).content + assert_equal "hello world", Reminder.first.content WeNeedReminders.down - assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) } + assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } ensure ActiveRecord::Base.table_name_prefix = '' ActiveRecord::Base.table_name_suffix = '' @@ -375,6 +375,27 @@ class MigrationTest < ActiveRecord::TestCase end end + def test_out_of_range_limit_should_raise + skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + + Person.connection.drop_table :test_limits rescue nil + assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do + Person.connection.create_table :test_integer_limits, :force => true do |t| + t.column :bigone, :integer, :limit => 10 + end + end + + unless current_adapter?(:PostgreSQLAdapter) + assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do + Person.connection.create_table :test_text_limits, :force => true do |t| + t.column :bigtext, :text, :limit => 0xfffffffff + end + end + end + + Person.connection.drop_table :test_limits rescue nil + end + protected def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz @@ -400,227 +421,6 @@ class ReservedWordsMigrationTest < ActiveRecord::TestCase end end - -class ChangeTableMigrationsTest < ActiveRecord::TestCase - def setup - @connection = Person.connection - @connection.create_table :delete_me, :force => true do |t| - end - end - - def teardown - Person.connection.drop_table :delete_me rescue nil - end - - def test_references_column_type_adds_id - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {}) - t.references :customer - end - end - - def test_remove_references_column_type_removes_id - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'customer_id') - t.remove_references :customer - end - end - - def test_add_belongs_to_works_like_add_references - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {}) - t.belongs_to :customer - end - end - - def test_remove_belongs_to_works_like_remove_references - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'customer_id') - t.remove_belongs_to :customer - end - end - - def test_references_column_type_with_polymorphic_adds_type - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {}) - @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {}) - t.references :taggable, :polymorphic => true - end - end - - def test_remove_references_column_type_with_polymorphic_removes_type - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'taggable_type') - @connection.expects(:remove_column).with(:delete_me, 'taggable_id') - t.remove_references :taggable, :polymorphic => true - end - end - - def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {:null => false}) - @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {:null => false}) - t.references :taggable, :polymorphic => true, :null => false - end - end - - def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'taggable_type') - @connection.expects(:remove_column).with(:delete_me, 'taggable_id') - t.remove_references :taggable, :polymorphic => true, :null => false - end - end - - def test_timestamps_creates_updated_at_and_created_at - with_change_table do |t| - @connection.expects(:add_timestamps).with(:delete_me) - t.timestamps - end - end - - def test_remove_timestamps_creates_updated_at_and_created_at - with_change_table do |t| - @connection.expects(:remove_timestamps).with(:delete_me) - t.remove_timestamps - end - end - - def string_column - if current_adapter?(:PostgreSQLAdapter) - "character varying(255)" - elsif current_adapter?(:OracleAdapter) - 'VARCHAR2(255)' - else - 'varchar(255)' - end - end - - def integer_column - if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) - 'int(11)' - elsif current_adapter?(:OracleAdapter) - 'NUMBER(38)' - else - 'integer' - end - end - - def test_integer_creates_integer_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :foo, integer_column, {}) - @connection.expects(:add_column).with(:delete_me, :bar, integer_column, {}) - t.integer :foo, :bar - end - end - - def test_string_creates_string_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :foo, string_column, {}) - @connection.expects(:add_column).with(:delete_me, :bar, string_column, {}) - t.string :foo, :bar - end - end - - def test_column_creates_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :bar, :integer, {}) - t.column :bar, :integer - end - end - - def test_column_creates_column_with_options - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :bar, :integer, {:null => false}) - t.column :bar, :integer, :null => false - end - end - - def test_index_creates_index - with_change_table do |t| - @connection.expects(:add_index).with(:delete_me, :bar, {}) - t.index :bar - end - end - - def test_index_creates_index_with_options - with_change_table do |t| - @connection.expects(:add_index).with(:delete_me, :bar, {:unique => true}) - t.index :bar, :unique => true - end - end - - def test_index_exists - with_change_table do |t| - @connection.expects(:index_exists?).with(:delete_me, :bar, {}) - t.index_exists?(:bar) - end - end - - def test_index_exists_with_options - with_change_table do |t| - @connection.expects(:index_exists?).with(:delete_me, :bar, {:unique => true}) - t.index_exists?(:bar, :unique => true) - end - end - - def test_change_changes_column - with_change_table do |t| - @connection.expects(:change_column).with(:delete_me, :bar, :string, {}) - t.change :bar, :string - end - end - - def test_change_changes_column_with_options - with_change_table do |t| - @connection.expects(:change_column).with(:delete_me, :bar, :string, {:null => true}) - t.change :bar, :string, :null => true - end - end - - def test_change_default_changes_column - with_change_table do |t| - @connection.expects(:change_column_default).with(:delete_me, :bar, :string) - t.change_default :bar, :string - end - end - - def test_remove_drops_single_column - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, [:bar]) - t.remove :bar - end - end - - def test_remove_drops_multiple_columns - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, [:bar, :baz]) - t.remove :bar, :baz - end - end - - def test_remove_index_removes_index_with_options - with_change_table do |t| - @connection.expects(:remove_index).with(:delete_me, {:unique => true}) - t.remove_index :unique => true - end - end - - def test_rename_renames_column - with_change_table do |t| - @connection.expects(:rename_column).with(:delete_me, :bar, :baz) - t.rename :bar, :baz - end - end - - protected - def with_change_table - Person.connection.change_table :delete_me do |t| - yield t - end - end -end - if ActiveRecord::Base.connection.supports_bulk_alter? class BulkAlterTableMigrationsTest < ActiveRecord::TestCase def setup @@ -882,8 +682,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase def test_skipping_migrations @migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps" @existing_migrations = Dir[@migrations_path + "/*.rb"] - - sources = {} + + sources = {} sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps" sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_name_collision" diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index f7a5d05582..a03c4f552e 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -27,19 +27,19 @@ class ModulesTest < ActiveRecord::TestCase end def test_module_spanning_associations - firm = MyApplication::Business::Firm.find(:first) + firm = MyApplication::Business::Firm.first assert !firm.clients.empty?, "Firm should have clients" assert_nil firm.class.table_name.match('::'), "Firm shouldn't have the module appear in its table name" end def test_module_spanning_has_and_belongs_to_many_associations - project = MyApplication::Business::Project.find(:first) + project = MyApplication::Business::Project.first project.developers << MyApplication::Business::Developer.create("name" => "John") assert_equal "John", project.developers.last.name end def test_associations_spanning_cross_modules - account = MyApplication::Billing::Account.find(:first, :order => 'id') + account = MyApplication::Billing::Account.scoped(:order => 'id').first assert_kind_of MyApplication::Business::Firm, account.firm assert_kind_of MyApplication::Billing::Firm, account.qualified_billing_firm assert_kind_of MyApplication::Billing::Firm, account.unqualified_billing_firm @@ -48,7 +48,7 @@ class ModulesTest < ActiveRecord::TestCase end def test_find_account_and_include_company - account = MyApplication::Billing::Account.find(1, :include => :firm) + account = MyApplication::Billing::Account.scoped(:includes => :firm).find(1) assert_kind_of MyApplication::Business::Firm, account.firm end @@ -72,8 +72,8 @@ class ModulesTest < ActiveRecord::TestCase clients = [] assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do - clients << MyApplication::Business::Client.find(3, :include => {:firm => :account}, :conditions => 'accounts.id IS NOT NULL', :references => :accounts) - clients << MyApplication::Business::Client.find(3, :include => {:firm => :account}) + clients << MyApplication::Business::Client.references(:accounts).scoped(:includes => {:firm => :account}, :where => 'accounts.id IS NOT NULL').find(3) + clients << MyApplication::Business::Client.scoped(:includes => {:firm => :account}).find(3) end clients.each do |client| @@ -123,7 +123,7 @@ class ModulesTest < ActiveRecord::TestCase old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = true - collection = Shop::Collection.find(:first) + collection = Shop::Collection.first assert !collection.products.empty?, "Collection should have products" assert_nothing_raised { collection.destroy } ensure @@ -134,7 +134,7 @@ class ModulesTest < ActiveRecord::TestCase old = ActiveRecord::Base.store_full_sti_class ActiveRecord::Base.store_full_sti_class = true - product = Shop::Product.find(:first) + product = Shop::Product.first assert !product.variants.empty?, "Product should have variants" assert_nothing_raised { product.destroy } ensure diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index a802cfbf31..06d6596725 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -86,17 +86,25 @@ class MultipleDbTest < ActiveRecord::TestCase end def test_arel_table_engines - assert_equal Entrant.arel_engine, Bird.arel_engine + assert_not_equal Entrant.arel_engine, Bird.arel_engine + assert_not_equal Entrant.arel_engine, Course.arel_engine end - def test_associations_should_work_when_model_has_no_connection - begin - ActiveRecord::Model.remove_connection - assert_nothing_raised ActiveRecord::ConnectionNotEstablished do - College.first.courses.first + def test_connection + assert_equal Entrant.arel_engine.connection, Bird.arel_engine.connection + assert_not_equal Entrant.arel_engine.connection, Course.arel_engine.connection + end + + unless in_memory_db? + def test_associations_should_work_when_model_has_no_connection + begin + ActiveRecord::Model.remove_connection + assert_nothing_raised ActiveRecord::ConnectionNotEstablished do + College.first.courses.first + end + ensure + ActiveRecord::Model.establish_connection 'arunit' end - ensure - ActiveRecord::Model.establish_connection 'arunit' end end end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index e17ba76437..bf825c002a 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -10,12 +10,12 @@ class NamedScopeTest < ActiveRecord::TestCase fixtures :posts, :authors, :topics, :comments, :author_addresses def test_implements_enumerable - assert !Topic.find(:all).empty? + assert !Topic.all.empty? - assert_equal Topic.find(:all), Topic.base - assert_equal Topic.find(:all), Topic.base.to_a - assert_equal Topic.find(:first), Topic.base.first - assert_equal Topic.find(:all), Topic.base.map { |i| i } + assert_equal Topic.all, Topic.base + assert_equal Topic.all, Topic.base.to_a + assert_equal Topic.first, Topic.base.first + assert_equal Topic.all, Topic.base.map { |i| i } end def test_found_items_are_cached @@ -38,10 +38,10 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_delegates_finds_and_calculations_to_the_base_class - assert !Topic.find(:all).empty? + assert !Topic.all.empty? - assert_equal Topic.find(:all), Topic.base.find(:all) - assert_equal Topic.find(:first), Topic.base.find(:first) + assert_equal Topic.all, Topic.base.all + assert_equal Topic.first, Topic.base.first assert_equal Topic.count, Topic.base.count assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count) end @@ -58,10 +58,10 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified - assert !Topic.find(:all, :conditions => {:approved => true}).empty? + assert !Topic.scoped(:where => {:approved => true}).all.empty? - assert_equal Topic.find(:all, :conditions => {:approved => true}), Topic.approved - assert_equal Topic.count(:conditions => {:approved => true}), Topic.approved.count + assert_equal Topic.scoped(:where => {:approved => true}).all, Topic.approved + assert_equal Topic.where(:approved => true).count, Topic.approved.count end def test_scopes_with_string_name_can_be_composed @@ -70,13 +70,9 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal Topic.replied.approved, Topic.replied.approved_as_string end - def test_scopes_can_be_specified_with_deep_hash_conditions - assert_equal Topic.replied.approved, Topic.replied.approved_as_hash_condition - end - def test_scopes_are_composable - assert_equal((approved = Topic.find(:all, :conditions => {:approved => true})), Topic.approved) - assert_equal((replied = Topic.find(:all, :conditions => 'replies_count > 0')), Topic.replied) + assert_equal((approved = Topic.scoped(:where => {:approved => true}).all), Topic.approved) + assert_equal((replied = Topic.scoped(:where => 'replies_count > 0').all), Topic.replied) assert !(approved == replied) assert !(approved & replied).empty? @@ -84,8 +80,8 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_procedural_scopes - topics_written_before_the_third = Topic.find(:all, :conditions => ['written_on < ?', topics(:third).written_on]) - topics_written_before_the_second = Topic.find(:all, :conditions => ['written_on < ?', topics(:second).written_on]) + topics_written_before_the_third = Topic.where('written_on < ?', topics(:third).written_on) + topics_written_before_the_second = Topic.where('written_on < ?', topics(:second).written_on) assert_not_equal topics_written_before_the_second, topics_written_before_the_third assert_equal topics_written_before_the_third, Topic.written_before(topics(:third).written_on) @@ -93,46 +89,17 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_procedural_scopes_returning_nil - all_topics = Topic.find(:all) + all_topics = Topic.all assert_equal all_topics, Topic.written_before(nil) end - def test_scopes_with_joins - address = author_addresses(:david_address) - posts_with_authors_at_address = Post.find( - :all, :joins => 'JOIN authors ON authors.id = posts.author_id', - :conditions => [ 'authors.author_address_id = ?', address.id ] - ) - assert_equal posts_with_authors_at_address, Post.with_authors_at_address(address) - end - - def test_scopes_with_joins_respects_custom_select - address = author_addresses(:david_address) - posts_with_authors_at_address_titles = Post.find(:all, - :select => 'title', - :joins => 'JOIN authors ON authors.id = posts.author_id', - :conditions => [ 'authors.author_address_id = ?', address.id ] - ) - assert_equal posts_with_authors_at_address_titles.map(&:title), Post.with_authors_at_address(address).find(:all, :select => 'title').map(&:title) - end - def test_scope_with_object objects = Topic.with_object assert_operator objects.length, :>, 0 assert objects.all?(&:approved?), 'all objects should be approved' end - def test_extensions - assert_equal 1, Topic.anonymous_extension.one - assert_equal 2, Topic.named_extension.two - end - - def test_multiple_extensions - assert_equal 2, Topic.multiple_extensions.extension_two - assert_equal 1, Topic.multiple_extensions.extension_one - end - def test_has_many_associations_have_access_to_scopes assert_not_equal Post.containing_the_letter_a, authors(:david).posts assert !Post.containing_the_letter_a.empty? @@ -163,20 +130,16 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_active_records_have_scope_named__all__ - assert !Topic.find(:all).empty? + assert !Topic.all.empty? - assert_equal Topic.find(:all), Topic.base + assert_equal Topic.all, Topic.base end def test_active_records_have_scope_named__scoped__ - assert !Topic.find(:all, scope = {:conditions => "content LIKE '%Have%'"}).empty? - - assert_equal Topic.find(:all, scope), Topic.scoped(scope) - end + scope = Topic.where("content LIKE '%Have%'") + assert !scope.empty? - def test_first_and_last_should_support_find_options - assert_equal Topic.base.first(:order => 'title'), Topic.base.find(:first, :order => 'title') - assert_equal Topic.base.last(:order => 'title'), Topic.base.find(:last, :order => 'title') + assert_equal scope, Topic.scoped(where: "content LIKE '%Have%'") end def test_first_and_last_should_allow_integers_for_limit @@ -193,15 +156,6 @@ class NamedScopeTest < ActiveRecord::TestCase end end - def test_first_and_last_find_options_should_use_query_when_results_are_loaded - topics = Topic.base - topics.reload # force load - assert_queries(2) do - topics.first(:order => 'title') - topics.last(:order => 'title') - end - end - def test_empty_should_not_load_results topics = Topic.base assert_queries(2) do @@ -264,9 +218,9 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_many_should_return_false_if_none_or_one - topics = Topic.base.scoped(:conditions => {:id => 0}) + topics = Topic.base.where(:id => 0) assert !topics.many? - topics = Topic.base.scoped(:conditions => {:id => 1}) + topics = Topic.base.where(:id => 1) assert !topics.many? end @@ -318,7 +272,7 @@ class NamedScopeTest < ActiveRecord::TestCase end def test_should_use_where_in_query_for_scope - assert_equal Developer.find_all_by_name('Jamis').to_set, Developer.find_all_by_id(Developer.jamises).to_set + assert_equal Developer.where(name: 'Jamis').to_set, Developer.where(id: Developer.jamises).to_set end def test_size_should_use_count_when_results_are_not_loaded @@ -344,7 +298,7 @@ class NamedScopeTest < ActiveRecord::TestCase def test_chaining_with_duplicate_joins join = "INNER JOIN comments ON comments.post_id = posts.id" post = Post.find(1) - assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size + assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size end def test_chaining_should_use_latest_conditions_when_creating @@ -393,25 +347,6 @@ class NamedScopeTest < ActiveRecord::TestCase end end - def test_scopes_with_reserved_names - class << Topic - def public_method; end - public :public_method - - def protected_method; end - protected :protected_method - - def private_method; end - private :private_method - end - - [:public_method, :protected_method, :private_method].each do |reserved_method| - assert Topic.respond_to?(reserved_method, true) - ActiveRecord::Base.logger.expects(:warn) - Topic.scope(reserved_method) - end - end - def test_scopes_on_relations # Topic.replied approved_topics = Topic.scoped.approved.order('id DESC') @@ -483,42 +418,24 @@ class NamedScopeTest < ActiveRecord::TestCase require "models/without_table" end end -end -class DynamicScopeMatchTest < ActiveRecord::TestCase - def test_scoped_by_no_match - assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all") - end + def test_eager_scopes_are_deprecated + klass = Class.new(ActiveRecord::Base) + klass.table_name = 'posts' - def test_scoped_by - match = ActiveRecord::DynamicScopeMatch.match("scoped_by_age_and_sex_and_location") - assert_not_nil match - assert match.scope? - assert_equal %w(age sex location), match.attribute_names - end -end - -class DynamicScopeTest < ActiveRecord::TestCase - fixtures :posts - - def setup - @test_klass = Class.new(Post) do - def self.name; "Post"; end + assert_deprecated do + klass.scope :welcome_2, klass.where(:id => posts(:welcome).id) end + assert_equal [posts(:welcome).title], klass.welcome_2.map(&:title) end - def test_dynamic_scope - assert_equal @test_klass.scoped_by_author_id(1).find(1), @test_klass.find(1) - assert_equal @test_klass.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, @test_klass.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"}) - end - - def test_dynamic_scope_should_create_methods_after_hitting_method_missing - assert_blank @test_klass.methods.grep(/scoped_by_type/) - @test_klass.scoped_by_type(nil) - assert_present @test_klass.methods.grep(/scoped_by_type/) - end + def test_eager_default_scope_relations_are_deprecated + klass = Class.new(ActiveRecord::Base) + klass.table_name = 'posts' - def test_dynamic_scope_with_less_number_of_arguments - assert_raise(ArgumentError){ @test_klass.scoped_by_author_id_and_title(1) } + assert_deprecated do + klass.send(:default_scope, klass.where(:id => posts(:welcome).id)) + end + assert_equal [posts(:welcome).title], klass.all.map(&:title) end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 2ae9cb4888..0559bbbe9a 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -172,6 +172,19 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase man.interests_attributes = [{:id => interest.id, :topic => 'gardening'}] assert_equal man.interests.first.topic, man.interests[0].topic end + + def test_allows_class_to_override_setter_and_call_super + mean_pirate_class = Class.new(Pirate) do + accepts_nested_attributes_for :parrot + def parrot_attributes=(attrs) + super(attrs.merge(:color => "blue")) + end + end + mean_pirate = mean_pirate_class.new + mean_pirate.parrot_attributes = { :name => "James" } + assert_equal "James", mean_pirate.parrot.name + assert_equal "blue", mean_pirate.parrot.color + end end class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase @@ -663,7 +676,7 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_sort_the_hash_by_the_keys_before_building_new_associated_models - attributes = ActiveSupport::OrderedHash.new + attributes = {} attributes['123726353'] = { :name => 'Grace OMalley' } attributes['2'] = { :name => 'Privateers Greed' } # 2 is lower then 123726353 @pirate.send(association_setter, attributes) @@ -673,7 +686,7 @@ module NestedAttributesOnACollectionAssociationTests def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) } - assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, ActiveSupport::OrderedHash.new) } + assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, Hash.new) } assert_raise_with_message ArgumentError, 'Hash or Array expected, got String ("foo")' do @pirate.send(association_setter, "foo") diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index adfd8e83a1..0933a4ff3d 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -13,12 +13,14 @@ require 'models/warehouse_thing' require 'models/parrot' require 'models/minivan' require 'models/person' +require 'models/pet' +require 'models/toy' require 'rexml/document' require 'active_support/core_ext/exception' class PersistencesTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys # Oracle UPDATE does not support ORDER BY unless current_adapter?(:OracleAdapter) @@ -53,7 +55,7 @@ class PersistencesTest < ActiveRecord::TestCase author = authors(:david) assert_nothing_raised do assert_equal 1, author.posts_sorted_by_id_limited.size - assert_equal 2, author.posts_sorted_by_id_limited.find(:all, :limit => 2).size + assert_equal 2, author.posts_sorted_by_id_limited.scoped(:limit => 2).all.size assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ]) assert_equal "bulk update!", posts(:welcome).body assert_not_equal "bulk update!", posts(:thinking).body @@ -76,10 +78,20 @@ class PersistencesTest < ActiveRecord::TestCase assert_equal Topic.count, Topic.delete_all end - def test_update_by_condition - Topic.update_all "content = 'bulk updated!'", ["approved = ?", true] - assert_equal "Have a nice day", Topic.find(1).content - assert_equal "bulk updated!", Topic.find(2).content + def test_delete_all_with_joins_and_where_part_is_hash + where_args = {:toys => {:name => 'Bone'}} + count = Pet.joins(:toys).where(where_args).count + + assert_equal count, 1 + assert_equal count, Pet.joins(:toys).where(where_args).delete_all + end + + def test_delete_all_with_joins_and_where_part_is_not_hash + where_args = ['toys.name = ?', 'Bone'] + count = Pet.joins(:toys).where(where_args).count + + assert_equal count, 1 + assert_equal count, Pet.joins(:toys).where(where_args).delete_all end def test_increment_attribute @@ -108,7 +120,7 @@ class PersistencesTest < ActiveRecord::TestCase def test_destroy_all conditions = "author_name = 'Mary'" - topics_by_mary = Topic.all(:conditions => conditions, :order => 'id') + topics_by_mary = Topic.scoped(:where => conditions, :order => 'id').to_a assert ! topics_by_mary.empty? assert_difference('Topic.count', -topics_by_mary.size) do @@ -119,7 +131,7 @@ class PersistencesTest < ActiveRecord::TestCase end def test_destroy_many - clients = Client.find([2, 3], :order => 'id') + clients = Client.scoped(:order => 'id').find([2, 3]) assert_difference('Client.count', -2) do destroyed = Client.destroy([2, 3]).sort_by(&:id) @@ -320,7 +332,7 @@ class PersistencesTest < ActiveRecord::TestCase end def test_update_all_with_non_standard_table_name - assert_equal 1, WarehouseThing.update_all(['value = ?', 0], ['id = ?', 1]) + assert_equal 1, WarehouseThing.where(id: 1).update_all(['value = ?', 0]) assert_equal 0, WarehouseThing.find(1).value end @@ -393,7 +405,6 @@ class PersistencesTest < ActiveRecord::TestCase def test_update_attribute_with_one_updated t = Topic.first - title = t.title t.update_attribute(:title, 'super_title') assert_equal 'super_title', t.title assert !t.changed?, "topic should not have changed" diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index fba3006ebe..0a6354f5cc 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -17,7 +17,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase end def checkout_connections - ActiveRecord::Model.establish_connection(@connection.merge({:pool => 2, :wait_timeout => 0.3})) + ActiveRecord::Model.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.3})) @connections = [] @timed_out = 0 @@ -34,7 +34,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase # Will deadlock due to lack of Monitor timeouts in 1.9 def checkout_checkin_connections(pool_size, threads) - ActiveRecord::Model.establish_connection(@connection.merge({:pool => pool_size, :wait_timeout => 0.5})) + ActiveRecord::Model.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5})) @connection_count = 0 @timed_out = 0 threads.times do diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index f36f4237b1..a712e5f689 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -96,7 +96,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_clear_after_close mw = ActiveRecord::QueryCache.new lambda { |env| - Post.find(:first) + Post.first [200, {}, nil] } body = mw.call({}).last @@ -107,7 +107,7 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_find_queries - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) { Task.find(1); Task.find(1) } + assert_queries(2) { Task.find(1); Task.find(1) } end def test_find_queries_with_cache @@ -233,8 +233,8 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_cache_is_expired_by_habtm_update ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) ActiveRecord::Base.cache do - c = Category.find(:first) - p = Post.find(:first) + c = Category.first + p = Post.first p.categories << c end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index e21109baae..df0399f548 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -23,11 +23,12 @@ class ReadOnlyTest < ActiveRecord::TestCase end assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save } assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! } + assert_raise(ActiveRecord::ReadOnlyRecord) { dev.destroy } end def test_find_with_readonly_option - Developer.find(:all).each { |d| assert !d.readonly? } + Developer.all.each { |d| assert !d.readonly? } Developer.readonly(false).each { |d| assert !d.readonly? } Developer.readonly(true).each { |d| assert d.readonly? } Developer.readonly.each { |d| assert d.readonly? } @@ -48,7 +49,7 @@ class ReadOnlyTest < ActiveRecord::TestCase post = Post.find(1) assert !post.comments.empty? assert !post.comments.any?(&:readonly?) - assert !post.comments.find(:all).any?(&:readonly?) + assert !post.comments.all.any?(&:readonly?) assert post.comments.readonly(true).all?(&:readonly?) end @@ -70,13 +71,13 @@ class ReadOnlyTest < ActiveRecord::TestCase end def test_readonly_scoping - Post.send(:with_scope, :find => { :conditions => '1=1' }) do + Post.where('1=1').scoped do assert !Post.find(1).readonly? assert Post.readonly(true).find(1).readonly? assert !Post.readonly(false).find(1).readonly? end - Post.send(:with_scope, :find => { :joins => ' ' }) do + Post.joins(' ').scoped do assert !Post.find(1).readonly? assert Post.readonly.find(1).readonly? assert !Post.readonly(false).find(1).readonly? @@ -85,14 +86,14 @@ class ReadOnlyTest < ActiveRecord::TestCase # Oracle barfs on this because the join includes unqualified and # conflicting column names unless current_adapter?(:OracleAdapter) - Post.send(:with_scope, :find => { :joins => ', developers' }) do + Post.joins(', developers').scoped do assert Post.find(1).readonly? assert Post.readonly.find(1).readonly? assert !Post.readonly(false).find(1).readonly? end end - Post.send(:with_scope, :find => { :readonly => true }) do + Post.readonly(true).scoped do assert Post.find(1).readonly? assert Post.readonly.find(1).readonly? assert !Post.readonly(false).find(1).readonly? diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index 576ab60090..e53a27d5dd 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -64,7 +64,7 @@ module ActiveRecord spec.config[:reaping_frequency] = 0.0001 pool = ConnectionPool.new spec - pool.timeout = 0 + pool.dead_connection_timeout = 0 conn = pool.checkout count = pool.connections.length diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 16f05f2198..7dd5698dcf 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -63,7 +63,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_column_null_not_null - subscriber = Subscriber.find(:first) + subscriber = Subscriber.first assert subscriber.column_for_attribute("name").null assert !subscriber.column_for_attribute("nick").null end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index edf38cb7a3..cf367242f2 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -53,8 +53,8 @@ class RelationScopingTest < ActiveRecord::TestCase end def test_scoped_find_last_preserves_scope - lowest_salary = Developer.first :order => "salary ASC" - highest_salary = Developer.first :order => "salary DESC" + lowest_salary = Developer.order("salary ASC").first + highest_salary = Developer.order("salary DESC").first Developer.order("salary").scoping do assert_equal highest_salary, Developer.last @@ -84,7 +84,7 @@ class RelationScopingTest < ActiveRecord::TestCase def test_scope_select_concatenates Developer.select("id, name").scoping do - developer = Developer.select('id, salary').where("name = 'David'").first + developer = Developer.select('salary').where("name = 'David'").first assert_equal 80000, developer.salary assert developer.has_attribute?(:id) assert developer.has_attribute?(:name) @@ -254,11 +254,6 @@ class HasManyScopingTest< ActiveRecord::TestCase assert_equal 2, @welcome.comments.search_by_type('Comment').size end - def test_forwarding_to_dynamic_finders - assert_equal 4, Comment.find_all_by_type('Comment').size - assert_equal 2, @welcome.comments.find_all_by_type('Comment').size - end - def test_nested_scope_finder Comment.where('1=0').scoping do assert_equal 0, @welcome.comments.count @@ -300,12 +295,6 @@ class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase assert_equal 'a category...', @welcome.categories.what_are_you end - def test_forwarding_to_dynamic_finders - assert_equal 4, Category.find_all_by_type('SpecialCategory').size - assert_equal 0, @welcome.categories.find_all_by_type('SpecialCategory').size - assert_equal 2, @welcome.categories.find_all_by_type('Category').size - end - def test_nested_scope_finder Category.where('1=0').scoping do assert_equal 0, @welcome.categories.count @@ -323,8 +312,8 @@ class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts def test_default_scope - expected = Developer.find(:all, :order => 'salary DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } + expected = Developer.scoped(:order => 'salary DESC').all.collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.all.collect { |dev| dev.salary } assert_equal expected, received end @@ -362,12 +351,12 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_default_scope_with_conditions_string - assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.find(:all).map(&:id).sort + assert_equal Developer.where(name: 'David').map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort assert_equal nil, DeveloperCalledDavid.create!.name end def test_default_scope_with_conditions_hash - assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.find(:all).map(&:id).sort + assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort assert_equal 'Jamis', DeveloperCalledJamis.create!.name end @@ -395,23 +384,9 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 50000, wheres[:salary] end - def test_method_scope - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } - assert_equal expected, received - end - - def test_nested_scope - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do - DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - end - assert_equal expected, received - end - def test_scope_overwrites_default - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } + expected = Developer.scoped(:order => 'salary DESC, name DESC').all.collect { |dev| dev.name } + received = DeveloperOrderedBySalary.by_name.all.collect { |dev| dev.name } assert_equal expected, received end @@ -427,17 +402,9 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end - def test_nested_exclusive_scope - expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do - DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - end - assert_equal expected, received - end - def test_order_in_default_scope_should_prevail - expected = Developer.find(:all, :order => 'salary desc').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } + expected = Developer.scoped(:order => 'salary desc').all.collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.scoped(:order => 'salary').all.collect { |dev| dev.salary } assert_equal expected, received end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index ac6dee3c6a..89f818a689 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -19,33 +19,12 @@ module ActiveRecord assert !relation.loaded, 'relation is not loaded' end - def test_single_values - assert_equal [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :uniq].map(&:to_s).sort, - Relation::SINGLE_VALUE_METHODS.map(&:to_s).sort - end - def test_initialize_single_values relation = Relation.new :a, :b - Relation::SINGLE_VALUE_METHODS.each do |method| + (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end - end - - def test_association_methods - assert_equal [:includes, :eager_load, :preload].map(&:to_s).sort, - Relation::ASSOCIATION_METHODS.map(&:to_s).sort - end - - def test_initialize_association_methods - relation = Relation.new :a, :b - Relation::ASSOCIATION_METHODS.each do |method| - assert_equal [], relation.send("#{method}_values"), method.to_s - end - end - - def test_multi_value_methods - assert_equal [:select, :group, :order, :joins, :where, :having, :bind, :references].map(&:to_s).sort, - Relation::MULTI_VALUE_METHODS.map(&:to_s).sort + assert_equal({}, relation.create_with_value) end def test_multi_value_initialize @@ -64,19 +43,19 @@ module ActiveRecord relation = Relation.new :a, :b assert_equal({}, relation.where_values_hash) - relation.where_values << :hello + relation.where! :hello assert_equal({}, relation.where_values_hash) end def test_has_values relation = Relation.new Post, Post.arel_table - relation.where_values << relation.table[:id].eq(10) + relation.where! relation.table[:id].eq(10) assert_equal({:id => 10}, relation.where_values_hash) end def test_values_wrong_table relation = Relation.new Post, Post.arel_table - relation.where_values << Comment.arel_table[:id].eq(10) + relation.where! Comment.arel_table[:id].eq(10) assert_equal({}, relation.where_values_hash) end @@ -85,7 +64,7 @@ module ActiveRecord left = relation.table[:id].eq(10) right = relation.table[:id].eq(10) combine = left.and right - relation.where_values << combine + relation.where! combine assert_equal({}, relation.where_values_hash) end @@ -108,7 +87,7 @@ module ActiveRecord def test_create_with_value_with_wheres relation = Relation.new Post, Post.arel_table - relation.where_values << relation.table[:id].eq(10) + relation.where! relation.table[:id].eq(10) relation.create_with_value = {:hello => 'world'} assert_equal({:hello => 'world', :id => 10}, relation.scope_for_create) end @@ -118,7 +97,7 @@ module ActiveRecord relation = Relation.new Post, Post.arel_table assert_equal({}, relation.scope_for_create) - relation.where_values << relation.table[:id].eq(10) + relation.where! relation.table[:id].eq(10) assert_equal({}, relation.scope_for_create) relation.create_with_value = {:hello => 'world'} @@ -132,7 +111,7 @@ module ActiveRecord def test_eager_load_values relation = Relation.new :a, :b - relation.eager_load_values << :b + relation.eager_load! :b assert relation.eager_loading? end @@ -149,10 +128,121 @@ module ActiveRecord assert_equal ['foo'], relation.references_values end - def test_apply_finder_options_takes_references + test 'merging a hash into a relation' do relation = Relation.new :a, :b - relation = relation.apply_finder_options(:references => :foo) - assert_equal ['foo'], relation.references_values + relation = relation.merge where: :lol, readonly: true + + assert_equal [:lol], relation.where_values + assert_equal true, relation.readonly_value + end + + test 'merging an empty hash into a relation' do + assert_equal [], Relation.new(:a, :b).merge({}).where_values + end + + test 'merging a hash with unknown keys raises' do + assert_raises(ArgumentError) { Relation::HashMerger.new(nil, omg: 'lol') } + end + + test '#values returns a dup of the values' do + relation = Relation.new(:a, :b).where! :foo + values = relation.values + + values[:where] = nil + assert_not_nil relation.where_values + end + + test 'relations can be created with a values hash' do + relation = Relation.new(:a, :b, where: [:foo]) + assert_equal [:foo], relation.where_values + end + + test 'merging a single where value' do + relation = Relation.new(:a, :b) + relation.merge!(where: :foo) + assert_equal [:foo], relation.where_values + end + + test 'merging a hash interpolates conditions' do + klass = stub + klass.stubs(:sanitize_sql).with(['foo = ?', 'bar']).returns('foo = bar') + + relation = Relation.new(klass, :b) + relation.merge!(where: ['foo = ?', 'bar']) + assert_equal ['foo = bar'], relation.where_values + end + end + + class RelationMutationTest < ActiveSupport::TestCase + def relation + @relation ||= Relation.new :a, :b + end + + (Relation::MULTI_VALUE_METHODS - [:references, :extending]).each do |method| + test "##{method}!" do + assert relation.public_send("#{method}!", :foo).equal?(relation) + assert_equal [:foo], relation.public_send("#{method}_values") + end + end + + test '#references!' do + assert relation.references!(:foo).equal?(relation) + assert relation.references_values.include?('foo') + end + + test 'extending!' do + mod = Module.new + + assert relation.extending!(mod).equal?(relation) + assert [mod], relation.extending_values + assert relation.is_a?(mod) + end + + test 'extending! with empty args' do + relation.extending! + assert_equal [], relation.extending_values + end + + (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method| + test "##{method}!" do + assert relation.public_send("#{method}!", :foo).equal?(relation) + assert_equal :foo, relation.public_send("#{method}_value") + end + end + + test '#from!' do + assert relation.from!('foo').equal?(relation) + assert_equal ['foo', nil], relation.from_value + end + + test '#lock!' do + assert relation.lock!('foo').equal?(relation) + assert_equal 'foo', relation.lock_value + end + + test '#reorder!' do + relation = self.relation.order('foo') + + assert relation.reorder!('bar').equal?(relation) + assert_equal ['bar'], relation.order_values + assert relation.reordering_value + end + + test 'reverse_order!' do + assert relation.reverse_order!.equal?(relation) + assert relation.reverse_order_value + relation.reverse_order! + assert !relation.reverse_order_value + end + + test 'create_with!' do + assert relation.create_with!(foo: 'bar').equal?(relation) + assert_equal({foo: 'bar'}, relation.create_with_value) + end + + test 'merge!' do + assert relation.merge!(where: :foo).equal?(relation) + assert_equal [:foo], relation.where_values end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 7b1d65c6db..2dc8f0053b 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -133,6 +133,13 @@ class RelationTest < ActiveRecord::TestCase assert topics.loaded? end + def test_finding_with_subquery + relation = Topic.where(:approved => true) + assert_equal relation.to_a, Topic.select('*').from(relation).to_a + assert_equal relation.to_a, Topic.select('subquery.*').from(relation).to_a + assert_equal relation.to_a, Topic.select('a.*').from(relation, :a).to_a + end + def test_finding_with_conditions assert_equal ["David"], Author.where(:name => 'David').map(&:name) assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name) @@ -219,6 +226,7 @@ class RelationTest < ActiveRecord::TestCase assert_no_queries do assert_equal [], Developer.none assert_equal [], Developer.scoped.none + assert Developer.none.is_a?(ActiveRecord::NullRelation) end end @@ -228,6 +236,38 @@ class RelationTest < ActiveRecord::TestCase end end + def test_none_chained_to_methods_firing_queries_straight_to_db + assert_no_queries do + assert_equal [], Developer.none.pluck(:id) # => uses select_all + assert_equal 0, Developer.none.delete_all + assert_equal 0, Developer.none.update_all(:name => 'David') + assert_equal 0, Developer.none.delete(1) + assert_equal false, Developer.none.exists?(1) + end + end + + def test_null_relation_content_size_methods + assert_no_queries do + assert_equal 0, Developer.none.size + assert_equal 0, Developer.none.count + assert_equal true, Developer.none.empty? + assert_equal false, Developer.none.any? + assert_equal false, Developer.none.many? + end + end + + def test_null_relation_calculations_methods + assert_no_queries do + assert_equal 0, Developer.none.count + assert_equal nil, Developer.none.calculate(:average, 'salary') + end + end + + def test_null_relation_metadata_methods + assert_equal "", Developer.none.to_sql + assert_equal({}, Developer.none.where_values_hash) + end + def test_joins_with_nil_argument assert_nothing_raised { DependentFirm.joins(nil).first } end @@ -249,7 +289,7 @@ class RelationTest < ActiveRecord::TestCase end def test_find_on_hash_conditions - assert_equal Topic.find(:all, :conditions => {:approved => false}), Topic.where({ :approved => false }).to_a + assert_equal Topic.scoped(:where => {:approved => false}).all, Topic.where({ :approved => false }).to_a end def test_joins_with_string_array @@ -297,7 +337,6 @@ class RelationTest < ActiveRecord::TestCase end def test_respond_to_class_methods_and_scopes - assert DeveloperOrderedBySalary.scoped.respond_to?(:all_ordered_by_name) assert Topic.scoped.respond_to?(:by_lifo) end @@ -323,7 +362,7 @@ class RelationTest < ActiveRecord::TestCase assert posts.first.comments.first end - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do + assert_queries(2) do posts = Post.preload(:comments).order('posts.id') assert posts.first.comments.first end @@ -333,12 +372,12 @@ class RelationTest < ActiveRecord::TestCase assert posts.first.author end - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do + assert_queries(2) do posts = Post.preload(:author).order('posts.id') assert posts.first.author end - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 3) do + assert_queries(3) do posts = Post.preload(:author, :comments).order('posts.id') assert posts.first.author assert posts.first.comments.first @@ -351,7 +390,7 @@ class RelationTest < ActiveRecord::TestCase assert posts.first.comments.first end - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) do + assert_queries(2) do posts = Post.scoped.includes(:comments).order('posts.id') assert posts.first.comments.first end @@ -361,7 +400,7 @@ class RelationTest < ActiveRecord::TestCase assert posts.first.author end - assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 3) do + assert_queries(3) do posts = Post.includes(:author, :comments).order('posts.id') assert posts.first.author assert posts.first.comments.first @@ -369,18 +408,18 @@ class RelationTest < ActiveRecord::TestCase end def test_default_scope_with_conditions_string - assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.scoped.map(&:id).sort + assert_equal Developer.where(name: 'David').map(&:id).sort, DeveloperCalledDavid.scoped.map(&:id).sort assert_nil DeveloperCalledDavid.create!.name end def test_default_scope_with_conditions_hash - assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.scoped.map(&:id).sort + assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.scoped.map(&:id).sort assert_equal 'Jamis', DeveloperCalledJamis.create!.name end def test_default_scoping_finder_methods developers = DeveloperCalledDavid.order('id').map(&:id).sort - assert_equal Developer.find_all_by_name('David').map(&:id).sort, developers + assert_equal Developer.where(name: 'David').map(&:id).sort, developers end def test_loading_with_one_association @@ -404,15 +443,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.find(1).last_comment, post.last_comment end - def test_dynamic_find_by_attributes_should_yield_found_object - david = authors(:david) - yielded_value = nil - Author.find_by_name(david.name) do |author| - yielded_value = author - end - assert_equal david, yielded_value - end - def test_dynamic_find_by_attributes david = authors(:david) author = Author.preload(:taggings).find_by_id(david.id) @@ -434,34 +464,6 @@ class RelationTest < ActiveRecord::TestCase assert_raises(ActiveRecord::RecordNotFound) { Author.scoped.find_by_id_and_name!(20, 'invalid') } end - def test_dynamic_find_all_by_attributes - authors = Author.scoped - - davids = authors.find_all_by_name('David') - assert_kind_of Array, davids - assert_equal [authors(:david)], davids - end - - def test_dynamic_find_or_initialize_by_attributes - authors = Author.scoped - - lifo = authors.find_or_initialize_by_name('Lifo') - assert_equal "Lifo", lifo.name - assert !lifo.persisted? - - assert_equal authors(:david), authors.find_or_initialize_by_name(:name => 'David') - end - - def test_dynamic_find_or_create_by_attributes - authors = Author.scoped - - lifo = authors.find_or_create_by_name('Lifo') - assert_equal "Lifo", lifo.name - assert lifo.persisted? - - assert_equal authors(:david), authors.find_or_create_by_name(:name => 'David') - end - def test_find_id authors = Author.scoped @@ -600,6 +602,7 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.exists?(authors(:mary).id) assert ! davids.exists?("42") assert ! davids.exists?(42) + assert ! davids.exists?(davids.new) fake = Author.where(:name => 'fake author') assert ! fake.exists? @@ -644,6 +647,10 @@ class RelationTest < ActiveRecord::TestCase assert davids.loaded? end + def test_delete_all_limit_error + assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all } + end + def test_select_argument_error assert_raises(ArgumentError) { Developer.select } end @@ -673,10 +680,8 @@ class RelationTest < ActiveRecord::TestCase end def test_relation_merging_with_preload - ActiveRecord::IdentityMap.without do - [Post.scoped.merge(Post.preload(:author)), Post.preload(:author).merge(Post.scoped)].each do |posts| - assert_queries(2) { assert posts.first.author } - end + [Post.scoped.merge(Post.preload(:author)), Post.preload(:author).merge(Post.scoped)].each do |posts| + assert_queries(2) { assert posts.first.author } end end @@ -1017,10 +1022,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.all, all_posts.all end - def test_extensions_with_except - assert_equal 2, Topic.named_extension.order(:author_name).except(:order).two - end - def test_only relation = Post.where(:author_id => 1).order('id ASC').limit(1) assert_equal [posts(:welcome)], relation.all @@ -1032,10 +1033,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.limit(1).all.first, all_posts.first end - def test_extensions_with_only - assert_equal 2, Topic.named_extension.order(:author_name).only(:order).two - end - def test_anonymous_extension relation = Post.where(:author_id => 1).order('id ASC').extending do def author @@ -1057,36 +1054,26 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.order(Post.arel_table[:title]).all, Post.order("title").all end - def test_order_with_find_with_order - assert_equal 'zyke', CoolCar.order('name desc').find(:first, :order => 'id').name - assert_equal 'zyke', FastCar.order('name desc').find(:first, :order => 'id').name - end - def test_default_scope_order_with_scope_order assert_equal 'zyke', CoolCar.order_using_new_style.limit(1).first.name - assert_equal 'zyke', CoolCar.order_using_old_style.limit(1).first.name assert_equal 'zyke', FastCar.order_using_new_style.limit(1).first.name - assert_equal 'zyke', FastCar.order_using_old_style.limit(1).first.name end def test_order_using_scoping car1 = CoolCar.order('id DESC').scoping do - CoolCar.find(:first, :order => 'id asc') + CoolCar.scoped(:order => 'id asc').first end assert_equal 'zyke', car1.name car2 = FastCar.order('id DESC').scoping do - FastCar.find(:first, :order => 'id asc') + FastCar.scoped(:order => 'id asc').first end assert_equal 'zyke', car2.name end def test_unscoped_block_style assert_equal 'honda', CoolCar.unscoped { CoolCar.order_using_new_style.limit(1).first.name} - assert_equal 'honda', CoolCar.unscoped { CoolCar.order_using_old_style.limit(1).first.name} - assert_equal 'honda', FastCar.unscoped { FastCar.order_using_new_style.limit(1).first.name} - assert_equal 'honda', FastCar.unscoped { FastCar.order_using_old_style.limit(1).first.name} end def test_intersection_with_array @@ -1097,10 +1084,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal [rails_author], relation & [rails_author] end - def test_removing_limit_with_options - assert_not_equal 1, Post.limit(1).all(:limit => nil).count - end - def test_primary_key assert_equal "id", Post.scoped.primary_key end @@ -1224,4 +1207,68 @@ class RelationTest < ActiveRecord::TestCase scope = Post.order('foo(comments.body)') assert_equal [], scope.references_values end + + def test_presence + topics = Topic.scoped + + # the first query is triggered because there are no topics yet. + assert_queries(1) { assert topics.present? } + + # checking if there are topics is used before you actually display them, + # thus it shouldn't invoke an extra count query. + assert_no_queries { assert topics.present? } + assert_no_queries { assert !topics.blank? } + + # shows count of topics and loops after loading the query should not trigger extra queries either. + assert_no_queries { topics.size } + assert_no_queries { topics.length } + assert_no_queries { topics.each } + + # count always trigger the COUNT query. + assert_queries(1) { topics.count } + + assert topics.loaded? + end + + test "find_by with hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by(author_id: 2) + end + + test "find_by with non-hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by("author_id = 2") + end + + test "find_by with multi-arg conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by('author_id = ?', 2) + end + + test "find_by returns nil if the record is missing" do + assert_equal nil, Post.scoped.find_by("1 = 0") + end + + test "find_by doesn't have implicit ordering" do + assert_sql(/^((?!ORDER).)*$/) { Post.find_by(author_id: 2) } + end + + test "find_by! with hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by!(author_id: 2) + end + + test "find_by! with non-hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by!("author_id = 2") + end + + test "find_by! with multi-arg conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.order(:id).find_by!('author_id = ?', 2) + end + + test "find_by! doesn't have implicit ordering" do + assert_sql(/^((?!ORDER).)*$/) { Post.find_by!(author_id: 2) } + end + + test "find_by! raises RecordNotFound if the record is missing" do + assert_raises(ActiveRecord::RecordNotFound) do + Post.scoped.find_by!("1 = 0") + end + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 3314013cd4..ab80dd1d6d 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -236,10 +236,31 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dump_includes_inet_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_network_address"} =~ output + assert_match %r{t.inet "inet_address"}, output + end + end + + def test_schema_dump_includes_cidr_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_network_address"} =~ output + assert_match %r{t.cidr "cidr_address"}, output + end + end + + def test_schema_dump_includes_macaddr_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_network_address"} =~ output + assert_match %r{t.macaddr "macaddr_address"}, output + end + end + def test_schema_dump_includes_hstores_shorthand_definition output = standard_dump if %r{create_table "postgresql_hstores"} =~ output - assert_match %r{t.hstore "hash_store", default => ""}, output + assert_match %r[t.hstore "hash_store", :default => {}], output end end diff --git a/activerecord/test/cases/session_store/sql_bypass.rb b/activerecord/test/cases/session_store/sql_bypass_test.rb index 7402b2afd6..6749d4ce98 100644 --- a/activerecord/test/cases/session_store/sql_bypass.rb +++ b/activerecord/test/cases/session_store/sql_bypass_test.rb @@ -18,6 +18,11 @@ module ActiveRecord assert !Session.table_exists? end + def test_new_record? + s = SqlBypass.new :data => 'foo', :session_id => 10 + assert s.new_record?, 'this is a new record!' + end + def test_persisted? s = SqlBypass.new :data => 'foo', :session_id => 10 assert !s.persisted?, 'this is a new record!' diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 40520d6da2..3a5d84df9f 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -4,7 +4,7 @@ require 'models/admin/user' class StoreTest < ActiveRecord::TestCase setup do - @john = Admin::User.create(:name => 'John Doe', :color => 'black', :remember_login => true) + @john = Admin::User.create(:name => 'John Doe', :color => 'black', :remember_login => true, :height => 'tall', :is_a_good_guy => true) end test "reading store attributes through accessors" do @@ -40,4 +40,72 @@ class StoreTest < ActiveRecord::TestCase @john.remember_login = false assert_equal false, @john.remember_login end + + test "preserve store attributes data in HashWithIndifferentAccess format without any conversion" do + @john.json_data = HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy') + @john.height = 'low' + assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess) + assert_equal 'low', @john.json_data[:height] + assert_equal 'low', @john.json_data['height'] + assert_equal 'heavy', @john.json_data[:weight] + assert_equal 'heavy', @john.json_data['weight'] + end + + test "convert store attributes from Hash to HashWithIndifferentAccess saving the data and access attributes indifferently" do + @john.json_data = { :height => 'tall', 'weight' => 'heavy' } + assert_equal true, @john.json_data.instance_of?(Hash) + assert_equal 'tall', @john.json_data[:height] + assert_equal nil, @john.json_data['height'] + assert_equal nil, @john.json_data[:weight] + assert_equal 'heavy', @john.json_data['weight'] + @john.height = 'low' + assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess) + assert_equal 'low', @john.json_data[:height] + assert_equal 'low', @john.json_data['height'] + assert_equal 'heavy', @john.json_data[:weight] + assert_equal 'heavy', @john.json_data['weight'] + end + + test "convert store attributes from any format other than Hash or HashWithIndifferent access losing the data" do + @john.json_data = "somedata" + @john.height = 'low' + assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess) + assert_equal 'low', @john.json_data[:height] + assert_equal 'low', @john.json_data['height'] + assert_equal false, @john.json_data.delete_if { |k, v| k == 'height' }.any? + end + + test "reading store attributes through accessors encoded with JSON" do + assert_equal 'tall', @john.height + assert_nil @john.weight + end + + test "writing store attributes through accessors encoded with JSON" do + @john.height = 'short' + @john.weight = 'heavy' + + assert_equal 'short', @john.height + assert_equal 'heavy', @john.weight + end + + test "accessing attributes not exposed by accessors encoded with JSON" do + @john.json_data['somestuff'] = 'somecoolstuff' + @john.save + + assert_equal 'somecoolstuff', @john.reload.json_data['somestuff'] + end + + test "updating the store will mark it as changed encoded with JSON" do + @john.height = 'short' + assert @john.json_data_changed? + end + + test "object initialization with not nullable column encoded with JSON" do + assert_equal true, @john.is_a_good_guy + end + + test "writing with not nullable column encoded with JSON" do + @john.is_a_good_guy = false + assert_equal false, @john.is_a_good_guy + end end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index f8b3e01a49..961ba8d9ba 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -287,6 +287,74 @@ class TransactionObserverCallbacksTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end + assert topic.id.nil? + assert !topic.persisted? assert_equal %w{ after_rollback }, topic.history end + + class TopicWithManualRollbackObserverAttached < ActiveRecord::Base + self.table_name = :topics + def history + @history ||= [] + end + end + + class TopicWithManualRollbackObserverAttachedObserver < ActiveRecord::Observer + def after_save(record) + record.history.push "after_save" + raise ActiveRecord::Rollback + end + end + + def test_after_save_called_with_manual_rollback + assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer' + + topic = TopicWithManualRollbackObserverAttached.new + + assert !topic.save + assert_equal nil, topic.id + assert !topic.persisted? + assert_equal %w{ after_save }, topic.history + end + def test_after_save_called_with_manual_rollback_bang + assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer' + + topic = TopicWithManualRollbackObserverAttached.new + + topic.save! + assert_equal nil, topic.id + assert !topic.persisted? + assert_equal %w{ after_save }, topic.history + end +end + +class SaveFromAfterCommitBlockTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class TopicWithSaveInCallback < ActiveRecord::Base + self.table_name = :topics + after_commit :cache_topic, :on => :create + after_commit :call_update, :on => :update + attr_accessor :cached, :record_updated + + def call_update + self.record_updated = true + end + + def cache_topic + unless cached + self.cached = true + self.save + else + self.cached = false + end + end + end + + def test_after_commit_in_save + topic = TopicWithSaveInCallback.new() + topic.save + assert_equal true, topic.cached + assert_equal true, topic.record_updated + end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 203dd054f1..a9ccd00fac 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -362,6 +362,16 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_rollback_when_saving_a_frozen_record + topic = Topic.new(:title => 'test') + topic.freeze + e = assert_raise(RuntimeError) { topic.save } + assert_equal "can't modify frozen Hash", e.message + assert !topic.persisted?, 'not persisted' + assert_nil topic.id + assert topic.frozen?, 'not frozen' + end + def test_restore_active_record_state_for_all_records_in_a_transaction topic_1 = Topic.new(:title => 'test_1') topic_2 = Topic.new(:title => 'test_2') diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index c72b7b35cd..7ac34bc71e 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -86,7 +86,7 @@ class AssociationValidationTest < ActiveRecord::TestCase assert !r.valid? assert r.errors[:topic].any? - r.topic = Topic.find :first + r.topic = Topic.first assert r.valid? end diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index 628029f8df..a8e513d81f 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -8,6 +8,16 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase I18n.backend = I18n::Backend::Simple.new end + def reset_i18n_load_path + @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new + yield + ensure + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend + end + # validates_associated: generate_message(attr_name, :invalid, :message => custom_message, :value => value) def test_generate_message_invalid_with_default_message assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :value => 'title') @@ -35,4 +45,13 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase assert_equal "Validation failed: Title is invalid, Title can't be blank", ActiveRecord::RecordInvalid.new(topic).message end + test "RecordInvalid exception translation falls back to the :errors namespace" do + reset_i18n_load_path do + I18n.backend.store_translations 'en', :errors => {:messages => {:record_invalid => 'fallback message'}} + topic = Topic.new + topic.errors.add(:title, :blank) + assert_equal "fallback message", ActiveRecord::RecordInvalid.new(topic).message + end + end + end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 79442d68b0..c173ee9a15 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -189,7 +189,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t_utf8.save, "Should save t_utf8 as unique" # If database hasn't UTF-8 character set, this test fails - if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!" + if Topic.scoped(:select => 'LOWER(title) AS title').find(t_utf8).title == "я тоже уникальный!" t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!") assert !t2_utf8.valid?, "Shouldn't be valid" assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique" @@ -261,10 +261,10 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert i1.errors[:value].any?, "Should not be empty" end - def test_validates_uniqueness_inside_with_scope + def test_validates_uniqueness_inside_scoping Topic.validates_uniqueness_of(:title) - Topic.send(:with_scope, :find => { :conditions => { :author_name => "David" } }) do + Topic.where(:author_name => "David").scoping do t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary") assert t1.save t2 = Topic.new("title" => "I'm unique!", "author_name" => "David") @@ -325,4 +325,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert w6.errors[:city].any?, "Should have errors for city" assert_equal ["has already been taken"], w6.errors[:city], "Should have uniqueness message for city" end + + def test_validate_uniqueness_with_conditions + Topic.validates_uniqueness_of(:title, :conditions => Topic.where('approved = ?', true)) + Topic.create("title" => "I'm a topic", "approved" => true) + Topic.create("title" => "I'm an unapproved topic", "approved" => false) + + t3 = Topic.new("title" => "I'm a topic", "approved" => true) + assert !t3.valid?, "t3 shouldn't be valid" + + t4 = Topic.new("title" => "I'm an unapproved topic", "approved" => false) + assert t4.valid?, "t4 should be valid" + end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index e575a98170..b11b330374 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -93,30 +93,6 @@ class ValidationsTest < ActiveRecord::TestCase end end - def test_scoped_create_without_attributes - WrongReply.send(:with_scope, :create => {}) do - assert_raise(ActiveRecord::RecordInvalid) { WrongReply.create! } - end - end - - def test_create_with_exceptions_using_scope_for_protected_attributes - assert_nothing_raised do - ProtectedPerson.send(:with_scope, :create => { :first_name => "Mary" } ) do - person = ProtectedPerson.create! :addon => "Addon" - assert_equal person.first_name, "Mary", "scope should ignore attr_protected" - end - end - end - - def test_create_with_exceptions_using_scope_and_empty_attributes - assert_nothing_raised do - ProtectedPerson.send(:with_scope, :create => { :first_name => "Mary" } ) do - person = ProtectedPerson.create! - assert_equal person.first_name, "Mary", "should be ok when no attributes are passed to create!" - end - end - end - def test_create_without_validation reply = WrongReply.new assert !reply.save diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 88751a72f9..12373333b0 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -92,7 +92,7 @@ class DefaultXmlSerializationTest < ActiveRecord::TestCase end def test_should_serialize_datetime - assert_match %r{<created-at type=\"datetime\">2006-08-01T00:00:00Z</created-at>}, @xml + assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @xml end def test_should_serialize_boolean @@ -109,7 +109,7 @@ class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"datetime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml ensure Time.zone = timezone end @@ -118,7 +118,7 @@ class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)" toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"datetime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml + assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml ensure Time.zone = timezone end @@ -152,7 +152,7 @@ class NilXmlSerializationTest < ActiveRecord::TestCase assert %r{<created-at (.*)></created-at>}.match(@xml) attributes = $1 assert_match %r{nil="true"}, attributes - assert_match %r{type="datetime"}, attributes + assert_match %r{type="dateTime"}, attributes end def test_should_serialize_boolean @@ -188,7 +188,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text - assert_equal "datetime" , xml.elements["//written-on"].attributes['type'] + assert_equal "dateTime" , xml.elements["//written-on"].attributes['type'] assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text @@ -198,7 +198,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase if current_adapter?(:SybaseAdapter) assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text - assert_equal "datetime" , xml.elements["//last-read"].attributes['type'] + assert_equal "dateTime" , xml.elements["//last-read"].attributes['type'] else # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) assert_equal "2004-04-15", xml.elements["//last-read"].text @@ -211,7 +211,7 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert_equal "boolean" , xml.elements["//approved"].attributes['type'] assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text - assert_equal "datetime" , xml.elements["//bonus-time"].attributes['type'] + assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type'] end end diff --git a/activerecord/test/fixtures/peoples_treasures.yml b/activerecord/test/fixtures/peoples_treasures.yml new file mode 100644 index 0000000000..a72b190d0c --- /dev/null +++ b/activerecord/test/fixtures/peoples_treasures.yml @@ -0,0 +1,3 @@ +michael_diamond: + rich_person_id: <%= ActiveRecord::Fixtures.identify(:michael) %> + treasure_id: <%= ActiveRecord::Fixtures.identify(:diamond) %> diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index d0e628bd50..ad30039304 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -2,4 +2,6 @@ class Admin::User < ActiveRecord::Base belongs_to :account store :settings, :accessors => [ :color, :homepage ] store :preferences, :accessors => [ :remember_login ] + store :json_data, :accessors => [ :height, :weight ], :coder => JSON + store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => JSON end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index d50e11d6c9..14444a0092 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -140,8 +140,8 @@ class Author < ActiveRecord::Base has_many :posts_with_default_include, :class_name => 'PostWithDefaultInclude' has_many :comments_on_posts_with_default_include, :through => :posts_with_default_include, :source => :comments - scope :relation_include_posts, includes(:posts) - scope :relation_include_tags, includes(:tags) + scope :relation_include_posts, -> { includes(:posts) } + scope :relation_include_tags, -> { includes(:tags) } attr_accessor :post_log after_initialize :set_post_log diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 888afc7604..640e57555d 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,5 +1,5 @@ class Bulb < ActiveRecord::Base - default_scope where(:name => 'defaulty') + default_scope { where(:name => 'defaulty') } belongs_to :car attr_protected :car_id, :frickinawesome diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index 6ff1329d8e..b4bc0ad5fa 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -11,18 +11,17 @@ class Car < ActiveRecord::Base has_many :engines, :dependent => :destroy has_many :wheels, :as => :wheelable, :dependent => :destroy - scope :incl_tyres, includes(:tyres) - scope :incl_engines, includes(:engines) + scope :incl_tyres, -> { includes(:tyres) } + scope :incl_engines, -> { includes(:engines) } - scope :order_using_new_style, order('name asc') - scope :order_using_old_style, :order => 'name asc' + scope :order_using_new_style, -> { order('name asc') } end class CoolCar < Car - default_scope :order => 'name desc' + default_scope { order('name desc') } end class FastCar < Car - default_scope :order => 'name desc' + default_scope { order('name desc') } end diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 4bd980e606..6588531de6 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -12,7 +12,7 @@ end class SpecialCategorization < ActiveRecord::Base self.table_name = 'categorizations' - default_scope where(:special => true) + default_scope { where(:special => true) } belongs_to :author belongs_to :category diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index 02b85fd38a..ab3139680c 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -27,7 +27,7 @@ class Category < ActiveRecord::Base has_many :authors, :through => :categorizations has_many :authors_with_select, :through => :categorizations, :source => :author, :select => 'authors.*, categorizations.post_id' - scope :general, :conditions => { :name => 'General' } + scope :general, -> { where(:name => 'General') } end class SpecialCategory < Category diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 88b139d931..3e9f1b0635 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -1,16 +1,16 @@ class Comment < ActiveRecord::Base scope :limit_by, lambda {|l| limit(l) } - scope :containing_the_letter_e, :conditions => "comments.body LIKE '%e%'" - scope :not_again, where("comments.body NOT LIKE '%again%'") - scope :for_first_post, :conditions => { :post_id => 1 } - scope :for_first_author, - :joins => :post, - :conditions => { "posts.author_id" => 1 } - scope :created + scope :containing_the_letter_e, -> { where("comments.body LIKE '%e%'") } + scope :not_again, -> { where("comments.body NOT LIKE '%again%'") } + scope :for_first_post, -> { where(:post_id => 1) } + scope :for_first_author, -> { joins(:post).where("posts.author_id" => 1) } + scope :created, -> { scoped } belongs_to :post, :counter_cache => true has_many :ratings + belongs_to :first_post, :foreign_key => :post_id + has_many :children, :class_name => 'Comment', :foreign_key => :parent_id belongs_to :parent, :class_name => 'Comment', :counter_cache => :children_count @@ -19,13 +19,13 @@ class Comment < ActiveRecord::Base end def self.search_by_type(q) - self.find(:all, :conditions => ["#{QUOTED_TYPE} = ?", q]) + self.scoped(:where => ["#{QUOTED_TYPE} = ?", q]).all end def self.all_as_method all end - scope :all_as_scope, {} + scope :all_as_scope, -> { scoped } end class SpecialComment < Comment diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index fbdfaa2c29..7b993d5a2c 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -198,6 +198,11 @@ class Account < ActiveRecord::Base @destroyed_account_ids ||= Hash.new { |h,k| h[k] = [] } end + # Test private kernel method through collection proxy using has_many. + def self.open + where('firm_name = ?', '37signals') + end + before_destroy do |account| if account.firm Account.destroyed_account_ids[account.firm.id] << account.id diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 777f6b5ba0..7e8e82542f 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -1,7 +1,11 @@ class Customer < ActiveRecord::Base + cattr_accessor :gps_conversion_was_run + composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money } composed_of :gps_location, :allow_nil => true + composed_of :non_blank_gps_location, :class_name => "GpsLocation", :allow_nil => true, :mapping => %w(gps_location gps_location), + :converter => lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps)} composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse end diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 4dc9fff9fd..83482f4d07 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -2,20 +2,20 @@ require 'ostruct' module DeveloperProjectsAssociationExtension def find_most_recent - find(:first, :order => "id DESC") + scoped(:order => "id DESC").first end end module DeveloperProjectsAssociationExtension2 def find_least_recent - find(:first, :order => "id ASC") + scoped(:order => "id ASC").first end end class Developer < ActiveRecord::Base has_and_belongs_to_many :projects do def find_most_recent - find(:first, :order => "id DESC") + scoped(:order => "id DESC").first end end @@ -37,7 +37,7 @@ class Developer < ActiveRecord::Base :association_foreign_key => "project_id", :extend => DeveloperProjectsAssociationExtension do def find_least_recent - find(:first, :order => "id ASC") + scoped(:order => "id ASC").first end end @@ -45,7 +45,7 @@ class Developer < ActiveRecord::Base has_many :audit_logs - scope :jamises, :conditions => {:name => 'Jamis'} + scope :jamises, -> { where(:name => 'Jamis') } validates_inclusion_of :salary, :in => 50000..200000 validates_length_of :name, :within => 3..20 @@ -57,12 +57,6 @@ class Developer < ActiveRecord::Base def log=(message) audit_logs.build :message => message end - - def self.all_johns - self.with_exclusive_scope :find => where(:name => 'John') do - self.all - end - end end class AuditLog < ActiveRecord::Base @@ -88,31 +82,25 @@ end class DeveloperWithSelect < ActiveRecord::Base self.table_name = 'developers' - default_scope select('name') + default_scope { select('name') } end class DeveloperWithIncludes < ActiveRecord::Base self.table_name = 'developers' has_many :audit_logs, :foreign_key => :developer_id - default_scope includes(:audit_logs) + default_scope { includes(:audit_logs) } end class DeveloperOrderedBySalary < ActiveRecord::Base self.table_name = 'developers' - default_scope :order => 'salary DESC' - - scope :by_name, order('name DESC') + default_scope { order('salary DESC') } - def self.all_ordered_by_name - with_scope(:find => { :order => 'name DESC' }) do - find(:all) - end - end + scope :by_name, -> { order('name DESC') } end class DeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' - default_scope where("name = 'David'") + default_scope { where("name = 'David'") } end class LazyLambdaDeveloperCalledDavid < ActiveRecord::Base @@ -140,7 +128,7 @@ end class ClassMethodReferencingScopeDeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' - scope :david, where(:name => 'David') + scope :david, -> { where(:name => 'David') } def self.default_scope david @@ -149,40 +137,40 @@ end class LazyBlockReferencingScopeDeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' - scope :david, where(:name => 'David') + scope :david, -> { where(:name => 'David') } default_scope { david } end class DeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' - default_scope where(:name => 'Jamis') - scope :poor, where('salary < 150000') + default_scope { where(:name => 'Jamis') } + scope :poor, -> { where('salary < 150000') } end class PoorDeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' - default_scope where(:name => 'Jamis', :salary => 50000) + default_scope -> { where(:name => 'Jamis', :salary => 50000) } end class InheritedPoorDeveloperCalledJamis < DeveloperCalledJamis self.table_name = 'developers' - default_scope where(:salary => 50000) + default_scope -> { where(:salary => 50000) } end class MultiplePoorDeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' - default_scope where(:name => 'Jamis') - default_scope where(:salary => 50000) + default_scope -> { where(:name => 'Jamis') } + default_scope -> { where(:salary => 50000) } end module SalaryDefaultScope extend ActiveSupport::Concern - included { default_scope where(:salary => 50000) } + included { default_scope { where(:salary => 50000) } } end class ModuleIncludedPoorDeveloperCalledJamis < DeveloperCalledJamis @@ -195,7 +183,7 @@ class EagerDeveloperWithDefaultScope < ActiveRecord::Base self.table_name = 'developers' has_and_belongs_to_many :projects, :foreign_key => 'developer_id', :join_table => 'developers_projects', :order => 'projects.id' - default_scope includes(:projects) + default_scope { includes(:projects) } end class EagerDeveloperWithClassMethodDefaultScope < ActiveRecord::Base diff --git a/activerecord/test/models/organization.rb b/activerecord/test/models/organization.rb index 4a4111833f..72e7bade68 100644 --- a/activerecord/test/models/organization.rb +++ b/activerecord/test/models/organization.rb @@ -8,5 +8,5 @@ class Organization < ActiveRecord::Base has_one :author, :primary_key => :name has_one :author_owned_essay_category, :through => :author, :source => :owned_essay_category - scope :clubs, { :from => 'clubs' } + scope :clubs, -> { from('clubs') } end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index d2a0c6b40c..d5c0b351aa 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -1,8 +1,10 @@ class Person < ActiveRecord::Base has_many :readers + has_many :secure_readers has_one :reader has_many :posts, :through => :readers + has_many :secure_posts, :through => :secure_readers has_many :posts_with_no_comments, :through => :readers, :source => :post, :include => :comments, :conditions => 'comments.id is null', :references => :comments @@ -25,8 +27,8 @@ class Person < ActiveRecord::Base has_many :agents_posts, :through => :agents, :source => :posts has_many :agents_posts_authors, :through => :agents_posts, :source => :author - scope :males, :conditions => { :gender => 'M' } - scope :females, :conditions => { :gender => 'F' } + scope :males, -> { where(:gender => 'M') } + scope :females, -> { where(:gender => 'F') } end class PersonWithDependentDestroyJobs < ActiveRecord::Base @@ -83,3 +85,9 @@ class TightPerson < ActiveRecord::Base end class TightDescendant < TightPerson; end + +class RichPerson < ActiveRecord::Base + self.table_name = 'people' + + has_and_belongs_to_many :treasures, :join_table => 'peoples_treasures' +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 1cab78d8c7..1aaf9a1b82 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -5,15 +5,10 @@ class Post < ActiveRecord::Base end end - scope :containing_the_letter_a, where("body LIKE '%a%'") - scope :ranked_by_comments, order("comments_count DESC") + scope :containing_the_letter_a, -> { where("body LIKE '%a%'") } + scope :ranked_by_comments, -> { order("comments_count DESC") } scope :limit_by, lambda {|l| limit(l) } - scope :with_authors_at_address, lambda { |address| { - :conditions => [ 'authors.author_address_id = ?', address.id ], - :joins => 'JOIN authors ON authors.id = posts.author_id' - } - } belongs_to :author do def greeting @@ -30,15 +25,13 @@ class Post < ActiveRecord::Base has_one :first_comment, :class_name => 'Comment', :order => 'id ASC' has_one :last_comment, :class_name => 'Comment', :order => 'id desc' - scope :with_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'SpecialComment'} } - scope :with_very_special_comments, joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) - scope :with_post, lambda {|post_id| - { :joins => :comments, :conditions => {:comments => {:post_id => post_id} } } - } + scope :with_special_comments, -> { joins(:comments).where(:comments => {:type => 'SpecialComment'}) } + scope :with_very_special_comments, -> { joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) } + scope :with_post, ->(post_id) { joins(:comments).where(:comments => { :post_id => post_id }) } has_many :comments do def find_most_recent - find(:first, :order => "id DESC") + scoped(:order => "id DESC").first end def newest @@ -71,8 +64,8 @@ class Post < ActiveRecord::Base has_many :taggings, :as => :taggable has_many :tags, :through => :taggings do def add_joins_and_select - find :all, :select => 'tags.*, authors.id as author_id', - :joins => 'left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id' + scoped(:select => 'tags.*, authors.id as author_id', + :joins => 'left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id').all end end @@ -115,8 +108,10 @@ class Post < ActiveRecord::Base has_many :named_categories, :through => :standard_categorizations has_many :readers + has_many :secure_readers has_many :readers_with_person, :include => :person, :class_name => "Reader" has_many :people, :through => :readers + has_many :secure_people, :through => :secure_readers has_many :single_people, :through => :readers has_many :people_with_callbacks, :source=>:person, :through => :readers, :before_add => lambda {|owner, reader| log(:added, :before, reader.first_name) }, @@ -169,7 +164,7 @@ end class FirstPost < ActiveRecord::Base self.table_name = 'posts' - default_scope where(:id => 1) + default_scope { where(:id => 1) } has_many :comments, :foreign_key => :post_id has_one :comment, :foreign_key => :post_id @@ -177,16 +172,16 @@ end class PostWithDefaultInclude < ActiveRecord::Base self.table_name = 'posts' - default_scope includes(:comments) + default_scope { includes(:comments) } has_many :comments, :foreign_key => :post_id end class PostWithDefaultScope < ActiveRecord::Base self.table_name = 'posts' - default_scope :order => :title + default_scope { order(:title) } end class SpecialPostWithDefaultScope < ActiveRecord::Base self.table_name = 'posts' - default_scope where(:id => [1, 5,6]) + default_scope { where(:id => [1, 5,6]) } end diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index efe1ce67da..32ce164995 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -32,7 +32,7 @@ class Project < ActiveRecord::Base def self.all_as_method all end - scope :all_as_scope, {} + scope :all_as_scope, -> { scoped } end class SpecialProject < Project diff --git a/activerecord/test/models/reader.rb b/activerecord/test/models/reader.rb index 0207a2bd92..59005ac604 100644 --- a/activerecord/test/models/reader.rb +++ b/activerecord/test/models/reader.rb @@ -3,3 +3,12 @@ class Reader < ActiveRecord::Base belongs_to :person, :inverse_of => :readers belongs_to :single_person, :class_name => 'Person', :foreign_key => :person_id, :inverse_of => :reader end + +class SecureReader < ActiveRecord::Base + self.table_name = "readers" + + belongs_to :secure_post, :class_name => "Post", :foreign_key => "post_id" + belongs_to :secure_person, :inverse_of => :secure_readers, :class_name => "Person", :foreign_key => "person_id" + + attr_accessible nil +end diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index c5af0b5d5f..561b431766 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -19,5 +19,5 @@ end class BadReference < ActiveRecord::Base self.table_name = 'references' - default_scope where(:favourite => false) + default_scope { where(:favourite => false) } end diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index 6adfe0ae3c..53bc95e5f2 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -1,7 +1,7 @@ require 'models/topic' class Reply < Topic - scope :base + scope :base, -> { scoped } belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true belongs_to :topic_with_primary_key, :class_name => "Topic", :primary_key => "title", :foreign_key => "parent_title", :counter_cache => "replies_count" diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 8bcb9df8a8..079e403444 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -1,21 +1,20 @@ class Topic < ActiveRecord::Base - scope :base + scope :base, -> { scoped } scope :written_before, lambda { |time| if time - { :conditions => ['written_on < ?', time] } + where 'written_on < ?', time end } - scope :approved, :conditions => {:approved => true} - scope :rejected, :conditions => {:approved => false} + scope :approved, -> { where(:approved => true) } + scope :rejected, -> { where(:approved => false) } scope :scope_with_lambda, lambda { scoped } - scope :by_lifo, :conditions => {:author_name => 'lifo'} + scope :by_lifo, -> { where(:author_name => 'lifo') } + scope :replied, -> { where 'replies_count > 0' } - scope :approved_as_hash_condition, :conditions => {:topics => {:approved => true}} - scope 'approved_as_string', :conditions => {:approved => true} - scope :replied, :conditions => ['replies_count > 0'] - scope :anonymous_extension do + scope 'approved_as_string', -> { where(:approved => true) } + scope :anonymous_extension, -> { scoped } do def one 1 end @@ -32,18 +31,6 @@ class Topic < ActiveRecord::Base 2 end end - module MultipleExtensionOne - def extension_one - 1 - end - end - module MultipleExtensionTwo - def extension_two - 2 - end - end - scope :named_extension, :extend => NamedExtension - scope :multiple_extensions, :extend => [MultipleExtensionTwo, MultipleExtensionOne] has_many :replies, :dependent => :destroy, :foreign_key => "parent_id" has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title" diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb index 0377e50011..ddc7048a56 100644 --- a/activerecord/test/models/toy.rb +++ b/activerecord/test/models/toy.rb @@ -2,5 +2,5 @@ class Toy < ActiveRecord::Base self.primary_key = :toy_id belongs_to :pet - scope :with_pet, joins(:pet) + scope :with_pet, -> { joins(:pet) } end diff --git a/activerecord/test/models/without_table.rb b/activerecord/test/models/without_table.rb index 184ab1649e..50c824e4ac 100644 --- a/activerecord/test/models/without_table.rb +++ b/activerecord/test/models/without_table.rb @@ -1,3 +1,3 @@ class WithoutTable < ActiveRecord::Base - default_scope where(:published => true) + default_scope -> { where(:published => true) } end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 25b416a906..e51db50ae3 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,8 +1,8 @@ ActiveRecord::Schema.define do %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings - postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones).each do |table_name| - execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" + postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| + execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end execute 'DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE' @@ -10,6 +10,8 @@ ActiveRecord::Schema.define do execute "ALTER TABLE companies ALTER COLUMN id SET DEFAULT nextval('companies_nonstd_seq')" execute 'DROP SEQUENCE IF EXISTS companies_id_seq' + execute 'DROP FUNCTION IF EXISTS partitioned_insert_trigger()' + %w(accounts_id_seq developers_id_seq projects_id_seq topics_id_seq customers_id_seq orders_id_seq).each do |seq_name| execute "SELECT setval('#{seq_name}', 100)" end @@ -125,6 +127,37 @@ _SQL ); _SQL +begin + execute <<_SQL + CREATE TABLE postgresql_partitioned_table_parent ( + id SERIAL PRIMARY KEY, + number integer + ); + CREATE TABLE postgresql_partitioned_table ( ) + INHERITS (postgresql_partitioned_table_parent); + + CREATE OR REPLACE FUNCTION partitioned_insert_trigger() + RETURNS TRIGGER AS $$ + BEGIN + INSERT INTO postgresql_partitioned_table VALUES (NEW.*); + RETURN NULL; + END; + $$ + LANGUAGE plpgsql; + + CREATE TRIGGER insert_partitioning_trigger + BEFORE INSERT ON postgresql_partitioned_table_parent + FOR EACH ROW EXECUTE PROCEDURE partitioned_insert_trigger(); +_SQL +rescue ActiveRecord::StatementInvalid => e + if e.message =~ /language "plpgsql" does not exist/ + execute "CREATE LANGUAGE 'plpgsql';" + retry + else + raise e + end +end + begin execute <<_SQL CREATE TABLE postgresql_xml_data_type ( diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 428a85ab4e..5bcb9652cd 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -38,7 +38,11 @@ ActiveRecord::Schema.define do create_table :admin_users, :force => true do |t| t.string :name t.text :settings, :null => true - t.text :preferences, :null => false, :default => "" + # MySQL does not allow default values for blobs. Fake it out with a + # big varchar below. + t.string :preferences, :null => false, :default => '', :limit => 1024 + t.string :json_data, :null => true, :limit => 1024 + t.string :json_data_empty, :null => false, :default => "", :limit => 1024 t.references :account end @@ -76,6 +80,7 @@ ActiveRecord::Schema.define do create_table :binaries, :force => true do |t| t.string :name t.binary :data + t.binary :short_data, :limit => 2048 end create_table :birds, :force => true do |t| @@ -91,6 +96,7 @@ ActiveRecord::Schema.define do create_table :booleans, :force => true do |t| t.boolean :value + t.boolean :has_fun, :null => false, :default => false end create_table :bulbs, :force => true do |t| @@ -172,6 +178,7 @@ ActiveRecord::Schema.define do t.integer :client_of t.integer :rating, :default => 1 t.integer :account_id + t.string :description, :null => false, :default => "" end add_index :companies, [:firm_id, :type, :rating, :ruby_type], :name => "company_index" @@ -425,6 +432,7 @@ ActiveRecord::Schema.define do t.string :name t.column :updated_at, :datetime t.column :happy_at, :datetime + t.column :eats_at, :time t.string :essay_id end @@ -438,6 +446,7 @@ ActiveRecord::Schema.define do create_table :parrots, :force => true do |t| t.column :name, :string + t.column :color, :string t.column :parrot_sti_class, :string t.column :killer_id, :integer t.column :created_at, :datetime @@ -468,6 +477,11 @@ ActiveRecord::Schema.define do t.timestamps end + create_table :peoples_treasures, :id => false, :force => true do |t| + t.column :rich_person_id, :integer + t.column :treasure_id, :integer + end + create_table :pets, :primary_key => :pet_id ,:force => true do |t| t.string :name t.integer :owner_id, :integer diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index ea05b35fe0..e9ddeb32cf 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -1,5 +1,5 @@ ActiveRecord::Schema.define do - # For sqlite 3.1.0+, make a table with a autoincrement column + # For sqlite 3.1.0+, make a table with an autoincrement column if supports_autoincrement? create_table :table_with_autoincrement, :force => true do |t| t.column :name, :string diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index 11154c3797..c176316a05 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -12,7 +12,7 @@ module ARTest end def self.connect - puts "Using #{connection_name} with Identity Map #{ActiveRecord::IdentityMap.enabled? ? 'on' : 'off'}" + puts "Using #{connection_name}" ActiveRecord::Model.logger = ActiveSupport::Logger.new("debug.log") ActiveRecord::Model.configurations = connection_config ActiveRecord::Model.establish_connection 'arunit' |