diff options
Diffstat (limited to 'activerecord')
28 files changed, 399 insertions, 87 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index a1d82fb45d..d6cc589381 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,22 @@ *Edge* +* Allow conditions on multiple tables to be specified using hash. [Pratik Naik]. Example: + + User.all :joins => :items, :conditions => { :age => 10, :items => { :color => 'black' } } + Item.first :conditions => { :items => { :color => 'red' } } + +* Always treat integer :limit as byte length. #420 [Tarmo Tänav] + +* Partial updates don't update lock_version if nothing changed. #426 [Daniel Morrison] + +* Fix column collision with named_scope and :joins. #46 [Duncan Beevers, Mark Catley] + +* db:migrate:down and :up update schema_migrations. #369 [Michael Raidel, RaceCondition] + +* PostgreSQL: support :conditions => [':foo::integer', { :foo => 1 }] without treating the ::integer typecast as a bind variable. [Tarmo Tänav] + +* MySQL: rename_column preserves column defaults. #466 [Diego Algorta] + * Add :from option to calculations. #397 [Ben Munat] * Add :validate option to associations to enable/disable the automatic validation of associated models. Resolves #301. [Jan De Poorter] diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 7a5296c458..60b17e02b9 100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -141,7 +141,7 @@ Rake::RDocTask.new { |rdoc| rdoc.title = "Active Record -- Object-relation mapping put on rails" rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' rdoc.options << '--charset' << 'utf-8' - rdoc.template = "#{ENV['template']}.rb" if ENV['template'] + rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo' rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG') rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.exclude('lib/active_record/vendor/*') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index db99b7165d..4b7154043f 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -711,7 +711,8 @@ module ActiveRecord configure_dependency_for_has_many(reflection) - add_multiple_associated_save_callbacks(reflection.name) unless options[:validate] == false + add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false + add_multiple_associated_save_callbacks(reflection.name) add_association_callbacks(reflection.name, reflection.options) if options[:through] @@ -801,7 +802,7 @@ module ActiveRecord end after_save method_name - add_single_associated_save_callbacks(reflection.name) if options[:validate] == true + add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true association_accessor_methods(reflection, HasOneAssociation) association_constructor_method(:build, reflection, HasOneAssociation) association_constructor_method(:create, reflection, HasOneAssociation) @@ -940,7 +941,7 @@ module ActiveRecord ) end - add_single_associated_save_callbacks(reflection.name) if options[:validate] == true + add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true configure_dependency_for_belongs_to(reflection) end @@ -1043,7 +1044,8 @@ module ActiveRecord def has_and_belongs_to_many(association_id, options = {}, &extension) reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) - add_multiple_associated_save_callbacks(reflection.name) unless options[:validate] == false + add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false + add_multiple_associated_save_callbacks(reflection.name) collection_accessor_methods(reflection, HasAndBelongsToManyAssociation) # Don't use a before_destroy callback since users' before_destroy @@ -1163,7 +1165,7 @@ module ActiveRecord end end - def add_single_associated_save_callbacks(association_name) + def add_single_associated_validation_callbacks(association_name) method_name = "validate_associated_records_for_#{association_name}".to_sym define_method(method_name) do association = instance_variable_get("@#{association_name}") @@ -1175,7 +1177,7 @@ module ActiveRecord validate method_name end - def add_multiple_associated_save_callbacks(association_name) + def add_multiple_associated_validation_callbacks(association_name) method_name = "validate_associated_records_for_#{association_name}".to_sym ivar = "@#{association_name}" @@ -1196,6 +1198,10 @@ module ActiveRecord end validate method_name + end + + def add_multiple_associated_save_callbacks(association_name) + ivar = "@#{association_name}" method_name = "before_save_associated_records_for_#{association_name}".to_sym define_method(method_name) do @@ -1217,7 +1223,6 @@ module ActiveRecord else [] end - records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank? # reconstruct the SQL queries now that we know the owner's id diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 11c64243a2..77fc827e11 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -85,7 +85,7 @@ module ActiveRecord end def conditions - @conditions ||= interpolate_sql(sanitize_sql(@reflection.options[:conditions])) if @reflection.options[:conditions] + @conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions end alias :sql_conditions :conditions @@ -219,6 +219,10 @@ module ActiveRecord def flatten_deeper(array) array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten end + + def owner_quoted_id + @owner.quoted_id + end end end end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 918404eac6..d516d54151 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -37,7 +37,7 @@ module ActiveRecord attributes = columns.inject({}) do |attrs, column| case column.name.to_s when @reflection.primary_key_name.to_s - attrs[column.name] = @owner.quoted_id + attrs[column.name] = owner_quoted_id when @reflection.association_foreign_key.to_s attrs[column.name] = record.quoted_id else @@ -64,7 +64,7 @@ module ActiveRecord records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } else ids = quoted_record_ids(records) - sql = "DELETE FROM #{@owner.connection.quote_table_name @reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})" + sql = "DELETE FROM #{@owner.connection.quote_table_name @reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})" @owner.connection.delete(sql) end end @@ -75,7 +75,7 @@ module ActiveRecord if @reflection.options[:finder_sql] @finder_sql = @reflection.options[:finder_sql] else - @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} " + @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} " @finder_sql << " AND (#{conditions})" if conditions end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 295beb2966..37440aa84d 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -60,7 +60,7 @@ module ActiveRecord ids = quoted_record_ids(records) @reflection.klass.update_all( "#{@reflection.primary_key_name} = NULL", - "#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})" + "#{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})" ) end end @@ -76,12 +76,12 @@ module ActiveRecord when @reflection.options[:as] @finder_sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" @finder_sql << " AND (#{conditions})" if conditions else - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}" + @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" @finder_sql << " AND (#{conditions})" if conditions end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 52ced36d16..e1bfff5923 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -107,12 +107,12 @@ module ActiveRecord # Associate attributes pointing to owner, quoted. def construct_quoted_owner_attributes(reflection) if as = reflection.options[:as] - { "#{as}_id" => @owner.quoted_id, + { "#{as}_id" => owner_quoted_id, "#{as}_type" => reflection.klass.quote_value( @owner.class.base_class.name.to_s, reflection.klass.columns_hash["#{as}_type"]) } else - { reflection.primary_key_name => @owner.quoted_id } + { reflection.primary_key_name => owner_quoted_id } end end @@ -183,7 +183,7 @@ module ActiveRecord when @reflection.options[:finder_sql] @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}" + @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" @finder_sql << " AND (#{conditions})" if conditions else @finder_sql = construct_conditions diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index c2b3503e0d..25a268e95c 100755 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -63,10 +63,10 @@ module ActiveRecord case when @reflection.options[:as] @finder_sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " + + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" else - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}" + @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" end @finder_sql << " AND (#{conditions})" if conditions end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 8fca34e524..962c2b36d9 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1479,7 +1479,7 @@ module ActiveRecord #:nodoc: def construct_finder_sql(options) scope = scope(:find) - sql = "SELECT #{options[:select] || (scope && scope[:select]) || (options[:joins] && quoted_table_name + '.*') || '*'} " + sql = "SELECT #{options[:select] || (scope && scope[:select]) || ((options[:joins] || (scope && scope[:joins])) && quoted_table_name + '.*') || '*'} " sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} " add_joins!(sql, options, scope) @@ -1999,24 +1999,28 @@ module ActiveRecord #:nodoc: # # => "age BETWEEN 13 AND 18" # { 'other_records.id' => 7 } # # => "`other_records`.`id` = 7" + # { :other_records => { :id => 7 } } + # # => "`other_records`.`id` = 7" # And for value objects on a composed_of relationship: # { :address => Address.new("123 abc st.", "chicago") } # # => "address_street='123 abc st.' and address_city='chicago'" - def sanitize_sql_hash_for_conditions(attrs) + def sanitize_sql_hash_for_conditions(attrs, table_name = quoted_table_name) attrs = expand_hash_conditions_for_aggregates(attrs) conditions = attrs.map do |attr, value| - attr = attr.to_s + unless value.is_a?(Hash) + attr = attr.to_s + + # Extract table name from qualified attribute names. + if attr.include?('.') + table_name, attr = attr.split('.', 2) + table_name = connection.quote_table_name(table_name) + end - # Extract table name from qualified attribute names. - if attr.include?('.') - table_name, attr = attr.split('.', 2) - table_name = connection.quote_table_name(table_name) + "#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}" else - table_name = quoted_table_name + sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s)) end - - "#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}" end.join(' AND ') replace_bind_variables(conditions, expand_range_bind_variables(attrs.values)) @@ -2055,9 +2059,10 @@ module ActiveRecord #:nodoc: end def replace_named_bind_variables(statement, bind_vars) #:nodoc: - statement.gsub(/:([a-zA-Z]\w*)/) do - match = $1.to_sym - if bind_vars.include?(match) + statement.gsub(/(:?):([a-zA-Z]\w*)/) do + if $1 == ':' # skip postgresql casts + $& # return the whole match + elsif bind_vars.include?(match = $2.to_sym) quote_bound_value(bind_vars[match]) else raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}" @@ -2069,6 +2074,8 @@ module ActiveRecord #:nodoc: expanded = [] bind_vars.each do |var| + next if var.is_a?(Hash) + if var.is_a?(Range) expanded << var.first expanded << var.last @@ -2169,11 +2176,11 @@ module ActiveRecord #:nodoc: def cache_key case when new_record? - "#{self.class.name.tableize}/new" - when self[:updated_at] - "#{self.class.name.tableize}/#{id}-#{updated_at.to_s(:number)}" + "#{self.class.model_name.cache_key}/new" + when timestamp = self[:updated_at] + "#{self.class.model_name.cache_key}/#{id}-#{timestamp.to_s(:number)}" else - "#{self.class.name.tableize}/#{id}" + "#{self.class.model_name.cache_key}/#{id}" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index f968b9b173..2c03de0f17 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -30,11 +30,11 @@ module ActiveRecord end def text? - [:string, :text].include? type + type == :string || type == :text end def number? - [:float, :integer, :decimal].include? type + type == :integer || type == :float || type == :decimal end # Returns the Ruby class that corresponds to the abstract data type. @@ -304,8 +304,7 @@ module ActiveRecord # # Available options are (none of these exists by default): # * <tt>:limit</tt> - - # Requests a maximum column length (<tt>:string</tt>, <tt>:text</tt>, - # <tt>:binary</tt> or <tt>:integer</tt> columns only) + # Requests a maximum column length. This is number of characters for <tt>:string</tt> and <tt>:text</tt> columns and number of bytes for :binary and :integer columns. # * <tt>:default</tt> - # The column's default value. Use nil for NULL. # * <tt>:null</tt> - diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 93aafaaad1..c5962764f5 100755 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -99,7 +99,8 @@ module ActiveRecord end def extract_limit(sql_type) - if sql_type =~ /blob|text/i + case sql_type + when /blob|text/i case sql_type when /tiny/i 255 @@ -110,6 +111,11 @@ module ActiveRecord else super # we could return 65535 here, but we leave it undecorated by default end + when /^bigint/i; 8 + when /^int/i; 4 + when /^mediumint/i; 3 + when /^smallint/i; 2 + when /^tinyint/i; 1 else super end @@ -168,7 +174,7 @@ module ActiveRecord :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze, :string => { :name => "varchar", :limit => 255 }, :text => { :name => "text" }, - :integer => { :name => "int"}, + :integer => { :name => "int", :limit => 4 }, :float => { :name => "float" }, :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, @@ -450,8 +456,16 @@ module ActiveRecord end def rename_column(table_name, column_name, new_column_name) #:nodoc: + options = {} + if column = columns(table_name).find { |c| c.name == column_name.to_s } + options[:default] = column.default + else + raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" + end current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] - execute "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" + rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" + add_column_options!(rename_column_sql, options) + execute(rename_column_sql) end # Maps logical Rails types to MySQL-specific data types. @@ -459,14 +473,12 @@ module ActiveRecord return super unless type.to_s == 'integer' case limit - when 0..3 - "smallint(#{limit})" - when 4..8 - "int(#{limit})" - when 9..20 - "bigint(#{limit})" - else - 'int(11)' + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 294f4c1929..2e2d50ccf4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -47,6 +47,15 @@ module ActiveRecord end private + def extract_limit(sql_type) + case sql_type + when /^integer/i; 4 + when /^bigint/i; 8 + when /^smallint/i; 2 + else super + end + end + # Extracts the scale from PostgreSQL-specific data types. def extract_scale(sql_type) # Money type has a fixed scale of 2. @@ -324,12 +333,7 @@ module ActiveRecord end def supports_insert_with_returning? - unless defined? @supports_insert_with_returning - @supports_insert_with_returning = - @connection.respond_to?(:server_version) && - @connection.server_version >= 80200 - end - @supports_insert_with_returning + postgresql_version >= 80200 end # Returns the configured supported identifier length supported by PostgreSQL, @@ -553,7 +557,15 @@ module ActiveRecord # Example: # drop_database 'matt_development' def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" + if postgresql_version >= 80200 + execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" + else + begin + execute "DROP DATABASE #{quote_table_name(name)}" + rescue ActiveRecord::StatementInvalid + @logger.warn "#{name} database doesn't exist." if @logger + end + end end @@ -782,15 +794,14 @@ module ActiveRecord def type_to_sql(type, limit = nil, precision = nil, scale = nil) return super unless type.to_s == 'integer' - if limit.nil? || limit == 4 - 'integer' - elsif limit < 4 - 'smallint' - else - 'bigint' + case limit + when 1..2; 'smallint' + when 3..4, nil; 'integer' + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") end end - + # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. # # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb index b32e17e990..a7d767486c 100644 --- a/activerecord/lib/active_record/dirty.rb +++ b/activerecord/lib/active_record/dirty.rb @@ -142,9 +142,11 @@ module ActiveRecord def field_changed?(attr, old, value) if column = column_for_attribute(attr) - if column.type == :integer && column.null && old.nil? + if column.type == :integer && column.null && (old.nil? || old == 0) # For nullable integer columns, NULL gets stored in database for blank (i.e. '') values. # Hence we don't record it as a change if the value changes from nil to ''. + # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll + # be typecast back to 0 (''.to_i => 0) value = nil if value.blank? else value = column.type_cast(value) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index c66034d18b..ff9899d032 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -68,6 +68,7 @@ module ActiveRecord def update_with_lock(attribute_names = @attributes.keys) #:nodoc: return update_without_lock(attribute_names) unless locking_enabled? + return 0 if attribute_names.empty? lock_col = self.class.locking_column previous_value = send(lock_col).to_i diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index b47b01e99a..e095b3c766 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -399,7 +399,10 @@ module ActiveRecord def run target = migrations.detect { |m| m.version == @target_version } raise UnknownMigrationVersionError.new(@target_version) if target.nil? - target.migrate(@direction) + unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i)) + target.migrate(@direction) + record_version_state_after_migrating(target.version) + end end def migrate diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index b0c8a8b815..eac61e9e43 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -82,6 +82,7 @@ module ActiveRecord # expected_options = { :conditions => { :colored => 'red' } } # assert_equal expected_options, Shirt.colored('red').proxy_options def named_scope(name, options = {}, &block) + name = name.to_sym scopes[name] = lambda do |parent_scope, *args| Scope.new(parent_scope, case options when Hash diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 8614ef8751..3f74c03714 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -112,6 +112,10 @@ module ActiveRecord name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record end + def sanitized_conditions #:nodoc: + @sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions] + end + private def derive_class_name name.to_s.camelize diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index b638143c5a..1e21614f45 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -345,7 +345,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_invalid_adding_with_validate_false firm = Firm.find(:first) client = Client.new - firm.unvalidated_clients_of_firm << Client.new + firm.unvalidated_clients_of_firm << client assert firm.valid? assert !client.valid? @@ -353,6 +353,23 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert client.new_record? end + def test_valid_adding_with_validate_false + no_of_clients = Client.count + + firm = Firm.find(:first) + client = Client.new("name" => "Apple") + + assert firm.valid? + assert client.valid? + assert client.new_record? + + firm.unvalidated_clients_of_firm << client + + assert firm.save + assert !client.new_record? + assert_equal no_of_clients+1, Client.count + end + def test_build company = companies(:first_firm) new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index c011ffaf57..e5e022050d 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -2,6 +2,7 @@ require 'cases/helper' require 'models/topic' # For booleans require 'models/pirate' # For timestamps require 'models/parrot' +require 'models/person' # For optimistic locking class Pirate # Just reopening it, not defining it attr_accessor :detected_changes_in_after_update # Boolean for if changes are detected @@ -54,6 +55,33 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_zero_to_blank_marked_as_changed + pirate = Pirate.new + pirate.catchphrase = "Yarrrr, me hearties" + pirate.parrot_id = 1 + pirate.save + + # check the change from 1 to '' + pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") + pirate.parrot_id = '' + assert pirate.parrot_id_changed? + assert_equal([1, nil], pirate.parrot_id_change) + pirate.save + + # check the change from nil to 0 + pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") + pirate.parrot_id = 0 + assert pirate.parrot_id_changed? + assert_equal([nil, 0], pirate.parrot_id_change) + pirate.save + + # check the change from 0 to '' + pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties") + pirate.parrot_id = '' + assert pirate.parrot_id_changed? + assert_equal([0, nil], pirate.parrot_id_change) + end + def test_object_should_be_changed_if_any_attribute_is_changed pirate = Pirate.new assert !pirate.changed? @@ -125,6 +153,24 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_partial_update_with_optimistic_locking + person = Person.new(:first_name => 'foo') + old_lock_version = 1 + + with_partial_updates Person, false do + assert_queries(2) { 2.times { person.save! } } + Person.update_all({ :first_name => 'baz' }, :id => person.id) + end + + with_partial_updates Person, true do + assert_queries(0) { 2.times { person.save! } } + assert_equal old_lock_version, person.reload.lock_version + + assert_queries(1) { person.first_name = 'bar'; person.save! } + assert_not_equal old_lock_version, person.reload.lock_version + end + end + def test_changed_attributes_should_be_preserved_if_save_failure pirate = Pirate.new pirate.parrot_id = 1 diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 80936d51f3..b97db73b68 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/author' +require 'models/categorization' require 'models/comment' require 'models/company' require 'models/topic' @@ -199,6 +200,23 @@ class FinderTest < ActiveRecord::TestCase assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) } end + def test_find_on_hash_conditions_with_hashed_table_name + assert Topic.find(1, :conditions => {:topics => { :approved => false }}) + assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) } + end + + def test_find_with_hash_conditions_on_joined_table + firms = Firm.all :joins => :account, :conditions => {:accounts => { :credit_limit => 50 }} + assert_equal 1, firms.size + assert_equal companies(:first_firm), firms.first + end + + def test_find_with_hash_conditions_on_joined_table_and_with_range + firms = DependentFirm.all :joins => :account, :conditions => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }} + assert_equal 1, firms.size + assert_equal companies(:rails_core), firms.first + end + def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate david = customers(:david) assert Customer.find(david.id, :conditions => { 'customers.name' => david.name, :address => david.address }) @@ -394,6 +412,12 @@ class FinderTest < ActiveRecord::TestCase assert_equal '1,1,1', bind('?', os) end + def test_named_bind_with_postgresql_type_casts + l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') } + assert_nothing_raised(&l) + assert_equal "#{ActiveRecord::Base.quote_value('10')}::integer '2009-01-01'::date", l.call + end + def test_string_sanitation assert_not_equal "#{ActiveRecord::Base.connection.quoted_string_prefix}'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1") assert_equal "#{ActiveRecord::Base.connection.quoted_string_prefix}'something; select table'", ActiveRecord::Base.sanitize("something; select table") diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 7db6c570b5..701187223f 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -29,10 +29,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, p1.lock_version assert_equal 0, p2.lock_version + p1.first_name = 'stu' p1.save! assert_equal 1, p1.lock_version assert_equal 0, p2.lock_version + p2.first_name = 'sue' assert_raises(ActiveRecord::StaleObjectError) { p2.save! } end @@ -42,11 +44,14 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, p1.lock_version assert_equal 0, p2.lock_version + p1.first_name = 'stu' p1.save! assert_equal 1, p1.lock_version assert_equal 0, p2.lock_version + p2.first_name = 'sue' assert_raises(ActiveRecord::StaleObjectError) { p2.save! } + p2.first_name = 'sue2' assert_raises(ActiveRecord::StaleObjectError) { p2.save! } end @@ -54,15 +59,18 @@ class OptimisticLockingTest < ActiveRecord::TestCase p1 = Person.new(:first_name => 'anika') assert_equal 0, p1.lock_version + p1.first_name = 'anika2' p1.save! p2 = Person.find(p1.id) assert_equal 0, p1.lock_version assert_equal 0, p2.lock_version + p1.first_name = 'anika3' p1.save! assert_equal 1, p1.lock_version assert_equal 0, p2.lock_version + p2.first_name = 'sue' assert_raises(ActiveRecord::StaleObjectError) { p2.save! } end @@ -81,10 +89,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, t1.version assert_equal 0, t2.version + t1.tps_report_number = 700 t1.save! assert_equal 1, t1.version assert_equal 0, t2.version + t2.tps_report_number = 800 assert_raises(ActiveRecord::StaleObjectError) { t2.save! } end @@ -93,6 +103,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, p1.lock_version assert_equal p1.lock_version, Person.new(p1.attributes).lock_version + p1.first_name = 'bianca2' p1.save! assert_equal 1, p1.lock_version assert_equal p1.lock_version, Person.new(p1.attributes).lock_version @@ -146,6 +157,15 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert ref.save end + # Useful for partial updates, don't only update the lock_version if there + # is nothing else being updated. + def test_update_without_attributes_does_not_only_update_lock_version + assert_nothing_raised do + p1 = Person.new(:first_name => 'anika') + p1.send(:update_with_lock, []) + end + end + private def add_counter_column_to(model) diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index 1a9a875730..d6b3e341df 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -87,6 +87,16 @@ class MethodScopingTest < ActiveRecord::TestCase assert_equal 1, scoped_developers.size end + def test_scoped_find_joins + scoped_developers = Developer.with_scope(:find => { :joins => 'JOIN developers_projects ON id = developer_id' } ) do + Developer.find(:all, :conditions => 'developers_projects.project_id = 2') + end + assert scoped_developers.include?(developers(:david)) + assert !scoped_developers.include?(developers(:jamis)) + assert_equal 1, scoped_developers.size + assert_equal developers(:david).attributes, scoped_developers.first.attributes + end + def test_scoped_count_include # with the include, will retrieve only developers for the given project Developer.with_scope(:find => { :include => :projects }) do diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index f36255e209..4482b487dd 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -153,9 +153,10 @@ if ActiveRecord::Base.connection.supports_migrations? t.column :default_int, :integer - t.column :one_int, :integer, :limit => 1 - t.column :four_int, :integer, :limit => 4 - t.column :eight_int, :integer, :limit => 8 + t.column :one_int, :integer, :limit => 1 + t.column :four_int, :integer, :limit => 4 + t.column :eight_int, :integer, :limit => 8 + t.column :eleven_int, :integer, :limit => 11 end end @@ -167,12 +168,20 @@ if ActiveRecord::Base.connection.supports_migrations? one = columns.detect { |c| c.name == "one_int" } four = columns.detect { |c| c.name == "four_int" } eight = columns.detect { |c| c.name == "eight_int" } + eleven = columns.detect { |c| c.name == "eleven_int" } if current_adapter?(:PostgreSQLAdapter) assert_equal 'integer', default.sql_type assert_equal 'smallint', one.sql_type assert_equal 'integer', four.sql_type assert_equal 'bigint', eight.sql_type + assert_equal 'integer', eleven.sql_type + elsif current_adapter?(:MysqlAdapter) + assert_match 'int(11)', default.sql_type + assert_match 'tinyint', one.sql_type + assert_match 'int', four.sql_type + assert_match 'bigint', eight.sql_type + assert_match 'int(11)', eleven.sql_type elsif current_adapter?(:OracleAdapter) assert_equal 'NUMBER(38)', default.sql_type assert_equal 'NUMBER(1)', one.sql_type @@ -483,6 +492,32 @@ if ActiveRecord::Base.connection.supports_migrations? end end + def test_rename_column_preserves_default_value_not_null + begin + default_before = Developer.connection.columns("developers").find { |c| c.name == "salary" }.default + assert_equal 70000, default_before + Developer.connection.rename_column "developers", "salary", "anual_salary" + Developer.reset_column_information + assert Developer.column_names.include?("anual_salary") + default_after = Developer.connection.columns("developers").find { |c| c.name == "anual_salary" }.default + assert_equal 70000, default_after + ensure + Developer.connection.rename_column "developers", "anual_salary", "salary" + Developer.reset_column_information + end + end + + def test_rename_nonexistent_column + ActiveRecord::Base.connection.create_table(:hats) do |table| + table.column :hat_name, :string, :default => nil + end + assert_raises(ActiveRecord::ActiveRecordError) do + Person.connection.rename_column "hats", "nonexistent", "should_fail" + end + ensure + ActiveRecord::Base.connection.drop_table(:hats) + end + def test_rename_column_with_sql_reserved_word begin assert_nothing_raised { Person.connection.rename_column "people", "first_name", "group" } @@ -799,6 +834,21 @@ if ActiveRecord::Base.connection.supports_migrations? assert !Reminder.table_exists? end + def test_migrator_double_up + assert_equal(0, ActiveRecord::Migrator.current_version) + ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 1) + assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 1) } + assert_equal(1, ActiveRecord::Migrator.current_version) + end + + def test_migrator_double_down + assert_equal(0, ActiveRecord::Migrator.current_version) + ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 1) + ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT + "/valid", 1) + assert_nothing_raised { ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT + "/valid", 1) } + assert_equal(0, ActiveRecord::Migrator.current_version) + end + def test_finds_migrations migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations [['1', 'people_have_last_names'], @@ -888,16 +938,6 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") assert_equal(0, ActiveRecord::Migrator.current_version) end - - def test_migrator_run - assert_equal(0, ActiveRecord::Migrator.current_version) - ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 3) - assert_equal(0, ActiveRecord::Migrator.current_version) - - assert_equal(0, ActiveRecord::Migrator.current_version) - ActiveRecord::Migrator.run(:down, MIGRATIONS_ROOT + "/valid", 3) - assert_equal(0, ActiveRecord::Migrator.current_version) - end def test_schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "prefix_" @@ -1206,10 +1246,10 @@ if ActiveRecord::Base.connection.supports_migrations? end def integer_column - if current_adapter?(:SQLite3Adapter) || current_adapter?(:SQLiteAdapter) || current_adapter?(:PostgreSQLAdapter) - "integer" - else + if current_adapter?(:MysqlAdapter) 'int(11)' + else + 'integer' end end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index d890cf7936..7d73541ee1 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -6,7 +6,7 @@ require 'models/reply' require 'models/author' class NamedScopeTest < ActiveRecord::TestCase - fixtures :posts, :authors, :topics, :comments + fixtures :posts, :authors, :topics, :comments, :author_addresses def test_implements_enumerable assert !Topic.find(:all).empty? @@ -59,6 +59,16 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal Topic.count(:conditions => {:approved => true}), Topic.approved.count end + def test_scopes_with_string_name_can_be_composed + # NOTE that scopes defined with a string as a name worked on their own + # but when called on another scope the other scope was completely replaced + assert_equal Topic.replied.approved, Topic.replied.approved_as_string + end + + def test_scopes_can_be_specified_with_deep_hash_conditions + assert_equal Topic.replied.approved, Topic.replied.approved_as_hash_condition + end + def test_scopes_are_composable assert_equal (approved = Topic.find(:all, :conditions => {:approved => true})), Topic.approved assert_equal (replied = Topic.find(:all, :conditions => 'replies_count > 0')), Topic.replied @@ -77,6 +87,25 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal topics_written_before_the_second, Topic.written_before(topics(:second).written_on) end + def test_scopes_with_joins + address = author_addresses(:david_address) + posts_with_authors_at_address = Post.find( + :all, :joins => 'JOIN authors ON authors.id = posts.author_id', + :conditions => [ 'authors.author_address_id = ?', address.id ] + ) + assert_equal posts_with_authors_at_address, Post.with_authors_at_address(address) + end + + def test_scopes_with_joins_respects_custom_select + address = author_addresses(:david_address) + posts_with_authors_at_address_titles = Post.find(:all, + :select => 'title', + :joins => 'JOIN authors ON authors.id = posts.author_id', + :conditions => [ 'authors.author_address_id = ?', address.id ] + ) + assert_equal posts_with_authors_at_address_titles, Post.with_authors_at_address(address).find(:all, :select => 'title') + end + def test_extensions assert_equal 1, Topic.anonymous_extension.one assert_equal 2, Topic.named_extension.two diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index c42b0efba0..ee7e285a73 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -72,6 +72,52 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{:null => false}, output end + def test_schema_dump_includes_limit_constraint_for_integer_columns + stream = StringIO.new + + ActiveRecord::SchemaDumper.ignore_tables = [/^(?!integer_limits)/] + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + output = stream.string + + if current_adapter?(:PostgreSQLAdapter) + assert_match %r{c_int_1.*:limit => 2}, output + assert_match %r{c_int_2.*:limit => 2}, output + + # int 3 is 4 bytes in postgresql + assert_match %r{c_int_3.*}, output + assert_no_match %r{c_int_3.*:limit}, output + + assert_match %r{c_int_4.*}, output + assert_no_match %r{c_int_4.*:limit}, output + elsif current_adapter?(:MysqlAdapter) + assert_match %r{c_int_1.*:limit => 1}, output + assert_match %r{c_int_2.*:limit => 2}, output + assert_match %r{c_int_3.*:limit => 3}, output + + assert_match %r{c_int_4.*}, output + assert_no_match %r{c_int_4.*:limit}, output + elsif current_adapter?(:SQLiteAdapter) + assert_match %r{c_int_1.*:limit => 1}, output + assert_match %r{c_int_2.*:limit => 2}, output + assert_match %r{c_int_3.*:limit => 3}, output + assert_match %r{c_int_4.*:limit => 4}, output + end + assert_match %r{c_int_without_limit.*}, output + assert_no_match %r{c_int_without_limit.*:limit}, output + + if current_adapter?(:SQLiteAdapter) + assert_match %r{c_int_5.*:limit => 5}, output + assert_match %r{c_int_6.*:limit => 6}, output + assert_match %r{c_int_7.*:limit => 7}, output + assert_match %r{c_int_8.*:limit => 8}, output + else + assert_match %r{c_int_5.*:limit => 8}, output + assert_match %r{c_int_6.*:limit => 8}, output + assert_match %r{c_int_7.*:limit => 8}, output + assert_match %r{c_int_8.*:limit => 8}, output + end + end + def test_schema_dump_with_string_ignored_table stream = StringIO.new diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index d9101706b5..3adbc0ce1f 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -1,6 +1,11 @@ class Post < ActiveRecord::Base named_scope :containing_the_letter_a, :conditions => "body LIKE '%a%'" - + named_scope :with_authors_at_address, lambda { |address| { + :conditions => [ 'authors.author_address_id = ?', address.id ], + :joins => 'JOIN authors ON authors.id = posts.author_id' + } + } + belongs_to :author do def greeting "hello" diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index f63e862cfd..47b2eec938 100755 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -4,6 +4,8 @@ class Topic < ActiveRecord::Base { :conditions => ['written_on < ?', time] } } named_scope :approved, :conditions => {:approved => true} + named_scope :approved_as_hash_condition, :conditions => {:topics => {:approved => true}} + named_scope 'approved_as_string', :conditions => {:approved => true} named_scope :replied, :conditions => ['replies_count > 0'] named_scope :anonymous_extension do def one diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 423929fd55..234c43494a 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -407,6 +407,13 @@ ActiveRecord::Schema.define do t.column :key, :string end + create_table :integer_limits, :force => true do |t| + t.integer :"c_int_without_limit" + (1..8).each do |i| + t.integer :"c_int_#{i}", :limit => i + end + end + except 'SQLite' do # fk_test_has_fk should be before fk_test_has_pk create_table :fk_test_has_fk, :force => true do |t| |