diff options
Diffstat (limited to 'activerecord')
64 files changed, 1302 insertions, 591 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 9ff29f1155..32bcf02139 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,24 @@ *Rails 3.1.0 (unreleased)* +* CSV Fixtures are deprecated and support will be removed in Rails 3.2.0 + +* AR#new, AR#create, AR#create!, AR#update_attributes and AR#update_attributes! all accept a second hash as option that allows you + to specify which role to consider when assigning attributes. This is built on top of ActiveModel's + new mass assignment capabilities: + + class Post < ActiveRecord::Base + attr_accessible :title + attr_accessible :title, :published_at, :as => :admin + end + + Post.new(params[:post], :as => :admin) + + assign_attributes() with similar API was also added and attributes=(params, guard) was deprecated. + + Please note that this changes the method signatures for AR#new, AR#create, AR#create!, AR#update_attributes and AR#update_attributes!. If you have overwritten these methods you should update them accordingly. + + [Josh Kalderimis] + * default_scope can take a block, lambda, or any other object which responds to `call` for lazy evaluation: @@ -22,25 +41,7 @@ [Jon Leighton] -* Calling 'default_scope' multiple times in a class (including when a superclass calls - 'default_scope') is deprecated. The current behavior is that this will merge the default - scopes together: - - class Post < ActiveRecord::Base # Rails 3.1 - default_scope where(:published => true) - default_scope where(:hidden => false) - # The default scope is now: where(:published => true, :hidden => false) - end - - In Rails 3.2, the behavior will be changed to overwrite previous scopes: - - class Post < ActiveRecord::Base # Rails 3.2 - default_scope where(:published => true) - default_scope where(:hidden => false) - # The default scope is now: where(:hidden => false) - end - - If you wish to merge default scopes in special ways, it is recommended to define your default +* If you wish to merge default scopes in special ways, it is recommended to define your default scope as a class method and use the standard techniques for sharing code (inheritance, mixins, etc.): @@ -293,6 +294,84 @@ IrreversibleMigration exception will be raised when going down. [Aaron Patterson] +*Rails 3.0.7 (April 18, 2011)* + +* Destroying records via nested attributes works independent of reject_if LH #6006 [Durran Jordan] + +* Delegate any? and many? to Model.scoped for consistency [Andrew White] + +* Quote the ORDER BY clause in batched finds - fixes #6620 [Andrew White] + +* Change exists? so records are not instantiated - fixes #6127. This prevents after_find + and after_initialize callbacks being triggered when checking for record existence. + [Andrew White] + +* Fix performance bug with attribute accessors which only occurred on Ruby 1.8.7, and ensure we + cache type-casted values when the column returned from the db contains non-standard chars. + [Jon Leighton] + +* Fix a performance regression introduced here 86acbf1cc050c8fa8c74a10c735e467fb6fd7df8 + related to read_attribute method [Stian Grytøyr] + + +*Rails 3.0.6 (April 5, 2011)* + +* Un-deprecate reorder method [Sebastian Martinez] + +* Extensions are applied when calling +except+ or +only+ on relations. + Thanks to Iain Hecker. + +* Schemas set in set_table_name are respected by the mysql adapter. LH #5322 + +* Fixed a bug when empty? was called on a grouped Relation that wasn't loaded. + LH #5829 + +* Reapply extensions when using except and only. Thanks Iain Hecker. + +* Binary data is escaped when being inserted to SQLite3 Databases. Thanks + Naruse! + + +*Rails 3.0.5 (February 26, 2011)* + +* Model.where(:column => 1).where(:column => 2) will always produce an AND +query. + + [Aaron Patterson] + +* Deprecated support for interpolated association conditions in the form of :conditions => 'foo = #{bar}'. + + Instead, you should use a proc, like so: + + Before: + + has_many :things, :conditions => 'foo = #{bar}' + + After: + + has_many :things, :conditions => proc { "foo = #{bar}" } + + Inside the proc, 'self' is the object which is the owner of the association, unless you are + eager loading the association, in which case 'self' is the class which the association is within. + + You can have any "normal" conditions inside the proc, so the following will work too: + + has_many :things, :conditions => proc { ["foo = ?", bar] } + + Previously :insert_sql and :delete_sql on has_and_belongs_to_many association allowed you to call + 'record' to get the record being inserted or deleted. This is now passed as an argument to + the proc. + + [Jon Leighton] + + +*Rails 3.0.4 (February 8, 2011)* + +* Added deprecation warning for has_and_belongs_to_many associations where the join table has + additional attributes other than the keys. Access to these attributes is removed in 3.1. + Please use has_many :through instead. [Jon Leighton] + + *Rails 3.0.3 (November 16, 2010)* * Support find by class like this: Post.where(:name => Post) @@ -329,10 +408,12 @@ IrreversibleMigration exception will be raised when going down. [Aaron Patterson] + *Rails 3.0.1 (October 15, 2010)* * Introduce a fix for CVE-2010-3993 + *Rails 3.0.0 (August 29, 2010)* * Changed update_attribute to not run callbacks and update the record directly in the database [Neeraj Singh] @@ -532,12 +613,12 @@ IrreversibleMigration exception will be raised when going down. * Add Support for updating deeply nested models from a single form. #1202 [Eloy Duran] - class Book < ActiveRecord::Base - has_one :author - has_many :pages + class Book < ActiveRecord::Base + has_one :author + has_many :pages - accepts_nested_attributes_for :author, :pages - end + accepts_nested_attributes_for :author, :pages + end * Make after_save callbacks fire only if the record was successfully saved. #1735 [Michael Lovitt] @@ -957,7 +1038,7 @@ so newlines etc are escaped #10385 [Norbert Crombach] "foo.bar" => "`foo`.`bar`" * Complete the assimilation of Sexy Migrations from ErrFree [Chris Wanstrath, PJ Hyett] - http://errtheblog.com/post/2381 + http://errtheblog.com/post/2381 * Qualified column names work in hash conditions, like :conditions => { 'comments.created_at' => ... }. #9733 [Jack Danger Canty] @@ -1073,7 +1154,7 @@ single-table inheritance. #3833, #9886 [Gabriel Gironda, rramdas, François Bea * Improve performance and functionality of the postgresql adapter. Closes #8049 [roderickvd] - For more information see: http://dev.rubyonrails.org/ticket/8049 + For more information see: http://dev.rubyonrails.org/ticket/8049 * Don't clobber includes passed to has_many.count [Jack Danger Canty] @@ -1583,8 +1664,8 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Added support for conditions on Base.exists? #5689 [Josh Peek]. Examples: assert (Topic.exists?(:author_name => "David")) - assert (Topic.exists?(:author_name => "Mary", :approved => true)) - assert (Topic.exists?(["parent_id = ?", 1])) + assert (Topic.exists?(:author_name => "Mary", :approved => true)) + assert (Topic.exists?(["parent_id = ?", 1])) * Schema dumper quotes date :default values. [Dave Thomas] @@ -2040,8 +2121,8 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing] * Added support for conditions on Base.exists? #5689 [Josh Peek]. Examples: assert (Topic.exists?(:author_name => "David")) - assert (Topic.exists?(:author_name => "Mary", :approved => true)) - assert (Topic.exists?(["parent_id = ?", 1])) + assert (Topic.exists?(:author_name => "Mary", :approved => true)) + assert (Topic.exists?(["parent_id = ?", 1])) * Schema dumper quotes date :default values. [Dave Thomas] diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index a27640eac9..3a89446a83 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -219,4 +219,4 @@ API documentation is at Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: -* https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets +* https://github.com/rails/rails/issues diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 9d4cbbc150..3a5035305b 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -23,5 +23,5 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) s.add_dependency('arel', '~> 2.1.0') - s.add_dependency('tzinfo', '~> 0.3.23') + s.add_dependency('tzinfo', '~> 0.3.27') end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 06a414b874..62d48d3a2c 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -13,6 +13,19 @@ module ActiveRecord::Associations::Builder private + def define_readers + super + name = self.name + + model.redefine_method("#{name}_loaded?") do + ActiveSupport::Deprecation.warn( + "Calling obj.#{name}_loaded? is deprecated. Please use " \ + "obj.association(:#{name}).loaded? instead." + ) + association(name).loaded? + end + end + def define_constructors name = self.name diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 6cdec8c487..4429c655a2 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -4,7 +4,7 @@ module ActiveRecord module Associations # = Active Record Association Collection # - # AssociationCollection is an abstract class that provides common stuff to + # CollectionAssociation is an abstract class that provides common stuff to # ease the implementation of association proxies that represent # collections. See the class hierarchy in AssociationProxy. # @@ -94,7 +94,13 @@ module ActiveRecord end def build(attributes = {}, options = {}, &block) - build_or_create(:build, attributes, options, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| build(attr, options, &block) } + else + add_to_target(build_record(attributes, options)) do |record| + yield(record) if block_given? + end + end end def create(attributes = {}, options = {}, &block) @@ -102,7 +108,16 @@ module ActiveRecord raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" end - build_or_create(:create, attributes, options, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr, options, &block) } + else + transaction do + add_to_target(build_record(attributes, options)) do |record| + yield(record) if block_given? + insert_record(record) + end + end + end end def create!(attrs = {}, options = {}, &block) @@ -321,15 +336,7 @@ module ActiveRecord def load_target if find_target? - targets = [] - - begin - targets = find_target - rescue ActiveRecord::RecordNotFound - reset - end - - @target = merge_target_lists(targets, target) + @target = merge_target_lists(find_target, target) end loaded! @@ -337,20 +344,18 @@ module ActiveRecord end def add_to_target(record) - transaction do - callback(:before_add, record) - yield(record) if block_given? + callback(:before_add, record) + yield(record) if block_given? - if options[:uniq] && index = @target.index(record) - @target[index] = record - else - @target << record - end - - callback(:after_add, record) - set_inverse_instance(record) + if options[:uniq] && index = @target.index(record) + @target[index] = record + else + @target << record end + callback(:after_add, record) + set_inverse_instance(record) + record end @@ -374,7 +379,7 @@ module ActiveRecord if options[:finder_sql] reflection.klass.find_by_sql(custom_finder_sql) else - find(:all) + scoped.all end records = options[:uniq] ? uniq(records) : records @@ -403,26 +408,16 @@ module ActiveRecord end + existing end - def build_or_create(method, attributes, options) - records = Array.wrap(attributes).map do |attrs| - record = build_record(attrs, options) - - add_to_target(record) do - yield(record) if block_given? - insert_record(record) if method == :create - end - end - - attributes.is_a?(Array) ? records : records.first - end - # Do the relevant stuff to insert the given record into the association collection. def insert_record(record, validate = true) raise NotImplementedError end def build_record(attributes, options) - reflection.build_association(scoped.scope_for_create.merge(attributes), options) + record = reflection.build_association + record.assign_attributes(scoped.scope_for_create, :without_protection => true) + record.assign_attributes(attributes, options) + record end def delete_or_destroy(records, method) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 388173c1fb..adfc71d435 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -64,9 +64,12 @@ module ActiveRecord def method_missing(method, *args, &block) match = DynamicFinderMatch.match(method) - if match && match.creator? - attributes = match.attribute_names - return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) + if match && match.instantiator? + record = send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r| + @association.send :set_owner_attributes, r + @association.send :add_to_target, r + yield(r) if block_given? + end end if target.respond_to?(method) || (!@association.klass.respond_to?(method) && Class.respond_to?(method)) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 7134dc85c8..2f3a6e71f1 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -10,7 +10,7 @@ module ActiveRecord reflection.klass.transaction do if target && target != record - remove_target!(options[:dependent]) + remove_target!(options[:dependent]) unless target.destroyed? end if record diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 0a666598ed..c32753782f 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -91,12 +91,12 @@ module ActiveRecord constraint = build_constraint(reflection, table, key, foreign_table, foreign_key) - relation.from(join(table, constraint)) - unless conditions[i].empty? - relation.where(sanitize(conditions[i], table)) + constraint = constraint.and(sanitize(conditions[i], table)) end + relation.from(join(table, constraint)) + # The current table in this iteration becomes the foreign table in the next foreign_table = table end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index ea4d73d414..877ddf3ee1 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -18,7 +18,7 @@ module ActiveRecord end def create(attributes = {}, options = {}) - new_record(:create, attributes, options) + build(attributes, options).tap { |record| record.save } end def create!(attributes = {}, options = {}) @@ -26,7 +26,14 @@ module ActiveRecord end def build(attributes = {}, options = {}) - new_record(:build, attributes, options) + record = reflection.build_association + record.assign_attributes( + scoped.scope_for_create.except(klass.primary_key), + :without_protection => true + ) + record.assign_attributes(attributes, options) + set_new_record(record) + record end private @@ -43,13 +50,6 @@ module ActiveRecord def set_new_record(record) replace(record) end - - def new_record(method, attributes, options) - attributes = scoped.scope_for_create.merge(attributes || {}) - record = reflection.send("#{method}_association", attributes, options) - set_new_record(record) - record - end end end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index e6ab628719..53c5c3cedf 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -14,7 +14,10 @@ module ActiveRecord def target_scope scope = super chain[1..-1].each do |reflection| - scope = scope.merge(reflection.klass.scoped) + scope = scope.merge( + reflection.klass.scoped.with_default_scope. + except(:select, :create_with) + ) end scope end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 58a056bce9..e1bf2ccc8a 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -482,7 +482,7 @@ module ActiveRecord #:nodoc: # # Create a single new object # User.create(:first_name => 'Jamie') # - # # Create a single new object using the :admin mass-assignment security scope + # # Create a single new object using the :admin mass-assignment security role # User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) # # # Create a single new object bypassing mass-assignment security @@ -830,6 +830,10 @@ module ActiveRecord #:nodoc: @symbolized_base_class ||= base_class.to_s.to_sym end + def symbolized_sti_name + @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class + end + # Returns the base AR subclass that this class descends from. If A # extends AR::Base, A.base_class will return A. If B descends from A # through some arbitrarily deep hierarchy, B.base_class will return A. @@ -891,7 +895,7 @@ module ActiveRecord #:nodoc: # not use the default_scope: # # Post.unscoped { - # limit(10) # Fires "SELECT * FROM posts LIMIT 10" + # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } # # It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt> @@ -1482,7 +1486,7 @@ MSG # # Instantiates a single new object # User.new(:first_name => 'Jamie') # - # # Instantiates a single new object using the :admin mass-assignment security scope + # # Instantiates a single new object using the :admin mass-assignment security role # User.new({ :first_name => 'Jamie', :is_admin => true }, :as => :admin) # # # Instantiates a single new object bypassing mass-assignment security @@ -1648,17 +1652,16 @@ MSG return unless new_attributes.is_a?(Hash) - guard_protected_attributes ||= true - if guard_protected_attributes - assign_attributes(new_attributes) - else + if guard_protected_attributes == false assign_attributes(new_attributes, :without_protection => true) + else + assign_attributes(new_attributes) end end # Allows you to set all the attributes for a particular mass-assignment - # security scope by passing in a hash of attributes with keys matching - # the attribute names (which again matches the column names) and the scope + # security role by passing in a hash of attributes with keys matching + # the attribute names (which again matches the column names) and the role # name using the :as option. # # To bypass mass-assignment security you can use the :without_protection => true @@ -1684,13 +1687,15 @@ MSG # user.name # => "Josh" # user.is_admin? # => true def assign_attributes(new_attributes, options = {}) + return unless new_attributes + attributes = new_attributes.stringify_keys - scope = options[:as] || :default + role = options[:as] || :default multi_parameter_attributes = [] unless options[:without_protection] - attributes = sanitize_for_mass_assignment(attributes, scope) + attributes = sanitize_for_mass_assignment(attributes, role) end attributes.each do |k, v| @@ -1943,32 +1948,9 @@ MSG errors = [] callstack.each do |name, values_with_empty_parameters| begin - klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass - # in order to allow a date to be set without a year, we must keep the empty values. - # Otherwise, we wouldn't be able to distinguish it from a date with an empty day. - values = values_with_empty_parameters.reject { |v| v.nil? } - - if values.empty? - send(name + "=", nil) - else - - value = if Time == klass - instantiate_time_object(name, values) - elsif Date == klass - begin - values = values_with_empty_parameters.collect do |v| v.nil? ? 1 : v end - Date.new(*values) - rescue ArgumentError => ex # if Date.new raises an exception on an invalid date - instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates - end - else - klass.new(*values) - end - - send(name + "=", value) - end + send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) rescue => ex - errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name) + errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name) end end unless errors.empty? @@ -1976,19 +1958,65 @@ MSG end end + def read_value_from_parameter(name, values_hash_from_param) + klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass + if values_hash_from_param.values.all?{|v|v.nil?} + nil + elsif klass == Time + read_time_parameter_value(name, values_hash_from_param) + elsif klass == Date + read_date_parameter_value(name, values_hash_from_param) + else + read_other_parameter_value(klass, name, values_hash_from_param) + end + end + + def read_time_parameter_value(name, values_hash_from_param) + # If Date bits were not provided, error + raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)} + max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) + set_values = (1..max_position).collect{|position| values_hash_from_param[position] } + # If Date bits were provided but blank, then default to 1 + # If Time bits are not there, then default to 0 + [1,1,1,0,0,0].each_with_index{|v,i| set_values[i] = set_values[i].blank? ? v : set_values[i]} + instantiate_time_object(name, set_values) + end + + def read_date_parameter_value(name, values_hash_from_param) + set_values = (1..3).collect{|position| values_hash_from_param[position].blank? ? 1 : values_hash_from_param[position]} + begin + Date.new(*set_values) + rescue ArgumentError => ex # if Date.new raises an exception on an invalid date + instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates + end + end + + def read_other_parameter_value(klass, name, values_hash_from_param) + max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) + values = (1..max_position).collect do |position| + raise "Missing Parameter" if !values_hash_from_param.has_key?(position) + values_hash_from_param[position] + end + klass.new(*values) + end + + def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) + [values_hash_from_param.keys.max,upper_cap].min + end + def extract_callstack_for_multiparameter_attributes(pairs) attributes = { } for pair in pairs multiparameter_name, value = pair attribute_name = multiparameter_name.split("(").first - attributes[attribute_name] = [] unless attributes.include?(attribute_name) + attributes[attribute_name] = {} unless attributes.include?(attribute_name) parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) - attributes[attribute_name] << [ find_parameter_position(multiparameter_name), parameter_value ] + attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value end - attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } } + attributes end def type_cast_attribute_value(multiparameter_name, value) @@ -1996,7 +2024,7 @@ MSG end def find_parameter_position(multiparameter_name) - multiparameter_name.scan(/\(([0-9]*).*\)/).first.first + multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i end # Returns a comma-separated pair list, like "key1 = val1, key2 = val2". 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 3045e30407..b3eb23bbb3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -6,15 +6,7 @@ module ActiveRecord # Returns an array of record hashes with the column names as keys and # column values as values. def select_all(sql, name = nil, binds = []) - if supports_statement_cache? - select(sql, name, binds) - else - return select(sql, name) if binds.empty? - binds = binds.dup - select sql.gsub('?') { - quote(*binds.shift.reverse) - }, name - end + select(sql, name, binds) end # Returns a record hash with the column names as keys and column values diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 8af22fe9f5..ac2da73a84 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -184,6 +184,10 @@ module ActiveRecord QUOTED_FALSE end + def substitute_at(column, index) + Arel.sql "\0" + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity(&block) #:nodoc: @@ -292,14 +296,14 @@ module ActiveRecord binds = binds.dup # Pretend to support bind parameters - execute sql.gsub('?') { quote(*binds.shift.reverse) }, name + execute sql.gsub("\0") { quote(*binds.shift.reverse) }, name end def exec_delete(sql, name, binds) binds = binds.dup # Pretend to support bind parameters - execute sql.gsub('?') { quote(*binds.shift.reverse) }, name + execute sql.gsub("\0") { quote(*binds.shift.reverse) }, name @connection.affected_rows end alias :exec_update :exec_delete @@ -646,7 +650,8 @@ module ActiveRecord # Returns an array of record hashes with the column names as keys and # column values as values. def select(sql, name = nil, binds = []) - exec_query(sql, name, binds).to_a + binds = binds.dup + exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a end def exec_query(sql, name = 'SQL', binds = []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 37db2be7a9..f4beeceb61 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -39,6 +39,16 @@ module ActiveRecord # :stopdoc: class << self attr_accessor :money_precision + def string_to_time(string) + return string unless String === string + + case string + when 'infinity' then 1.0 / 0.0 + when '-infinity' then -1.0 / 0.0 + else + super + end + end end # :startdoc: @@ -349,6 +359,9 @@ module ActiveRecord return super unless column case value + when Float + return super unless value.infinite? && column.type == :datetime + "'#{value.to_s.downcase}'" when Numeric return super unless column.sql_type == 'money' # Not truly string input, so doesn't require (or allow) escape string syntax. diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 96fea741e0..4aa6389a04 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -13,6 +13,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/logger' require 'active_support/ordered_hash' +require 'active_support/core_ext/module/deprecation' if defined? ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: @@ -28,11 +29,9 @@ class FixturesFileNotFound < StandardError; end # # = Fixture formats # -# Fixtures come in 3 flavors: +# Fixtures come in 1 flavor: # # 1. YAML fixtures -# 2. CSV fixtures -# 3. Single-file fixtures # # == YAML fixtures # @@ -74,56 +73,6 @@ class FixturesFileNotFound < StandardError; end # parent_id: 1 # title: Child # -# == CSV fixtures -# -# Fixtures can also be kept in the Comma Separated Value (CSV) format. Akin to YAML fixtures, CSV fixtures are stored -# in a single file, but instead end with the <tt>.csv</tt> file extension -# (Rails example: <tt><your-rails-app>/test/fixtures/web_sites.csv</tt>). -# -# The format of this type of fixture file is much more compact than the others, but also a little harder to read by us -# humans. The first line of the CSV file is a comma-separated list of field names. The rest of the -# file is then comprised -# of the actual data (1 per line). Here's an example: -# -# id, name, url -# 1, Ruby On Rails, http://www.rubyonrails.org -# 2, Google, http://www.google.com -# -# Should you have a piece of data with a comma character in it, you can place double quotes around that value. If you -# need to use a double quote character, you must escape it with another double quote. -# -# Another unique attribute of the CSV fixture is that it has *no* fixture name like the other two formats. Instead, the -# fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing -# number to the end. In our example, the 1st fixture would be called "web_site_1" and the 2nd one would be called -# "web_site_2". -# -# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you -# have existing data somewhere already. -# -# == Single-file fixtures -# -# This type of fixture was the original format for Active Record that has since been deprecated in -# favor of the YAML and CSV formats. -# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) -# to the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically -# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/<your-model-name>/</tt> -- -# like <tt><your-rails-app>/test/fixtures/web_sites/</tt> for the WebSite model). -# -# Each text file placed in this directory represents a "record". Usually these types of fixtures are named without -# extensions, but if you are on a Windows machine, you might consider adding <tt>.txt</tt> as the extension. -# Here's what the above example might look like: -# -# web_sites/google -# web_sites/yahoo.txt -# web_sites/ruby-on-rails -# -# The file format of a standard fixture is simple. Each line is a property (or column in db speak) and has the syntax -# of "name => value". Here's an example of the ruby-on-rails fixture above: -# -# id => 1 -# name => Ruby on Rails -# url => http://www.rubyonrails.org -# # = Using fixtures in testcases # # Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the @@ -176,7 +125,7 @@ class FixturesFileNotFound < StandardError; end # = Dynamic fixtures with ERB # # Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can -# mix ERB in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like: +# mix ERB in with your YAML fixtures to create a bunch of fixtures for load testing, like: # # <% for i in 1..1000 %> # fix_<%= i %>: @@ -423,8 +372,8 @@ class FixturesFileNotFound < StandardError; end # to the rescue: # # george_reginald: -# monkey_id: <%= Fixtures.identify(:reginald) %> -# pirate_id: <%= Fixtures.identify(:george) %> +# monkey_id: <%= ActiveRecord::Fixtures.identify(:reginald) %> +# pirate_id: <%= ActiveRecord::Fixtures.identify(:george) %> # # == Support for YAML defaults # @@ -444,369 +393,375 @@ class FixturesFileNotFound < StandardError; end # # Any fixture labeled "DEFAULTS" is safely ignored. -class Fixtures - MAX_ID = 2 ** 30 - 1 +Fixture = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Fixture', 'ActiveRecord::Fixture') +Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Fixtures', 'ActiveRecord::Fixtures') - @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } +module ActiveRecord + class Fixtures + MAX_ID = 2 ** 30 - 1 - def self.find_table_name(table_name) # :nodoc: - ActiveRecord::Base.pluralize_table_names ? - table_name.to_s.singularize.camelize : - table_name.to_s.camelize - end + @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.reset_cache - @@all_cached_fixtures.clear - end + def self.find_table_name(table_name) # :nodoc: + ActiveRecord::Base.pluralize_table_names ? + table_name.to_s.singularize.camelize : + table_name.to_s.camelize + end - def self.cache_for_connection(connection) - @@all_cached_fixtures[connection] - end + def self.reset_cache + @@all_cached_fixtures.clear + end - def self.fixture_is_cached?(connection, table_name) - cache_for_connection(connection)[table_name] - end + def self.cache_for_connection(connection) + @@all_cached_fixtures[connection] + end - def self.cached_fixtures(connection, keys_to_fetch = nil) - if keys_to_fetch - cache_for_connection(connection).values_at(*keys_to_fetch) - else - cache_for_connection(connection).values + def self.fixture_is_cached?(connection, table_name) + cache_for_connection(connection)[table_name] end - end - def self.cache_fixtures(connection, fixtures_map) - cache_for_connection(connection).update(fixtures_map) - end + def self.cached_fixtures(connection, keys_to_fetch = nil) + if keys_to_fetch + cache_for_connection(connection).values_at(*keys_to_fetch) + else + cache_for_connection(connection).values + end + end + + def self.cache_fixtures(connection, fixtures_map) + cache_for_connection(connection).update(fixtures_map) + end - def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true) - if load_instances - fixtures.each do |name, fixture| - begin - object.instance_variable_set "@#{name}", fixture.find - rescue FixtureClassNotFound - nil + def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true) + if load_instances + fixtures.each do |name, fixture| + begin + object.instance_variable_set "@#{name}", fixture.find + rescue FixtureClassNotFound + nil + end end end end - end - def self.instantiate_all_loaded_fixtures(object, load_instances = true) - all_loaded_fixtures.each do |table_name, fixtures| - Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances) + 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) + end end - end - cattr_accessor :all_loaded_fixtures - self.all_loaded_fixtures = {} + cattr_accessor :all_loaded_fixtures + self.all_loaded_fixtures = {} - def self.create_fixtures(fixtures_directory, table_names, class_names = {}) - table_names = [table_names].flatten.map { |n| n.to_s } - table_names.each { |n| - class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') - } + def self.create_fixtures(fixtures_directory, table_names, class_names = {}) + table_names = [table_names].flatten.map { |n| n.to_s } + table_names.each { |n| + class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') + } - # FIXME: Apparently JK uses this. - connection = block_given? ? yield : ActiveRecord::Base.connection + # 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 = table_names.reject { |table_name| + fixture_is_cached?(connection, table_name) + } - unless files_to_read.empty? - connection.disable_referential_integrity do - fixtures_map = {} + unless files_to_read.empty? + connection.disable_referential_integrity do + fixtures_map = {} - fixture_files = files_to_read.map do |path| - table_name = path.tr '/', '_' + fixture_files = files_to_read.map do |path| + table_name = path.tr '/', '_' - fixtures_map[path] = Fixtures.new( - connection, - table_name, - class_names[table_name.to_sym] || table_name.classify, - File.join(fixtures_directory, path)) - end + fixtures_map[path] = ActiveRecord::Fixtures.new( + connection, + table_name, + class_names[table_name.to_sym] || table_name.classify, + File.join(fixtures_directory, path)) + end - all_loaded_fixtures.update(fixtures_map) + 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 + 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 - table_rows.keys.each do |table| - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' - end + 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| - rows.each do |row| - conn.insert_fixture(row, table_name) + table_rows.each do |table_name,rows| + rows.each do |row| + conn.insert_fixture(row, table_name) + end end end - end - # Cap primary key sequences to max(pk). - if connection.respond_to?(:reset_pk_sequence!) - table_names.each do |table_name| - connection.reset_pk_sequence!(table_name.tr('/', '_')) + # Cap primary key sequences to max(pk). + if connection.respond_to?(:reset_pk_sequence!) + table_names.each do |table_name| + connection.reset_pk_sequence!(table_name.tr('/', '_')) + end end end - end - cache_fixtures(connection, fixtures_map) + cache_fixtures(connection, fixtures_map) + end end + cached_fixtures(connection, table_names) end - cached_fixtures(connection, table_names) - end - - # Returns a consistent, platform-independent identifier for +label+. - # Identifiers are positive integers less than 2^32. - def self.identify(label) - Zlib.crc32(label.to_s) % MAX_ID - end - attr_reader :table_name, :name, :fixtures, :model_class - - def initialize(connection, table_name, class_name, fixture_path) - @connection = connection - @table_name = table_name - @fixture_path = fixture_path - @name = table_name # preserve fixture base name - @class_name = class_name - - @fixtures = ActiveSupport::OrderedHash.new - @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" - - # Should be an AR::Base type class - if class_name.is_a?(Class) - @table_name = class_name.table_name - @connection = class_name.connection - @model_class = class_name - else - @model_class = class_name.constantize rescue nil + # Returns a consistent, platform-independent identifier for +label+. + # Identifiers are positive integers less than 2^32. + def self.identify(label) + Zlib.crc32(label.to_s) % MAX_ID end - read_fixture_files - end + attr_reader :table_name, :name, :fixtures, :model_class - def [](x) - fixtures[x] - end + def initialize(connection, table_name, class_name, fixture_path) + @connection = connection + @table_name = table_name + @fixture_path = fixture_path + @name = table_name # preserve fixture base name + @class_name = class_name - def []=(k,v) - fixtures[k] = v - end + @fixtures = ActiveSupport::OrderedHash.new + @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" - def each(&block) - fixtures.each(&block) - end + # Should be an AR::Base type class + if class_name.is_a?(Class) + @table_name = class_name.table_name + @connection = class_name.connection + @model_class = class_name + else + @model_class = class_name.constantize rescue nil + end - def size - fixtures.size - end + read_fixture_files + end - # Return a hash of rows to be inserted. The key is the table, the value is - # a list of rows to insert to that table. - def table_rows - now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now - now = now.to_s(:db) + def [](x) + fixtures[x] + end - # allow a standard key to be used for doing defaults in YAML - fixtures.delete('DEFAULTS') + def []=(k,v) + fixtures[k] = v + end - # track any join tables we need to insert later - rows = Hash.new { |h,table| h[table] = [] } + def each(&block) + fixtures.each(&block) + end - rows[table_name] = fixtures.map do |label, fixture| - row = fixture.to_hash + def size + fixtures.size + end - if model_class && model_class < ActiveRecord::Base - # 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) - end - end + # Return a hash of rows to be inserted. The key is the table, the value is + # a list of rows to insert to that table. + def table_rows + now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now + now = now.to_s(:db) - # interpolate the fixture label - row.each do |key, value| - row[key] = label if value == "$LABEL" - end + # allow a standard key to be used for doing defaults in YAML + fixtures.delete('DEFAULTS') - # generate a primary key if necessary - if has_primary_key_column? && !row.include?(primary_key_name) - row[primary_key_name] = Fixtures.identify(label) - end + # track any join tables we need to insert later + rows = Hash.new { |h,table| h[table] = [] } + + rows[table_name] = fixtures.map do |label, fixture| + row = fixture.to_hash - # If STI is used, find the correct subclass for association reflection - reflection_class = - if row.include?(inheritance_column_name) - row[inheritance_column_name].constantize rescue model_class - else - model_class + if model_class && model_class < ActiveRecord::Base + # 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) + end end - reflection_class.reflect_on_all_associations.each do |association| - case association.macro - when :belongs_to - # Do not replace association name with association foreign key if they are named the same - fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s + # interpolate the fixture label + row.each do |key, value| + row[key] = label if value == "$LABEL" + end - if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") - # support polymorphic belongs_to as "label (Type)" - row[association.foreign_type] = $1 - end + # generate a primary key if necessary + if has_primary_key_column? && !row.include?(primary_key_name) + row[primary_key_name] = ActiveRecord::Fixtures.identify(label) + end - row[fk_name] = Fixtures.identify(value) + # If STI is used, find the correct subclass for association reflection + reflection_class = + if row.include?(inheritance_column_name) + row[inheritance_column_name].constantize rescue model_class + else + model_class end - when :has_and_belongs_to_many - if (targets = row.delete(association.name.to_s)) - targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - table_name = association.options[:join_table] - rows[table_name].concat targets.map { |target| - { association.foreign_key => row[primary_key_name], - association.association_foreign_key => Fixtures.identify(target) } - } + + reflection_class.reflect_on_all_associations.each do |association| + case association.macro + when :belongs_to + # Do not replace association name with association foreign key if they are named the same + fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s + + if association.name.to_s != fk_name && value = row.delete(association.name.to_s) + if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + # support polymorphic belongs_to as "label (Type)" + row[association.foreign_type] = $1 + end + + row[fk_name] = ActiveRecord::Fixtures.identify(value) + end + when :has_and_belongs_to_many + if (targets = row.delete(association.name.to_s)) + targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) + table_name = association.options[:join_table] + rows[table_name].concat targets.map { |target| + { association.foreign_key => row[primary_key_name], + association.association_foreign_key => ActiveRecord::Fixtures.identify(target) } + } + end end end end - end - - row - end - rows - end - private - def primary_key_name - @primary_key_name ||= model_class && model_class.primary_key + row + end + rows end - def has_primary_key_column? - @has_primary_key_column ||= primary_key_name && - model_class.columns.any? { |c| c.name == primary_key_name } - end + private + def primary_key_name + @primary_key_name ||= model_class && model_class.primary_key + end - def timestamp_column_names - @timestamp_column_names ||= - %w(created_at created_on updated_at updated_on) & column_names - end + def has_primary_key_column? + @has_primary_key_column ||= primary_key_name && + model_class.columns.any? { |c| c.name == primary_key_name } + end - def inheritance_column_name - @inheritance_column_name ||= model_class && model_class.inheritance_column - end + def timestamp_column_names + @timestamp_column_names ||= + %w(created_at created_on updated_at updated_on) & column_names + end - def column_names - @column_names ||= @connection.columns(@table_name).collect { |c| c.name } - end + def inheritance_column_name + @inheritance_column_name ||= model_class && model_class.inheritance_column + end - def read_fixture_files - if File.file?(yaml_file_path) - read_yaml_fixture_files - elsif File.file?(csv_file_path) - read_csv_fixture_files - else - raise FixturesFileNotFound, "Could not find #{yaml_file_path} or #{csv_file_path}" + def column_names + @column_names ||= @connection.columns(@table_name).collect { |c| c.name } end - end - def read_yaml_fixture_files - yaml_string = (Dir["#{@fixture_path}/**/*.yml"].select { |f| - File.file?(f) - } + [yaml_file_path]).map { |file_path| IO.read(file_path) }.join - - if yaml = parse_yaml_string(yaml_string) - # If the file is an ordered map, extract its children. - yaml_value = - if yaml.respond_to?(:type_id) && yaml.respond_to?(:value) - yaml.value - else - [yaml] - end + def read_fixture_files + if File.file?(yaml_file_path) + read_yaml_fixture_files + elsif File.file?(csv_file_path) + read_csv_fixture_files + else + raise FixturesFileNotFound, "Could not find #{yaml_file_path} or #{csv_file_path}" + end + end - yaml_value.each do |fixture| - raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{fixture}" unless fixture.respond_to?(:each) - fixture.each do |name, data| - unless data - raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)" + def read_yaml_fixture_files + yaml_string = (Dir["#{@fixture_path}/**/*.yml"].select { |f| + File.file?(f) + } + [yaml_file_path]).map { |file_path| IO.read(file_path) }.join + + if yaml = parse_yaml_string(yaml_string) + # If the file is an ordered map, extract its children. + yaml_value = + if yaml.respond_to?(:type_id) && yaml.respond_to?(:value) + yaml.value + else + [yaml] end - fixtures[name] = Fixture.new(data, model_class) + yaml_value.each do |fixture| + raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{fixture}" unless fixture.respond_to?(:each) + fixture.each do |name, data| + unless data + raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)" + end + + fixtures[name] = ActiveRecord::Fixture.new(data, model_class) + end end end end - end - def read_csv_fixture_files - reader = CSV.parse(erb_render(IO.read(csv_file_path))) - header = reader.shift - i = 0 - reader.each do |row| - data = {} - row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip } - fixtures["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class) + def read_csv_fixture_files + reader = CSV.parse(erb_render(IO.read(csv_file_path))) + header = reader.shift + i = 0 + reader.each do |row| + data = {} + row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip } + fixtures["#{@class_name.to_s.underscore}_#{i+=1}"] = ActiveRecord::Fixture.new(data, model_class) + end end - end + deprecate :read_csv_fixture_files - def yaml_file_path - "#{@fixture_path}.yml" - end + def yaml_file_path + "#{@fixture_path}.yml" + end - def csv_file_path - @fixture_path + ".csv" - end + def csv_file_path + @fixture_path + ".csv" + end - def yaml_fixtures_key(path) - File.basename(@fixture_path).split(".").first - end + def yaml_fixtures_key(path) + File.basename(@fixture_path).split(".").first + end - def parse_yaml_string(fixture_content) - YAML::load(erb_render(fixture_content)) - rescue => error - raise Fixture::FormatError, "a YAML error occurred parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}" - end + def parse_yaml_string(fixture_content) + YAML::load(erb_render(fixture_content)) + rescue => error + raise Fixture::FormatError, "a YAML error occurred parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}" + end - def erb_render(fixture_content) - ERB.new(fixture_content).result - end -end + def erb_render(fixture_content) + ERB.new(fixture_content).result + end + end -class Fixture #:nodoc: - include Enumerable + class Fixture #:nodoc: + include Enumerable - class FixtureError < StandardError #:nodoc: - end + class FixtureError < StandardError #:nodoc: + end - class FormatError < FixtureError #:nodoc: - end + class FormatError < FixtureError #:nodoc: + end - attr_reader :model_class, :fixture + attr_reader :model_class, :fixture - def initialize(fixture, model_class) - @fixture = fixture - @model_class = model_class - end + def initialize(fixture, model_class) + @fixture = fixture + @model_class = model_class + end - def class_name - model_class.name if model_class - end + def class_name + model_class.name if model_class + end - def each - fixture.each { |item| yield item } - end + def each + fixture.each { |item| yield item } + end - def [](key) - fixture[key] - end + def [](key) + fixture[key] + end - alias :to_hash :fixture + alias :to_hash :fixture - def find - if model_class - model_class.find(fixture[model_class.primary_key]) - else - raise FixtureClassNotFound, "No class attached to find." + def find + if model_class + model_class.find(fixture[model_class.primary_key]) + else + raise FixtureClassNotFound, "No class attached to find." + end end end end @@ -832,7 +787,7 @@ module ActiveRecord self.pre_loaded_fixtures = false self.fixture_class_names = Hash.new do |h, table_name| - h[table_name] = Fixtures.find_table_name(table_name) + h[table_name] = ActiveRecord::Fixtures.find_table_name(table_name) end end @@ -944,7 +899,7 @@ module ActiveRecord ActiveRecord::Base.connection.begin_db_transaction # Load fixtures for every test. else - Fixtures.reset_cache + ActiveRecord::Fixtures.reset_cache @@already_loaded_fixtures[self.class] = nil @loaded_fixtures = load_fixtures end @@ -957,7 +912,7 @@ module ActiveRecord return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank? unless run_in_transaction? - Fixtures.reset_cache + ActiveRecord::Fixtures.reset_cache end # Rollback changes if a transaction is active. @@ -970,7 +925,7 @@ module ActiveRecord private def load_fixtures - fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) + fixtures = ActiveRecord::Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) Hash[fixtures.map { |f| [f.name, f] }] end @@ -979,16 +934,16 @@ module ActiveRecord def instantiate_fixtures if pre_loaded_fixtures - raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty? + raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::Fixtures.all_loaded_fixtures.empty? unless @@required_fixture_classes - self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys + self.class.require_fixture_classes ActiveRecord::Fixtures.all_loaded_fixtures.keys @@required_fixture_classes = true end - Fixtures.instantiate_all_loaded_fixtures(self, load_instances?) + 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| - Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?) + ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?) end end end diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb index 9eb47ad99f..b15b5a8133 100644 --- a/activerecord/lib/active_record/identity_map.rb +++ b/activerecord/lib/active_record/identity_map.rb @@ -12,10 +12,36 @@ module ActiveRecord # 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. + # 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 - extend ActiveSupport::Concern class << self def enabled=(flag) @@ -49,11 +75,11 @@ module ActiveRecord end def get(klass, primary_key) - record = repository[klass.symbolized_base_class][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})", + :line => "From Identity Map (id: #{primary_key})", :name => "#{klass} Loaded", :connection_id => object_id) @@ -64,15 +90,15 @@ module ActiveRecord end def add(record) - repository[record.class.symbolized_base_class][record.id] = record + repository[record.class.symbolized_sti_name][record.id] = record end def remove(record) - repository[record.class.symbolized_base_class].delete(record.id) + repository[record.class.symbolized_sti_name].delete(record.id) end - def remove_by_id(symbolized_base_class, id) - repository[symbolized_base_class].delete(id) + def remove_by_id(symbolized_sti_name, id) + repository[symbolized_sti_name].delete(id) end def clear diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 9a31675782..cdedcde0eb 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -94,7 +94,7 @@ module ActiveRecord relation = self.class.unscoped stmt = relation.where( - relation.table[self.class.primary_key].eq(quoted_id).and( + 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)) diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index 0893d7e337..c723436330 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -110,8 +110,8 @@ module ActiveRecord next unless respond_to?(callback) callback_meth = :"_notify_#{observer_name}_for_#{callback}" unless klass.respond_to?(callback_meth) - klass.send(:define_method, callback_meth) do - observer.send(callback, self) + klass.send(:define_method, callback_meth) do |&block| + observer.send(callback, self, &block) end klass.send(callback, callback_meth) end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index b4531ed35f..b9041f44d8 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -146,7 +146,7 @@ module ActiveRecord # will fail and false will be returned. # # When updating model attributes, mass-assignment security protection is respected. - # If no +:as+ option is supplied then the +:default+ scope will be used. + # If no +:as+ option is supplied then the +:default+ role will be used. # If you want to bypass the protection given by +attr_protected+ and # +attr_accessible+ then you can do so using the +:without_protection+ option. def update_attributes(attributes, options = {}) diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 929998eb85..4e61671473 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -40,6 +40,7 @@ module ActiveRecord def close @target.close if @target.respond_to?(:close) ensure + ActiveRecord::Base.connection.clear_query_cache unless @original_cache_value ActiveRecord::Base.connection.disable_query_cache! end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 5703fac033..85ad43b35f 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -203,18 +203,18 @@ db_namespace = namespace :db do # only files matching "20091231235959_some_name.rb" pattern if match_data = /^(\d{14})_(.+)\.rb$/.match(file) status = db_list.delete(match_data[1]) ? 'up' : 'down' - file_list << [status, match_data[1], match_data[2]] + file_list << [status, match_data[1], match_data[2].humanize] end end + db_list.map! do |version| + ['up', version, '********** NO FILE **********'] + end # output puts "\ndatabase: #{config['database']}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - file_list.each do |file| - puts "#{file[0].center(8)} #{file[1].ljust(14)} #{file[2].humanize}" - end - db_list.each do |version| - puts "#{'up'.center(8)} #{version.ljust(14)} *** NO FILE ***" + (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration| + puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}" end puts end @@ -305,7 +305,7 @@ db_namespace = namespace :db do fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.{yml,csv}"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| - Fixtures.create_fixtures(fixtures_dir, fixture_file) + ActiveRecord::Fixtures.create_fixtures(fixtures_dir, fixture_file) end end @@ -316,13 +316,13 @@ db_namespace = namespace :db do label, id = ENV['LABEL'], ENV['ID'] raise 'LABEL or ID required' if label.blank? && id.blank? - puts %Q(The fixture ID for "#{label}" is #{Fixtures.identify(label)}.) if label + puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::Fixtures.identify(label)}.) if label base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) data.keys.each do |key| - key_id = Fixtures.identify(key) + key_id = ActiveRecord::Fixtures.identify(key) if key == label || key_id == id.to_i puts "#{file}: #{key} (#{key_id})" diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 869eebfa34..0fcae92d51 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -146,7 +146,7 @@ module ActiveRecord if options.except(:distinct).present? apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct]) else - if eager_loading? || includes_values.present? + 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) @@ -161,21 +161,20 @@ module ActiveRecord def perform_calculation(operation, column_name, options = {}) operation = operation.to_s.downcase - distinct = nil + distinct = options[:distinct] if operation == "count" column_name ||= (select_for_count || :all) unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true - column_name = primary_key if column_name == :all end + column_name = primary_key if column_name == :all && distinct + distinct = nil if column_name =~ /\s*DISTINCT\s+/i end - distinct = options[:distinct] || distinct - if @group_values.any? execute_grouped_calculation(operation, column_name, distinct) else @@ -197,7 +196,7 @@ module ActiveRecord def execute_simple_calculation(operation, column_name, distinct) #:nodoc: # Postgresql doesn't like ORDER BY when there are no GROUP BY - relation = except(:order) + relation = reorder(nil) if operation == "count" && (relation.limit_value || relation.offset_value) # Shortcut when limit is zero. diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 57c9921ea8..32d1cff6c3 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -375,7 +375,12 @@ module ActiveRecord if loaded? @records.last else - @last ||= reverse_order.limit(1).to_a[0] + @last ||= + if offset_value || limit_value + to_a.last + else + reverse_order.limit(1).to_a[0] + end end end diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index 7e77aefb21..c3e976002e 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -40,7 +40,7 @@ module ActiveRecord # You must implement these methods: # # self.find_by_session_id(session_id) - # initialize(hash_of_session_id_and_data) + # initialize(hash_of_session_id_and_data, options_hash = {}) # attr_reader :session_id # attr_accessor :data # save @@ -83,6 +83,8 @@ module ActiveRecord cattr_accessor :data_column_name self.data_column_name = 'data' + attr_accessible :session_id, :data, :marshaled_data + before_save :marshal_data! before_save :raise_on_session_data_overflow! @@ -123,7 +125,7 @@ module ActiveRecord end end - def initialize(attributes = nil) + def initialize(attributes = nil, options = {}) @data = nil super end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 29efbbcb8c..0d47eb3338 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -13,6 +13,13 @@ module ActiveRecord ActiveRecord::IdentityMap.clear end + # Backport skip to Ruby 1.8. test/unit doesn't support it, so just + # make it a noop. + unless instance_methods.map(&:to_s).include?("skip") + def skip(message) + end + 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/validations.rb b/activerecord/lib/active_record/validations.rb index de36dd20b3..59b6876135 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -32,11 +32,11 @@ module ActiveRecord module ClassMethods # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+ # so an exception is raised if the record is invalid. - def create!(attributes = nil, &block) + def create!(attributes = nil, options = {}, &block) if attributes.is_a?(Array) - attributes.collect { |attr| create!(attr, &block) } + attributes.collect { |attr| create!(attr, options, &block) } else - object = new(attributes) + object = new(attributes, options) yield(object) if block_given? object.save! object diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 0667be7d23..2c20dd997f 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -3,7 +3,7 @@ module ActiveRecord MAJOR = 3 MINOR = 1 TINY = 0 - PRE = "beta" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 43015098c9..292c7efebb 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -138,7 +138,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase private # custom fixture loader, uses Fixtures#create_fixtures and appends base_path to the current file's path def create_test_fixtures(*fixture_names) - Fixtures.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) + ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) end # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name diff --git a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb new file mode 100644 index 0000000000..cd9c1041dc --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb @@ -0,0 +1,50 @@ +require "cases/helper" +require 'models/topic' + +module ActiveRecord + module ConnectionAdapters + class Mysql2Adapter + class BindParameterTest < ActiveRecord::TestCase + fixtures :topics + + def test_update_question_marks + str = "foo?bar" + x = Topic.find :first + x.title = str + x.content = str + x.save! + x.reload + assert_equal str, x.title + assert_equal str, x.content + end + + def test_create_question_marks + str = "foo?bar" + x = Topic.create!(:title => str, :content => str) + x.reload + assert_equal str, x.title + assert_equal str, x.content + end + + def test_update_null_bytes + str = "foo\0bar" + x = Topic.find :first + x.title = str + x.content = str + x.save! + x.reload + assert_equal str, x.title + assert_equal str, x.content + end + + def test_create_null_bytes + str = "foo\0bar" + x = Topic.create!(:title => str, :content => str) + x.reload + assert_equal str, x.title + assert_equal str, x.content + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 1efa7deaeb..752b864818 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -138,7 +138,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase private # custom fixture loader, uses Fixtures#create_fixtures and appends base_path to the current file's path def create_test_fixtures(*fixture_names) - Fixtures.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) + ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) end # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 39e8a7960a..49d8722aff 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -51,7 +51,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors]) assert_nothing_raised do - assert_equal 3, categories.count + assert_equal 4, categories.count + assert_equal 4, categories.all.count + assert_equal 3, categories.count(:distinct => true) assert_equal 3, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes 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 007f11b535..b149f5912f 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -66,6 +66,63 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 'exotic', bulb.name end + def test_create_from_association_with_nil_values_should_work + car = Car.create(:name => 'honda') + + bulb = car.bulbs.new(nil) + assert_equal 'defaulty', bulb.name + + bulb = car.bulbs.build(nil) + assert_equal 'defaulty', bulb.name + + bulb = car.bulbs.create(nil) + assert_equal 'defaulty', bulb.name + end + + def test_association_keys_bypass_attribute_protection + car = Car.create(:name => 'honda') + + bulb = car.bulbs.new + assert_equal car.id, bulb.car_id + + bulb = car.bulbs.new :car_id => car.id + 1 + assert_equal car.id, bulb.car_id + + bulb = car.bulbs.build + assert_equal car.id, bulb.car_id + + bulb = car.bulbs.build :car_id => car.id + 1 + assert_equal car.id, bulb.car_id + + bulb = car.bulbs.create + assert_equal car.id, bulb.car_id + + bulb = car.bulbs.create :car_id => car.id + 1 + assert_equal car.id, bulb.car_id + end + + def test_association_conditions_bypass_attribute_protection + car = Car.create(:name => 'honda') + + bulb = car.frickinawesome_bulbs.new + assert_equal true, bulb.frickinawesome? + + bulb = car.frickinawesome_bulbs.new(:frickinawesome => false) + assert_equal true, bulb.frickinawesome? + + bulb = car.frickinawesome_bulbs.build + assert_equal true, bulb.frickinawesome? + + bulb = car.frickinawesome_bulbs.build(:frickinawesome => false) + assert_equal true, bulb.frickinawesome? + + bulb = car.frickinawesome_bulbs.create + assert_equal true, bulb.frickinawesome? + + bulb = car.frickinawesome_bulbs.create(:frickinawesome => false) + assert_equal true, bulb.frickinawesome? + end + # When creating objects on the association, we must not do it within a scope (even though it # would be convenient), because this would cause that scope to be applied to any callbacks etc. def test_build_and_create_should_not_happen_within_scope @@ -605,6 +662,30 @@ class HasManyAssociationsTest < ActiveRecord::TestCase 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_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) diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index f3c96ccbe6..356a4a7a09 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -4,6 +4,7 @@ require 'models/project' require 'models/company' require 'models/ship' require 'models/pirate' +require 'models/car' require 'models/bulb' class HasOneAssociationsTest < ActiveRecord::TestCase @@ -95,6 +96,15 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nil Account.find(old_account_id).firm_id end + def test_natural_assignment_to_nil_after_destroy + firm = companies(:rails_core) + old_account_id = firm.account.id + firm.account.destroy + firm.account = nil + assert_nil companies(:rails_core).account + assert_raise(ActiveRecord::RecordNotFound) { Account.find(old_account_id) } + end + def test_association_change_calls_delete companies(:first_firm).deletable_account = Account.new(:credit_limit => 5) assert_equal [], Account.destroyed_account_ids[companies(:first_firm).id] @@ -359,4 +369,45 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal pirate.id, ships(:black_pearl).reload.pirate_id assert_nil new_ship.pirate_id end + + def test_deprecated_association_loaded + firm = companies(:first_firm) + firm.association(:account).stubs(:loaded?).returns(stub) + + assert_deprecated do + assert_equal firm.association(:account).loaded?, firm.account_loaded? + end + end + + def test_association_keys_bypass_attribute_protection + car = Car.create(:name => 'honda') + + bulb = car.build_bulb + assert_equal car.id, bulb.car_id + + bulb = car.build_bulb :car_id => car.id + 1 + assert_equal car.id, bulb.car_id + + bulb = car.create_bulb + assert_equal car.id, bulb.car_id + + bulb = car.create_bulb :car_id => car.id + 1 + assert_equal car.id, bulb.car_id + end + + def test_association_conditions_bypass_attribute_protection + car = Car.create(:name => 'honda') + + bulb = car.build_frickinawesome_bulb + assert_equal true, bulb.frickinawesome? + + bulb = car.build_frickinawesome_bulb(:frickinawesome => false) + assert_equal true, bulb.frickinawesome? + + bulb = car.create_frickinawesome_bulb + assert_equal true, bulb.frickinawesome? + + bulb = car.create_frickinawesome_bulb(:frickinawesome => false) + assert_equal true, bulb.frickinawesome? + end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 968025ade8..2503349c08 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -310,4 +310,8 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal dashboard, minivan.dashboard assert_equal dashboard, minivan.speedometer.dashboard end + + def test_has_one_through_with_custom_select_on_join_model_default_scope + assert_equal clubs(:boring_club), members(:groucho).selected_club + 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 e2228228a3..55d9a328a7 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/post' require 'models/comment' require 'models/author' +require 'models/essay' require 'models/category' require 'models/categorization' require 'models/person' @@ -34,6 +35,17 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase assert_no_match(/JOIN/i, sql) end + def test_join_conditions_added_to_join_clause + sql = Author.joins(:essays).to_sql + assert_match(/writer_type.*?=.*?Author/i, sql) + assert_no_match(/WHERE/i, sql) + end + + def test_join_conditions_allow_nil_associations + authors = Author.includes(:essays).where(:essays => {:id => nil}) + assert_equal 2, authors.count + end + def test_find_with_implicit_inner_joins_honors_readonly_without_select authors = Author.joins(:posts).to_a assert !authors.empty?, "expected authors to be non-empty" diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 04f628a398..49d82ba2df 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -85,7 +85,7 @@ class AssociationsTest < ActiveRecord::TestCase def test_should_construct_new_finder_sql_after_create person = Person.new :first_name => 'clark' - assert_equal [], person.readers.find(:all) + assert_equal [], person.readers.all person.save! reader = Reader.create! :person => person, :post => Post.new(:title => "foo", :body => "bar") assert person.readers.find(reader.id) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 5ee3b2d776..9bc04ed29c 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -45,6 +45,10 @@ class ReadonlyTitlePost < Post attr_readonly :title end +class ProtectedTitlePost < Post + attr_protected :title +end + class Weird < ActiveRecord::Base; end class Boolean < ActiveRecord::Base; end @@ -105,7 +109,7 @@ class BasicsTest < ActiveRecord::TestCase def test_select_symbol topic_ids = Topic.select(:id).map(&:id).sort - assert_equal Topic.find(:all).map(&:id).sort, topic_ids + assert_equal Topic.all.map(&:id).sort, topic_ids end def test_table_exists @@ -491,8 +495,9 @@ class BasicsTest < ActiveRecord::TestCase def test_attributes_guard_protected_attributes_is_deprecated attributes = { "title" => "An amazing title" } - topic = Topic.new - assert_deprecated { topic.send(:attributes=, attributes, false) } + post = ProtectedTitlePost.new + assert_deprecated { post.send(:attributes=, attributes, false) } + assert_equal "An amazing title", post.title end def test_multiparameter_attributes_on_date @@ -575,6 +580,29 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on end + def test_multiparameter_attributes_on_time_with_no_date + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + attributes = { + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + end + assert_equal("written_on", ex.errors[0].attribute) + end + + def test_multiparameter_attributes_on_time_with_invalid_time_params + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "2004", "written_on(5i)" => "36", "written_on(6i)" => "64", + } + topic = Topic.find(1) + topic.attributes = attributes + end + assert_equal("written_on", ex.errors[0].attribute) + end + def test_multiparameter_attributes_on_time_with_old_date attributes = { "written_on(1i)" => "1850", "written_on(2i)" => "6", "written_on(3i)" => "24", @@ -586,6 +614,82 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "1850-06-24 16:24:00", topic.written_on.to_s(:db) end + def test_multiparameter_attributes_on_time_will_raise_on_big_time_if_missing_date_parts + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + attributes = { + "written_on(4i)" => "16", "written_on(5i)" => "24" + } + topic = Topic.find(1) + topic.attributes = attributes + end + assert_equal("written_on", ex.errors[0].attribute) + end + + def test_multiparameter_attributes_on_time_with_raise_on_small_time_if_missing_date_parts + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + attributes = { + "written_on(4i)" => "16", "written_on(5i)" => "12", "written_on(6i)" => "02" + } + topic = Topic.find(1) + topic.attributes = attributes + end + assert_equal("written_on", ex.errors[0].attribute) + end + + def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", + "written_on(5i)" => "12", "written_on(6i)" => "02" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on + end + + def test_multiparameter_attributes_on_time_will_ignore_hour_if_blank + attributes = { + "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", + "written_on(4i)" => "", "written_on(5i)" => "12", "written_on(6i)" => "02" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal 1, topic.written_on.year + assert_equal 1, topic.written_on.month + assert_equal 1, topic.written_on.day + assert_equal 0, topic.written_on.hour + assert_equal 12, topic.written_on.min + assert_equal 2, topic.written_on.sec + end + + def test_multiparameter_attributes_on_time_will_ignore_date_if_empty + attributes = { + "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", + "written_on(4i)" => "16", "written_on(5i)" => "24" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal 1, topic.written_on.year + assert_equal 1, topic.written_on.month + assert_equal 1, topic.written_on.day + assert_equal 16, topic.written_on.hour + assert_equal 24, topic.written_on.min + assert_equal 0, topic.written_on.sec + end + def test_multiparameter_attributes_on_time_with_seconds_will_ignore_date_if_empty + attributes = { + "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", + "written_on(4i)" => "16", "written_on(5i)" => "12", "written_on(6i)" => "02" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal 1, topic.written_on.year + assert_equal 1, topic.written_on.month + assert_equal 1, topic.written_on.day + assert_equal 16, topic.written_on.hour + assert_equal 12, topic.written_on.min + assert_equal 02, topic.written_on.sec + end + def test_multiparameter_attributes_on_time_with_utc ActiveRecord::Base.default_timezone = :utc attributes = { @@ -692,6 +796,42 @@ class BasicsTest < ActiveRecord::TestCase assert_equal address, customer.address end + def test_multiparameter_assignment_of_aggregation_out_of_order + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(3)" => address.country, "address(2)" => address.city, "address(1)" => address.street } + customer.attributes = attributes + assert_equal address, customer.address + end + + def test_multiparameter_assignment_of_aggregation_with_missing_values + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(2)" => address.city, "address(3)" => address.country } + customer.attributes = attributes + end + assert_equal("address", ex.errors[0].attribute) + end + + def test_multiparameter_assignment_of_aggregation_with_blank_values + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(1)" => "", "address(2)" => address.city, "address(3)" => address.country } + customer.attributes = attributes + assert_equal Address.new(nil, "The City", "The Country"), customer.address + end + + def test_multiparameter_assignment_of_aggregation_with_large_index + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(1)" => "The Street", "address(2)" => address.city, "address(3000)" => address.country } + customer.attributes = attributes + end + assert_equal("address", ex.errors[0].attribute) + end + def test_attributes_on_dummy_time # Oracle, and Sybase do not have a TIME datatype. return true if current_adapter?(:OracleAdapter, :SybaseAdapter) diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 654c4c9010..56f6d795b6 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -319,6 +319,17 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 4, Account.count(:distinct => true, :include => :firm, :select => :credit_limit) end + def test_should_not_perform_joined_include_by_default + assert_equal Account.count, Account.includes(:firm).count + queries = assert_sql { Account.includes(:firm).count } + assert_no_match(/join/i, queries.last) + end + + def test_should_perform_joined_include_when_referencing_included_tables + joined_count = Account.includes(:firm).where(:companies => {:name => '37signals'}).count + assert_equal 1, joined_count + end + def test_should_count_scoped_select Account.update_all("credit_limit = NULL") assert_equal 0, Account.scoped(:select => "credit_limit").count diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index be4ba18555..4e75eafe3d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -683,6 +683,27 @@ class FinderTest < ActiveRecord::TestCase 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 diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 3e20155210..b0bd9c5763 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -36,7 +36,7 @@ class FixturesTest < ActiveRecord::TestCase FIXTURES.each do |name| fixtures = nil assert_nothing_raised { fixtures = create_fixtures(name).first } - assert_kind_of(Fixtures, fixtures) + assert_kind_of(ActiveRecord::Fixtures, fixtures) fixtures.each { |_name, fixture| fixture.each { |key, value| assert_match(MATCH_ATTRIBUTE_NAME, key) @@ -46,7 +46,7 @@ class FixturesTest < ActiveRecord::TestCase end def test_create_fixtures - Fixtures.create_fixtures(FIXTURES_ROOT, "parrots") + ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, "parrots") assert Parrot.find_by_name('Curious George'), 'George is in the database' end @@ -54,7 +54,7 @@ class FixturesTest < ActiveRecord::TestCase fixtures_array = nil assert_nothing_raised { fixtures_array = create_fixtures(*FIXTURES) } assert_kind_of(Array, fixtures_array) - fixtures_array.each { |fixtures| assert_kind_of(Fixtures, fixtures) } + fixtures_array.each { |fixtures| assert_kind_of(ActiveRecord::Fixtures, fixtures) } end def test_attributes @@ -75,7 +75,7 @@ class FixturesTest < ActiveRecord::TestCase if ActiveRecord::Base.connection.supports_migrations? def test_inserts_with_pre_and_suffix # Reset cache to make finds on the new table work - Fixtures.reset_cache + ActiveRecord::Fixtures.reset_cache ActiveRecord::Base.connection.create_table :prefix_topics_suffix do |t| t.column :title, :string @@ -150,11 +150,11 @@ class FixturesTest < ActiveRecord::TestCase end def test_empty_yaml_fixture - assert_not_nil Fixtures.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/yml/accounts") + assert_not_nil ActiveRecord::Fixtures.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/yml/accounts") end def test_empty_yaml_fixture_with_a_comment_in_it - assert_not_nil Fixtures.new( Account.connection, "companies", 'Company', FIXTURES_ROOT + "/naked/yml/companies") + assert_not_nil ActiveRecord::Fixtures.new( Account.connection, "companies", 'Company', FIXTURES_ROOT + "/naked/yml/companies") end def test_nonexistent_fixture_file @@ -164,23 +164,25 @@ class FixturesTest < ActiveRecord::TestCase assert Dir[nonexistent_fixture_path+"*"].empty? assert_raise(FixturesFileNotFound) do - Fixtures.new( Account.connection, "companies", 'Company', nonexistent_fixture_path) + ActiveRecord::Fixtures.new( Account.connection, "companies", 'Company', nonexistent_fixture_path) end end def test_dirty_dirty_yaml_file - assert_raise(Fixture::FormatError) do - Fixtures.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses") + assert_raise(ActiveRecord::Fixture::FormatError) do + ActiveRecord::Fixtures.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses") end end def test_empty_csv_fixtures - assert_not_nil Fixtures.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/csv/accounts") + assert_deprecated do + assert_not_nil ActiveRecord::Fixtures.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/csv/accounts") + end end def test_omap_fixtures assert_nothing_raised do - fixtures = Fixtures.new(Account.connection, 'categories', 'Category', FIXTURES_ROOT + "/categories_ordered") + fixtures = ActiveRecord::Fixtures.new(Account.connection, 'categories', 'Category', FIXTURES_ROOT + "/categories_ordered") i = 0 fixtures.each do |name, fixture| @@ -220,7 +222,7 @@ if Account.connection.respond_to?(:reset_pk_sequence!) def setup @instances = [Account.new(:credit_limit => 50), Company.new(:name => 'RoR Consulting')] - Fixtures.reset_cache # make sure tables get reinitialized + ActiveRecord::Fixtures.reset_cache # make sure tables get reinitialized end def test_resets_to_min_pk_with_specified_pk_and_sequence @@ -524,13 +526,13 @@ class FasterFixturesTest < ActiveRecord::TestCase def load_extra_fixture(name) fixture = create_fixtures(name).first - assert fixture.is_a?(Fixtures) + assert fixture.is_a?(ActiveRecord::Fixtures) @loaded_fixtures[fixture.table_name] = fixture end def test_cache - assert Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'categories') - assert Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'authors') + assert ActiveRecord::Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'categories') + assert ActiveRecord::Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'authors') assert_no_queries do create_fixtures('categories') @@ -538,7 +540,7 @@ class FasterFixturesTest < ActiveRecord::TestCase end load_extra_fixture('posts') - assert Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'posts') + assert ActiveRecord::Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'posts') self.class.setup_fixture_accessors('posts') assert_equal 'Welcome to the weblog', posts(:welcome).title end @@ -548,17 +550,17 @@ class FoxyFixturesTest < ActiveRecord::TestCase fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers, :"admin/accounts", :"admin/users" def test_identifies_strings - assert_equal(Fixtures.identify("foo"), Fixtures.identify("foo")) - assert_not_equal(Fixtures.identify("foo"), Fixtures.identify("FOO")) + assert_equal(ActiveRecord::Fixtures.identify("foo"), ActiveRecord::Fixtures.identify("foo")) + assert_not_equal(ActiveRecord::Fixtures.identify("foo"), ActiveRecord::Fixtures.identify("FOO")) end def test_identifies_symbols - assert_equal(Fixtures.identify(:foo), Fixtures.identify(:foo)) + assert_equal(ActiveRecord::Fixtures.identify(:foo), ActiveRecord::Fixtures.identify(:foo)) end def test_identifies_consistently - assert_equal 207281424, Fixtures.identify(:ruby) - assert_equal 1066363776, Fixtures.identify(:sapphire_2) + assert_equal 207281424, ActiveRecord::Fixtures.identify(:ruby) + assert_equal 1066363776, ActiveRecord::Fixtures.identify(:sapphire_2) end TIMESTAMP_COLUMNS = %w(created_at created_on updated_at updated_on) diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index fd20f1b120..fbb4ee6f7b 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -104,7 +104,7 @@ class ActiveSupport::TestCase self.use_transactional_fixtures = true def create_fixtures(*table_names, &block) - Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, table_names, fixture_class_names, &block) + ActiveRecord::Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, table_names, fixture_class_names, &block) end end diff --git a/activerecord/test/cases/identity_map_test.rb b/activerecord/test/cases/identity_map_test.rb index 649715fbb5..a0e16400d2 100644 --- a/activerecord/test/cases/identity_map_test.rb +++ b/activerecord/test/cases/identity_map_test.rb @@ -129,6 +129,41 @@ class IdentityMapTest < ActiveRecord::TestCase 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 behaviour with IM # ############################################################################## diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb index 6cd8494c9e..643e949087 100644 --- a/activerecord/test/cases/lifecycle_test.rb +++ b/activerecord/test/cases/lifecycle_test.rb @@ -107,6 +107,23 @@ class ValidatedCommentObserver < ActiveRecord::Observer end end + +class AroundTopic < Topic +end + +class AroundTopicObserver < ActiveRecord::Observer + observe :around_topic + def topic_ids + @topic_ids ||= [] + end + + def around_save(topic) + topic_ids << topic.id + yield(topic) + topic_ids << topic.id + end +end + class LifecycleTest < ActiveRecord::TestCase fixtures :topics, :developers, :minimalistics @@ -206,6 +223,14 @@ class LifecycleTest < ActiveRecord::TestCase assert_equal developer, SalaryChecker.instance.last_saved end + test "around filter from observer should accept block" do + observer = AroundTopicObserver.instance + topic = AroundTopic.new + topic.save + assert_nil observer.topic_ids.first + assert_not_nil observer.topic_ids.last + end + def test_observer_is_called_once observer = DeveloperObserver.instance # activate observer.calls.clear diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 636a709924..61baa55027 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -5,6 +5,7 @@ require 'models/job' require 'models/reader' require 'models/legacy_thing' require 'models/reference' +require 'models/string_key_object' class LockWithoutDefault < ActiveRecord::Base; end @@ -18,7 +19,40 @@ class ReadonlyFirstNamePerson < Person end class OptimisticLockingTest < ActiveRecord::TestCase - fixtures :people, :legacy_things, :references + fixtures :people, :legacy_things, :references, :string_key_objects + + def test_non_integer_lock_existing + s1 = StringKeyObject.find("record1") + s2 = StringKeyObject.find("record1") + assert_equal 0, s1.lock_version + assert_equal 0, s2.lock_version + + s1.name = 'updated record' + s1.save! + assert_equal 1, s1.lock_version + assert_equal 0, s2.lock_version + + s2.name = 'doubly updated record' + assert_raise(ActiveRecord::StaleObjectError) { s2.save! } + end + + def test_non_integer_lock_destroy + s1 = StringKeyObject.find("record1") + s2 = StringKeyObject.find("record1") + assert_equal 0, s1.lock_version + assert_equal 0, s2.lock_version + + s1.name = 'updated record' + s1.save! + assert_equal 1, s1.lock_version + assert_equal 0, s2.lock_version + assert_raise(ActiveRecord::StaleObjectError) { s2.destroy } + + assert s1.destroy + assert s1.frozen? + assert s1.destroyed? + assert_raises(ActiveRecord::RecordNotFound) { StringKeyObject.find("record1") } + end def test_lock_existing p1 = Person.find(1) diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb index fbbae99e8b..765033852d 100644 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -87,7 +87,11 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase end end - def test_assign_attributes_uses_default_scope_when_no_scope_is_provided + def test_mass_assigning_does_not_choke_on_nil + Firm.new.assign_attributes(nil) + end + + def test_assign_attributes_uses_default_role_when_no_role_is_provided p = LoosePerson.new p.assign_attributes(attributes_hash) @@ -101,28 +105,28 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase assert_all_attributes(p) end - def test_assign_attributes_with_default_scope_and_attr_protected_attributes + def test_assign_attributes_with_default_role_and_attr_protected_attributes p = LoosePerson.new p.assign_attributes(attributes_hash, :as => :default) assert_default_attributes(p) end - def test_assign_attributes_with_admin_scope_and_attr_protected_attributes + def test_assign_attributes_with_admin_role_and_attr_protected_attributes p = LoosePerson.new p.assign_attributes(attributes_hash, :as => :admin) assert_admin_attributes(p) end - def test_assign_attributes_with_default_scope_and_attr_accessible_attributes + def test_assign_attributes_with_default_role_and_attr_accessible_attributes p = TightPerson.new p.assign_attributes(attributes_hash, :as => :default) assert_default_attributes(p) end - def test_assign_attributes_with_admin_scope_and_attr_accessible_attributes + def test_assign_attributes_with_admin_role_and_attr_accessible_attributes p = TightPerson.new p.assign_attributes(attributes_hash, :as => :admin) @@ -153,30 +157,42 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase assert_default_attributes(p, true) end - def test_new_with_admin_scope_with_attr_accessible_attributes + def test_new_with_admin_role_with_attr_accessible_attributes p = TightPerson.new(attributes_hash, :as => :admin) assert_admin_attributes(p) end - def test_new_with_admin_scope_with_attr_protected_attributes + def test_new_with_admin_role_with_attr_protected_attributes p = LoosePerson.new(attributes_hash, :as => :admin) assert_admin_attributes(p) end - def test_create_with_admin_scope_with_attr_accessible_attributes + def test_create_with_admin_role_with_attr_accessible_attributes p = TightPerson.create(attributes_hash, :as => :admin) assert_admin_attributes(p, true) end - def test_create_with_admin_scope_with_attr_protected_attributes + def test_create_with_admin_role_with_attr_protected_attributes p = LoosePerson.create(attributes_hash, :as => :admin) assert_admin_attributes(p, true) end + def test_create_with_bang_with_admin_role_with_attr_accessible_attributes + p = TightPerson.create!(attributes_hash, :as => :admin) + + assert_admin_attributes(p, true) + end + + def test_create_with_bang_with_admin_role_with_attr_protected_attributes + p = LoosePerson.create!(attributes_hash, :as => :admin) + + assert_admin_attributes(p, true) + end + def test_new_with_without_protection_with_attr_accessible_attributes p = TightPerson.new(attributes_hash, :without_protection => true) @@ -201,6 +217,18 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase assert_all_attributes(p) end + def test_create_with_bang_with_without_protection_with_attr_accessible_attributes + p = TightPerson.create!(attributes_hash, :without_protection => true) + + assert_all_attributes(p) + end + + def test_create_with_bang_with_without_protection_with_attr_protected_attributes + p = LoosePerson.create!(attributes_hash, :without_protection => true) + + assert_all_attributes(p) + end + def test_protection_against_class_attribute_writers [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, :default_timezone, :schema_format, :lock_optimistically, :record_timestamps].each do |method| @@ -230,12 +258,12 @@ class MassAssignmentSecurityHasOneRelationsTest < ActiveRecord::TestCase assert_default_attributes(best_friend) end - def test_has_one_build_with_admin_scope_with_attr_protected_attributes + def test_has_one_build_with_admin_role_with_attr_protected_attributes best_friend = @person.build_best_friend(attributes_hash, :as => :admin) assert_admin_attributes(best_friend) end - def test_has_one_build_with_admin_scope_with_attr_accessible_attributes + def test_has_one_build_with_admin_role_with_attr_accessible_attributes best_friend = @person.build_best_friend(attributes_hash, :as => :admin) assert_admin_attributes(best_friend) end @@ -257,12 +285,12 @@ class MassAssignmentSecurityHasOneRelationsTest < ActiveRecord::TestCase assert_default_attributes(best_friend, true) end - def test_has_one_create_with_admin_scope_with_attr_protected_attributes + def test_has_one_create_with_admin_role_with_attr_protected_attributes best_friend = @person.create_best_friend(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_admin_scope_with_attr_accessible_attributes + def test_has_one_create_with_admin_role_with_attr_accessible_attributes best_friend = @person.create_best_friend(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end @@ -284,12 +312,12 @@ class MassAssignmentSecurityHasOneRelationsTest < ActiveRecord::TestCase assert_default_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_admin_scope_with_attr_protected_attributes + def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_admin_scope_with_attr_accessible_attributes + def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end @@ -318,12 +346,12 @@ class MassAssignmentSecurityBelongsToRelationsTest < ActiveRecord::TestCase assert_default_attributes(best_friend) end - def test_has_one_build_with_admin_scope_with_attr_protected_attributes + def test_has_one_build_with_admin_role_with_attr_protected_attributes best_friend = @person.build_best_friend_of(attributes_hash, :as => :admin) assert_admin_attributes(best_friend) end - def test_has_one_build_with_admin_scope_with_attr_accessible_attributes + def test_has_one_build_with_admin_role_with_attr_accessible_attributes best_friend = @person.build_best_friend_of(attributes_hash, :as => :admin) assert_admin_attributes(best_friend) end @@ -345,12 +373,12 @@ class MassAssignmentSecurityBelongsToRelationsTest < ActiveRecord::TestCase assert_default_attributes(best_friend, true) end - def test_has_one_create_with_admin_scope_with_attr_protected_attributes + def test_has_one_create_with_admin_role_with_attr_protected_attributes best_friend = @person.create_best_friend_of(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_admin_scope_with_attr_accessible_attributes + def test_has_one_create_with_admin_role_with_attr_accessible_attributes best_friend = @person.create_best_friend_of(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end @@ -372,12 +400,12 @@ class MassAssignmentSecurityBelongsToRelationsTest < ActiveRecord::TestCase assert_default_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_admin_scope_with_attr_protected_attributes + def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_admin_scope_with_attr_accessible_attributes + def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes best_friend = @person.create_best_friend!(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end @@ -406,12 +434,12 @@ class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase assert_default_attributes(best_friend) end - def test_has_one_build_with_admin_scope_with_attr_protected_attributes + def test_has_one_build_with_admin_role_with_attr_protected_attributes best_friend = @person.best_friends.build(attributes_hash, :as => :admin) assert_admin_attributes(best_friend) end - def test_has_one_build_with_admin_scope_with_attr_accessible_attributes + def test_has_one_build_with_admin_role_with_attr_accessible_attributes best_friend = @person.best_friends.build(attributes_hash, :as => :admin) assert_admin_attributes(best_friend) end @@ -433,12 +461,12 @@ class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase assert_default_attributes(best_friend, true) end - def test_has_one_create_with_admin_scope_with_attr_protected_attributes + def test_has_one_create_with_admin_role_with_attr_protected_attributes best_friend = @person.best_friends.create(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_admin_scope_with_attr_accessible_attributes + def test_has_one_create_with_admin_role_with_attr_accessible_attributes best_friend = @person.best_friends.create(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end @@ -460,12 +488,12 @@ class MassAssignmentSecurityHasManyRelationsTest < ActiveRecord::TestCase assert_default_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_admin_scope_with_attr_protected_attributes + def test_has_one_create_with_bang_with_admin_role_with_attr_protected_attributes best_friend = @person.best_friends.create!(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end - def test_has_one_create_with_bang_with_admin_scope_with_attr_accessible_attributes + def test_has_one_create_with_bang_with_admin_role_with_attr_accessible_attributes best_friend = @person.best_friends.create!(attributes_hash, :as => :admin) assert_admin_attributes(best_friend, true) end diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index 7f0f007a70..a0cb5dbdc5 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -68,7 +68,7 @@ class MethodScopingTest < ActiveRecord::TestCase def test_scoped_find_all Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - assert_equal [developers(:david)], Developer.find(:all) + assert_equal [developers(:david)], Developer.all end end @@ -235,23 +235,23 @@ class MethodScopingTest < ActiveRecord::TestCase def test_immutable_scope options = { :conditions => "name = 'David'" } Developer.send(:with_scope, :find => options) do - assert_equal %w(David), Developer.find(:all).map { |d| d.name } + assert_equal %w(David), Developer.all.map(&:name) options[:conditions] = "name != 'David'" - assert_equal %w(David), Developer.find(:all).map { |d| d.name } + 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.find(:all).map { |d| d.name } + assert_equal %w(David), Developer.all.map(&:name) scope[:find][:conditions] = "name != 'David'" - assert_equal %w(David), Developer.find(:all).map { |d| d.name } + 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.find(:all).map { |d| d.name } + assert_equal %w(David), Developer.all.map(&:name) end end @@ -432,7 +432,7 @@ class NestedScopingTest < ActiveRecord::TestCase 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.find(:all).map { |d| d.name } + assert_equal %w(David), Developer.all.map(&:name) end end end @@ -487,9 +487,9 @@ class NestedScopingTest < ActiveRecord::TestCase options2 = { :conditions => "name = 'David'" } Developer.send(:with_scope, :find => options1) do Developer.send(:with_exclusive_scope, :find => options2) do - assert_equal %w(David), Developer.find(:all).map { |d| d.name } + assert_equal %w(David), Developer.all.map(&:name) options1[:conditions] = options2[:conditions] = nil - assert_equal %w(David), Developer.find(:all).map { |d| d.name } + assert_equal %w(David), Developer.all.map(&:name) end end end @@ -499,9 +499,9 @@ class NestedScopingTest < ActiveRecord::TestCase options2 = { :conditions => "salary > 10000" } Developer.send(:with_scope, :find => options1) do Developer.send(:with_scope, :find => options2) do - assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name } + assert_equal %w(Jamis), Developer.all.map(&:name) options1[:conditions] = options2[:conditions] = nil - assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name } + assert_equal %w(Jamis), Developer.all.map(&:name) end end end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 8fd1fc2577..34188e4915 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -462,7 +462,7 @@ class NamedScopeTest < ActiveRecord::TestCase [:destroy_all, :reset, :delete_all].each do |method| before = post.comments.containing_the_letter_e post.association(:comments).send(method) - assert before.object_id != post.comments.containing_the_letter_e.object_id, "AssociationCollection##{method} should reset the named scopes cache" + assert before.object_id != post.comments.containing_the_letter_e.object_id, "CollectionAssociation##{method} should reset the named scopes cache" end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index b066575af8..57d1441128 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -336,6 +336,10 @@ class PersistencesTest < ActiveRecord::TestCase assert !Topic.find(1).approved? end + def test_update_attribute_does_not_choke_on_nil + assert Topic.find(1).update_attributes(nil) + end + def test_update_attribute_for_readonly_attribute minivan = Minivan.find('m1') assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') } diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index b2e40c6b22..a61180cfaf 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -65,6 +65,17 @@ class QueryCacheTest < ActiveRecord::TestCase assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled' end + def test_cache_clear_after_close + mw = ActiveRecord::QueryCache.new lambda { |env| + Post.find(:first) + } + body = mw.call({}).last + + assert !ActiveRecord::Base.connection.query_cache.empty?, 'cache not empty' + body.close + assert ActiveRecord::Base.connection.query_cache.empty?, 'cache should be empty' + end + def test_find_queries assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) { Task.find(1); Task.find(1) } end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 864b3d4846..c215602567 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -462,4 +462,8 @@ class DefaultScopingTest < ActiveRecord::TestCase assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis)) assert_equal 10, DeveloperCalledJamis.unscoped.poor.length end + + def test_default_scope_order_ignored_by_aggregations + assert_equal DeveloperOrderedBySalary.all.count, DeveloperOrderedBySalary.count + end end diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb index cee5ddd003..669c0b7b4d 100644 --- a/activerecord/test/cases/session_store/session_test.rb +++ b/activerecord/test/cases/session_store/session_test.rb @@ -21,6 +21,12 @@ module ActiveRecord assert_equal 'sessions', Session.table_name end + def test_accessible_attributes + assert Session.accessible_attributes.include?(:session_id) + assert Session.accessible_attributes.include?(:data) + assert Session.accessible_attributes.include?(:marshaled_data) + end + def test_create_table! assert !Session.table_exists? Session.create_table! diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index ceb1452afd..22d4cac422 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -14,6 +14,32 @@ class TimestampTest < ActiveRecord::TestCase @previously_updated_at = @developer.updated_at end + def test_load_infinity_and_beyond + unless current_adapter?(:PostgreSQLAdapter) + return skip("only tested on postgresql") + end + + d = Developer.find_by_sql("select 'infinity'::timestamp as updated_at") + assert d.first.updated_at.infinite?, 'timestamp should be infinite' + + d = Developer.find_by_sql("select '-infinity'::timestamp as updated_at") + time = d.first.updated_at + assert time.infinite?, 'timestamp should be infinite' + assert_operator time, :<, 0 + end + + def test_save_infinity_and_beyond + unless current_adapter?(:PostgreSQLAdapter) + return skip("only tested on postgresql") + end + + d = Developer.create!(:name => 'aaron', :updated_at => 1.0 / 0.0) + assert_equal(1.0 / 0.0, d.updated_at) + + d = Developer.create!(:name => 'aaron', :updated_at => -1.0 / 0.0) + assert_equal(-1.0 / 0.0, d.updated_at) + end + def test_saving_a_changed_record_updates_its_timestamp @developer.name = "Jack Bauer" @developer.save! diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index a6074b23e7..756c8a32eb 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -143,10 +143,7 @@ class NilXmlSerializationTest < ActiveRecord::TestCase end def test_should_serialize_yaml - assert %r{<preferences(.*)></preferences>}.match(@xml) - attributes = $1 - assert_match %r{type="yaml"}, attributes - assert_match %r{nil="true"}, attributes + assert_match %r{<preferences nil=\"true\"></preferences>}, @xml end end diff --git a/activerecord/test/fixtures/all/people.csv b/activerecord/test/fixtures/all/people.yml index e69de29bb2..e69de29bb2 100644 --- a/activerecord/test/fixtures/all/people.csv +++ b/activerecord/test/fixtures/all/people.yml diff --git a/activerecord/test/fixtures/mateys.yml b/activerecord/test/fixtures/mateys.yml index 9ecdd4ecd5..d3690955fc 100644 --- a/activerecord/test/fixtures/mateys.yml +++ b/activerecord/test/fixtures/mateys.yml @@ -1,4 +1,4 @@ blackbeard_to_redbeard: - pirate_id: <%= Fixtures.identify(:blackbeard) %> - target_id: <%= Fixtures.identify(:redbeard) %> + pirate_id: <%= ActiveRecord::Fixtures.identify(:blackbeard) %> + target_id: <%= ActiveRecord::Fixtures.identify(:redbeard) %> weight: 10 diff --git a/activerecord/test/fixtures/memberships.yml b/activerecord/test/fixtures/memberships.yml index 60eb641054..a5d52bd438 100644 --- a/activerecord/test/fixtures/memberships.yml +++ b/activerecord/test/fixtures/memberships.yml @@ -25,3 +25,10 @@ blarpy_winkup_crazy_club: member_id: 3 favourite: false type: CurrentMembership + +selected_membership_of_boring_club: + joined_on: <%= 3.weeks.ago.to_s(:db) %> + club: boring_club + member_id: 1 + favourite: false + type: SelectedMembership diff --git a/activerecord/test/fixtures/parrots_pirates.yml b/activerecord/test/fixtures/parrots_pirates.yml index 6b17a37d68..66472243c7 100644 --- a/activerecord/test/fixtures/parrots_pirates.yml +++ b/activerecord/test/fixtures/parrots_pirates.yml @@ -1,7 +1,7 @@ george_blackbeard: - parrot_id: <%= Fixtures.identify(:george) %> - pirate_id: <%= Fixtures.identify(:blackbeard) %> + parrot_id: <%= ActiveRecord::Fixtures.identify(:george) %> + pirate_id: <%= ActiveRecord::Fixtures.identify(:blackbeard) %> louis_blackbeard: - parrot_id: <%= Fixtures.identify(:louis) %> - pirate_id: <%= Fixtures.identify(:blackbeard) %> + parrot_id: <%= ActiveRecord::Fixtures.identify(:louis) %> + pirate_id: <%= ActiveRecord::Fixtures.identify(:blackbeard) %> diff --git a/activerecord/test/fixtures/string_key_objects.yml b/activerecord/test/fixtures/string_key_objects.yml new file mode 100644 index 0000000000..fa1299915b --- /dev/null +++ b/activerecord/test/fixtures/string_key_objects.yml @@ -0,0 +1,7 @@ +first: + id: record1 + name: first record + +second: + id: record2 + name: second record diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index c68d008c26..643dcefed3 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -2,6 +2,8 @@ class Bulb < ActiveRecord::Base default_scope where(:name => 'defaulty') belongs_to :car + attr_protected :car_id, :frickinawesome + attr_reader :scope_after_initialize after_initialize :record_scope_after_initialize diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index b036f0f5c9..76f20b1061 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -2,6 +2,11 @@ class Car < ActiveRecord::Base has_many :bulbs has_many :foo_bulbs, :class_name => "Bulb", :conditions => { :name => 'foo' } + has_many :frickinawesome_bulbs, :class_name => "Bulb", :conditions => { :frickinawesome => true } + + has_one :bulb + has_one :frickinawesome_bulb, :class_name => "Bulb", :conditions => { :frickinawesome => true } + has_many :tyres has_many :engines has_many :wheels, :as => :wheelable diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 991e0e051f..11a0f4ff63 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -1,8 +1,10 @@ class Member < ActiveRecord::Base has_one :current_membership + has_one :selected_membership has_one :membership has_many :fellow_members, :through => :club, :source => :members has_one :club, :through => :current_membership + has_one :selected_club, :through => :selected_membership, :source => :club has_one :favourite_club, :through => :membership, :conditions => ["memberships.favourite = ?", true], :source => :club has_one :hairy_club, :through => :membership, :conditions => {:clubs => {:name => "Moustache and Eyebrow Fancier Club"}}, :source => :club has_one :sponsor, :as => :sponsorable diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb index 905f948c37..bcbb7e42c5 100644 --- a/activerecord/test/models/membership.rb +++ b/activerecord/test/models/membership.rb @@ -7,3 +7,9 @@ class CurrentMembership < Membership belongs_to :member belongs_to :club end + +class SelectedMembership < Membership + def self.default_scope + select("'1' as foo") + end +end diff --git a/activerecord/test/models/string_key_object.rb b/activerecord/test/models/string_key_object.rb new file mode 100644 index 0000000000..f8d4c6e0e4 --- /dev/null +++ b/activerecord/test/models/string_key_object.rb @@ -0,0 +1,3 @@ +class StringKeyObject < ActiveRecord::Base + set_primary_key :id +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 9479242e4f..c8a98f121d 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -89,6 +89,7 @@ ActiveRecord::Schema.define do create_table :bulbs, :force => true do |t| t.integer :car_id t.string :name + t.boolean :frickinawesome end create_table "CamelCase", :force => true do |t| @@ -543,6 +544,12 @@ ActiveRecord::Schema.define do t.string :sponsorable_type end + create_table :string_key_objects, :id => false, :primary_key => :id, :force => true do |t| + t.string :id + t.string :name + t.integer :lock_version, :null => false, :default => 0 + end + create_table :students, :force => true do |t| t.string :name end |