diff options
Diffstat (limited to 'activerecord')
57 files changed, 1399 insertions, 861 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 659de99873..9adc6b887f 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,9 @@ *Edge* +* quoted_date converts time-like objects to ActiveRecord::Base.default_timezone before serialization. This allows you to use Time.now in find conditions and have it correctly be serialized as the current time in UTC when default_timezone == :utc. #2946 [Geoff Buesing] + +* SQLite: drop support for 'dbfile' option in favor of 'database.' #2363 [Paul Hinze, Jeremy Kemper] + * Added :primary_key option to belongs_to associations. #765 [Szymon Nowak, Philip Hallstrom, Noel Rocha] # employees.company_name references companies.name Employee.belongs_to :company, :primary_key => 'name', :foreign_key => 'company_name' diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 0d33b9d516..09dbc5ad6d 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -64,8 +64,8 @@ end namespace :mysql do desc 'Build the MySQL test databases' task :build_databases do - %x( mysqladmin --user=#{MYSQL_DB_USER} create activerecord_unittest ) - %x( mysqladmin --user=#{MYSQL_DB_USER} create activerecord_unittest2 ) + %x( echo "create DATABASE activerecord_unittest DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci " | mysql --user=#{MYSQL_DB_USER}) + %x( echo "create DATABASE activerecord_unittest2 DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci " | mysql --user=#{MYSQL_DB_USER}) end desc 'Drop the MySQL test databases' diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 63eb5c3eeb..68b0251982 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -52,7 +52,6 @@ module ActiveRecord autoload :Batches, 'active_record/batches' autoload :Calculations, 'active_record/calculations' autoload :Callbacks, 'active_record/callbacks' - autoload :Dirty, 'active_record/dirty' autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match' autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match' autoload :Migration, 'active_record/migration' @@ -66,11 +65,22 @@ module ActiveRecord autoload :SchemaDumper, 'active_record/schema_dumper' autoload :Serialization, 'active_record/serialization' autoload :SessionStore, 'active_record/session_store' + autoload :StateMachine, 'active_record/state_machine' autoload :TestCase, 'active_record/test_case' autoload :Timestamp, 'active_record/timestamp' autoload :Transactions, 'active_record/transactions' autoload :Validations, 'active_record/validations' + module AttributeMethods + autoload :BeforeTypeCast, 'active_record/attribute_methods/before_type_cast' + autoload :Dirty, 'active_record/attribute_methods/dirty' + autoload :PrimaryKey, 'active_record/attribute_methods/primary_key' + autoload :Query, 'active_record/attribute_methods/query' + autoload :Read, 'active_record/attribute_methods/read' + autoload :TimeZoneConversion, 'active_record/attribute_methods/time_zone_conversion' + autoload :Write, 'active_record/attribute_methods/write' + end + module Locking autoload :Optimistic, 'active_record/locking/optimistic' autoload :Pessimistic, 'active_record/locking/pessimistic' diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 66aa9332c8..7f299b2aa5 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1367,7 +1367,7 @@ module ActiveRecord define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| ids = (new_value || []).reject { |nid| nid.blank? } - send("#{reflection.name}=", reflection.class_name.constantize.find(ids)) + send("#{reflection.name}=", reflection.klass.find(ids)) end end end @@ -1912,7 +1912,7 @@ module ActiveRecord descendant end.flatten.compact - remove_duplicate_results!(reflection.class_name.constantize, parent_records, associations[name]) unless parent_records.empty? + remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty? end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index e21ef90391..f4507c979c 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -8,6 +8,8 @@ module ActiveRecord alias_method :new, :build def create!(attrs = nil) + ensure_owner_is_not_new + transaction do self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!) object @@ -15,6 +17,8 @@ module ActiveRecord end def create(attrs = nil) + ensure_owner_is_not_new + transaction do self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association) object @@ -50,7 +54,7 @@ module ActiveRecord options[:select] = construct_select(options[:select]) options[:from] ||= construct_from options[:joins] = construct_joins(options[:joins]) - options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? + options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include] end def insert_record(record, force = true, validate = true) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ecd2d57a5a..ab7ad34b9e 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -3,100 +3,13 @@ require 'active_support/core_ext/enumerable' module ActiveRecord module AttributeMethods #:nodoc: extend ActiveSupport::Concern + include ActiveModel::AttributeMethods - DEFAULT_SUFFIXES = %w(= ? _before_type_cast) - ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] - - included do - attribute_method_suffix(*DEFAULT_SUFFIXES) - - cattr_accessor :attribute_types_cached_by_default, :instance_writer => false - self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - - cattr_accessor :time_zone_aware_attributes, :instance_writer => false - self.time_zone_aware_attributes = false - - class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false - self.skip_time_zone_conversion_for_attributes = [] - end - - # Declare and check for suffixed attribute methods. module ClassMethods - # Declares a method available for all attributes with the given suffix. - # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method - # - # #{attr}#{suffix}(*args, &block) - # - # to - # - # attribute#{suffix}(#{attr}, *args, &block) - # - # An <tt>attribute#{suffix}</tt> instance method must exist and accept at least - # the +attr+ argument. - # - # For example: - # - # class Person < ActiveRecord::Base - # attribute_method_suffix '_changed?' - # - # private - # def attribute_changed?(attr) - # ... - # end - # end - # - # person = Person.find(1) - # person.name_changed? # => false - # person.name = 'Hubert' - # person.name_changed? # => true - def attribute_method_suffix(*suffixes) - attribute_method_suffixes.concat suffixes - rebuild_attribute_method_regexp - end - - # Returns MatchData if method_name is an attribute method. - def match_attribute_method?(method_name) - rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp - @@attribute_method_regexp.match(method_name) - end - - - # Contains the names of the generated attribute methods. - def generated_methods #:nodoc: - @generated_methods ||= Set.new - end - - def generated_methods? - !generated_methods.empty? - end - # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods - return if generated_methods? - columns_hash.each do |name, column| - unless instance_method_already_implemented?(name) - if self.serialized_attributes[name] - define_read_method_for_serialized_attribute(name) - elsif create_time_zone_conversion_attribute?(name, column) - define_read_method_for_time_zone_conversion(name) - else - define_read_method(name.to_sym, name, column) - end - end - - unless instance_method_already_implemented?("#{name}=") - if create_time_zone_conversion_attribute?(name, column) - define_write_method_for_time_zone_conversion(name) - else - define_write_method(name.to_sym) - end - end - - unless instance_method_already_implemented?("#{name}?") - define_question_method(name) - end - end + super(columns_hash.keys) end # Checks whether the method is defined in the model or any of its subclasses @@ -104,296 +17,35 @@ module ActiveRecord # method is defined by Active Record though. def instance_method_already_implemented?(method_name) method_name = method_name.to_s - return true if method_name =~ /^id(=$|\?$|$)/ @_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map {|m| m.to_s }.to_set @@_defined_activerecord_methods ||= (ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false)).map{|m| m.to_s }.to_set raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name) @_defined_class_methods.include?(method_name) end - - alias :define_read_methods :define_attribute_methods - - # +cache_attributes+ allows you to declare which converted attribute values should - # be cached. Usually caching only pays off for attributes with expensive conversion - # methods, like time related columns (e.g. +created_at+, +updated_at+). - def cache_attributes(*attribute_names) - attribute_names.each {|attr| cached_attributes << attr.to_s} - end - - # Returns the attributes which are cached. By default time related columns - # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached. - def cached_attributes - @cached_attributes ||= - columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map{|col| col.name}.to_set - end - - # Returns +true+ if the provided attribute is being cached. - def cache_attribute?(attr_name) - cached_attributes.include?(attr_name) - end - - private - - # Suffixes a, ?, c become regexp /(a|\?|c)$/ - def rebuild_attribute_method_regexp - suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) } - @@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze - end - - # Default to =, ?, _before_type_cast - def attribute_method_suffixes - @@attribute_method_suffixes ||= [] - end - - def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) - end - - # Define an attribute reader method. Cope with nil column. - def define_read_method(symbol, attr_name, column) - cast_code = column.type_cast_code('v') if column - access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" - - unless attr_name.to_s == self.primary_key.to_s - access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") - end - - if cache_attribute?(attr_name) - access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" - end - evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end" - end - - # Define read method for serialized attribute. - def define_read_method_for_serialized_attribute(attr_name) - evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end" - end - - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone. - def define_read_method_for_time_zone_conversion(attr_name) - method_body = <<-EOV - def #{attr_name}(reload = false) - cached = @attributes_cache['#{attr_name}'] - return cached if cached && !reload - time = read_attribute('#{attr_name}') - @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time - end - EOV - evaluate_attribute_method attr_name, method_body - end - - # Defines a predicate method <tt>attr_name?</tt>. - def define_question_method(attr_name) - evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?" - end - - def define_write_method(attr_name) - evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}=" - end - - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. - # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. - def define_write_method_for_time_zone_conversion(attr_name) - method_body = <<-EOV - def #{attr_name}=(time) - unless time.acts_like?(:time) - time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time - end - time = time.in_time_zone rescue nil if time - write_attribute(:#{attr_name}, time) - end - EOV - evaluate_attribute_method attr_name, method_body, "#{attr_name}=" - end - - # Evaluate the definition for an attribute related method - def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name) - - unless method_name.to_s == primary_key.to_s - generated_methods << method_name - end - - begin - class_eval(method_definition, __FILE__, __LINE__) - rescue SyntaxError => err - generated_methods.delete(attr_name) - if logger - logger.warn "Exception occurred during reader method compilation." - logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?" - logger.warn err.message - end - end - end - end # ClassMethods - + end - # Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they - # were first-class methods. So a Person class with a name attribute can use Person#name and - # Person#name= and never directly use the attributes hash -- except for multiple assigns with - # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that - # the completed attribute is not +nil+ or 0. - # - # It's also possible to instantiate related objects, so a Client class belonging to the clients - # table with a +master_id+ foreign key can instantiate master through Client#master. def method_missing(method_id, *args, &block) - method_name = method_id.to_s - # If we haven't generated any methods yet, generate them, then # see if we've created the method we're looking for. - if !self.class.generated_methods? + if !self.class.attribute_methods_generated? self.class.define_attribute_methods + method_name = method_id.to_s guard_private_attribute_method!(method_name, args) - if self.class.generated_methods.include?(method_name) + if self.class.generated_attribute_methods.instance_methods.include?(method_name) return self.send(method_id, *args, &block) end end - - guard_private_attribute_method!(method_name, args) - if self.class.primary_key.to_s == method_name - id - elsif md = self.class.match_attribute_method?(method_name) - attribute_name, method_type = md.pre_match, md.to_s - if @attributes.include?(attribute_name) - __send__("attribute#{method_type}", attribute_name, *args, &block) - else - super - end - elsif @attributes.include?(method_name) - read_attribute(method_name) - else - super - end - end - - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, - # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). - def read_attribute(attr_name) - attr_name = attr_name.to_s - if !(value = @attributes[attr_name]).nil? - if column = column_for_attribute(attr_name) - if unserializable_attribute?(attr_name, column) - unserialize_attribute(attr_name) - else - column.type_cast(value) - end - else - value - end - else - nil - end - end - - def read_attribute_before_type_cast(attr_name) - @attributes[attr_name] - end - - # Returns true if the attribute is of a text column and marked for serialization. - def unserializable_attribute?(attr_name, column) - column.text? && self.class.serialized_attributes[attr_name] - end - - # Returns the unserialized object of the attribute. - def unserialize_attribute(attr_name) - unserialized_object = object_from_yaml(@attributes[attr_name]) - - if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil? - @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object - else - raise SerializationTypeMismatch, - "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" - end - end - - - # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float - # columns are turned into +nil+. - def write_attribute(attr_name, value) - attr_name = attr_name.to_s - @attributes_cache.delete(attr_name) - if (column = column_for_attribute(attr_name)) && column.number? - @attributes[attr_name] = convert_number_column_value(value) - else - @attributes[attr_name] = value - end + super end - - def query_attribute(attr_name) - unless value = read_attribute(attr_name) - false - else - column = self.class.columns_hash[attr_name] - if column.nil? - if Numeric === value || value !~ /[^0-9]/ - !value.to_i.zero? - else - return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) - !value.blank? - end - elsif column.number? - !value.zero? - else - !value.blank? - end - end - end - - # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, - # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> - # which will all return +true+. - alias :respond_to_without_attributes? :respond_to? - def respond_to?(method, include_private_methods = false) - method_name = method.to_s - if super - return true - elsif !include_private_methods && super(method, true) - # If we're here than we haven't found among non-private methods - # but found among all methods. Which means that given method is private. - return false - elsif !self.class.generated_methods? - self.class.define_attribute_methods - if self.class.generated_methods.include?(method_name) - return true - end - end - - if @attributes.nil? - return super - elsif @attributes.include?(method_name) - return true - elsif md = self.class.match_attribute_method?(method_name) - return true if @attributes.include?(md.pre_match) - end + def respond_to?(*args) + self.class.define_attribute_methods super end - private - # prevent method_missing from calling private methods with #send - def guard_private_attribute_method!(method_name, args) - if self.class.private_method_defined?(method_name) - raise NoMethodError.new("Attempt to call private method", method_name, args) - end - end - - def missing_attribute(attr_name, stack) - raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack - end - - # Handle *? for method_missing. - def attribute?(attribute_name) - query_attribute(attribute_name) - end - - # Handle *= for method_missing. - def attribute=(attribute_name, value) - write_attribute(attribute_name, value) - end - - # Handle *_before_type_cast for method_missing. - def attribute_before_type_cast(attribute_name) - read_attribute_before_type_cast(attribute_name) + protected + def attribute_method?(attr_name) + attr_name == 'id' || attributes.include?(attr_name) end end end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb new file mode 100644 index 0000000000..a4e144f233 --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -0,0 +1,33 @@ +module ActiveRecord + module AttributeMethods + module BeforeTypeCast + extend ActiveSupport::Concern + + included do + attribute_method_suffix "_before_type_cast" + end + + def read_attribute_before_type_cast(attr_name) + @attributes[attr_name] + end + + # Returns a hash of attributes before typecasting and deserialization. + def attributes_before_type_cast + self.attribute_names.inject({}) do |attrs, name| + attrs[name] = read_attribute_before_type_cast(name) + attrs + end + end + + private + # Handle *_before_type_cast for method_missing. + def attribute_before_type_cast(attribute_name) + if attribute_name == 'id' + read_attribute_before_type_cast(self.class.primary_key) + else + read_attribute_before_type_cast(attribute_name) + end + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb new file mode 100644 index 0000000000..911c908c8b --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -0,0 +1,204 @@ +module ActiveRecord + module AttributeMethods + # Track unsaved attribute changes. + # + # A newly instantiated object is unchanged: + # person = Person.find_by_name('Uncle Bob') + # person.changed? # => false + # + # Change the name: + # person.name = 'Bob' + # person.changed? # => true + # person.name_changed? # => true + # person.name_was # => 'Uncle Bob' + # person.name_change # => ['Uncle Bob', 'Bob'] + # person.name = 'Bill' + # person.name_change # => ['Uncle Bob', 'Bill'] + # + # Save the changes: + # person.save + # person.changed? # => false + # person.name_changed? # => false + # + # Assigning the same value leaves the attribute unchanged: + # person.name = 'Bill' + # person.name_changed? # => false + # person.name_change # => nil + # + # Which attributes have changed? + # person.name = 'Bob' + # person.changed # => ['name'] + # person.changes # => { 'name' => ['Bill', 'Bob'] } + # + # Resetting an attribute returns it to its original state: + # person.reset_name! # => 'Bill' + # person.changed? # => false + # person.name_changed? # => false + # person.name # => 'Bill' + # + # Before modifying an attribute in-place: + # person.name_will_change! + # person.name << 'y' + # person.name_change # => ['Bill', 'Billy'] + module Dirty + extend ActiveSupport::Concern + + DIRTY_AFFIXES = [ + { :suffix => '_changed?' }, + { :suffix => '_change' }, + { :suffix => '_will_change!' }, + { :suffix => '_was' }, + { :prefix => 'reset_', :suffix => '!' } + ] + + included do + attribute_method_affix *DIRTY_AFFIXES + + alias_method_chain :save, :dirty + alias_method_chain :save!, :dirty + alias_method_chain :update, :dirty + alias_method_chain :reload, :dirty + + superclass_delegating_accessor :partial_updates + self.partial_updates = true + end + + # Do any attributes have unsaved changes? + # person.changed? # => false + # person.name = 'bob' + # person.changed? # => true + def changed? + !changed_attributes.empty? + end + + # List of attributes with unsaved changes. + # person.changed # => [] + # person.name = 'bob' + # person.changed # => ['name'] + def changed + changed_attributes.keys + end + + # Map of changed attrs => [original value, new value]. + # person.changes # => {} + # person.name = 'bob' + # person.changes # => { 'name' => ['bill', 'bob'] } + def changes + changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h } + end + + # Attempts to +save+ the record and clears changed attributes if successful. + def save_with_dirty(*args) #:nodoc: + if status = save_without_dirty(*args) + changed_attributes.clear + end + status + end + + # Attempts to <tt>save!</tt> the record and clears changed attributes if successful. + def save_with_dirty!(*args) #:nodoc: + status = save_without_dirty!(*args) + changed_attributes.clear + status + end + + # <tt>reload</tt> the record and clears changed attributes. + def reload_with_dirty(*args) #:nodoc: + record = reload_without_dirty(*args) + changed_attributes.clear + record + end + + private + # Map of change <tt>attr => original value</tt>. + def changed_attributes + @changed_attributes ||= {} + end + + # Handle <tt>*_changed?</tt> for +method_missing+. + def attribute_changed?(attr) + changed_attributes.include?(attr) + end + + # Handle <tt>*_change</tt> for +method_missing+. + def attribute_change(attr) + [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) + end + + # Handle <tt>*_was</tt> for +method_missing+. + def attribute_was(attr) + attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) + end + + # Handle <tt>reset_*!</tt> for +method_missing+. + def reset_attribute!(attr) + self[attr] = changed_attributes[attr] if attribute_changed?(attr) + end + + # Handle <tt>*_will_change!</tt> for +method_missing+. + def attribute_will_change!(attr) + changed_attributes[attr] = clone_attribute_value(:read_attribute, attr) + end + + # Wrap write_attribute to remember original attribute value. + def write_attribute(attr, value) + attr = attr.to_s + + # The attribute already has an unsaved change. + if changed_attributes.include?(attr) + old = changed_attributes[attr] + changed_attributes.delete(attr) unless field_changed?(attr, old, value) + else + old = clone_attribute_value(:read_attribute, attr) + changed_attributes[attr] = old if field_changed?(attr, old, value) + end + + # Carry on. + super(attr, value) + end + + def update_with_dirty + if partial_updates? + # Serialized attributes should always be written in case they've been + # changed in place. + update_without_dirty(changed | (attributes.keys & self.class.serialized_attributes.keys)) + else + update_without_dirty + end + end + + def field_changed?(attr, old, value) + if column = column_for_attribute(attr) + if column.number? && column.null && (old.nil? || old == 0) && value.blank? + # For nullable numeric 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 + else + value = column.type_cast(value) + end + end + + old != value + end + + module ClassMethods + def self.extended(base) + class << base + alias_method_chain :alias_attribute, :dirty + end + end + + def alias_attribute_with_dirty(new_name, old_name) + alias_attribute_without_dirty(new_name, old_name) + DIRTY_AFFIXES.each do |affixes| + module_eval <<-STR, __FILE__, __LINE__+1 + def #{affixes[:prefix]}#{new_name}#{affixes[:suffix]}; self.#{affixes[:prefix]}#{old_name}#{affixes[:suffix]}; end # def reset_subject!; self.reset_title!; end + STR + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb new file mode 100644 index 0000000000..365fdeb55a --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -0,0 +1,44 @@ +module ActiveRecord + module AttributeMethods + module PrimaryKey + extend ActiveSupport::Concern + + module ClassMethods + # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the + # primary_key_prefix_type setting, though. + def primary_key + reset_primary_key + end + + def reset_primary_key #:nodoc: + key = get_primary_key(base_class.name) + set_primary_key(key) + key + end + + def get_primary_key(base_name) #:nodoc: + key = 'id' + case primary_key_prefix_type + when :table_name + key = base_name.to_s.foreign_key(false) + when :table_name_with_underscore + key = base_name.to_s.foreign_key + end + key + end + + # Sets the name of the primary key column to use to the given value, + # or (if the value is nil or false) to the value returned by the given + # block. + # + # class Project < ActiveRecord::Base + # set_primary_key "sysid" + # end + def set_primary_key(value = nil, &block) + define_attr_method :primary_key, value, &block + end + alias :primary_key= :set_primary_key + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb new file mode 100644 index 0000000000..a949d80120 --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -0,0 +1,37 @@ +module ActiveRecord + module AttributeMethods + module Query + extend ActiveSupport::Concern + + included do + attribute_method_suffix "?" + end + + def query_attribute(attr_name) + unless value = read_attribute(attr_name) + false + else + column = self.class.columns_hash[attr_name] + if column.nil? + if Numeric === value || value !~ /[^0-9]/ + !value.to_i.zero? + else + return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) + !value.blank? + end + elsif column.number? + !value.zero? + else + !value.blank? + end + end + end + + private + # Handle *? for method_missing. + def attribute?(attribute_name) + query_attribute(attribute_name) + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb new file mode 100644 index 0000000000..3da3d9d8cc --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -0,0 +1,116 @@ +module ActiveRecord + module AttributeMethods + module Read + extend ActiveSupport::Concern + + ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] + + included do + attribute_method_suffix "" + + cattr_accessor :attribute_types_cached_by_default, :instance_writer => false + self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT + + # Undefine id so it can be used as an attribute name + undef_method(:id) if method_defined?(:id) + end + + module ClassMethods + # +cache_attributes+ allows you to declare which converted attribute values should + # be cached. Usually caching only pays off for attributes with expensive conversion + # methods, like time related columns (e.g. +created_at+, +updated_at+). + def cache_attributes(*attribute_names) + attribute_names.each {|attr| cached_attributes << attr.to_s} + end + + # Returns the attributes which are cached. By default time related columns + # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached. + def cached_attributes + @cached_attributes ||= + columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map{|col| col.name}.to_set + end + + # Returns +true+ if the provided attribute is being cached. + def cache_attribute?(attr_name) + cached_attributes.include?(attr_name) + end + + protected + def define_method_attribute(attr_name) + if self.serialized_attributes[attr_name] + define_read_method_for_serialized_attribute(attr_name) + else + define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name]) + end + + if attr_name == primary_key && attr_name != "id" + define_read_method(:id, attr_name, columns_hash[attr_name]) + end + end + + private + # Define read method for serialized attribute. + def define_read_method_for_serialized_attribute(attr_name) + generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__) + end + + # Define an attribute reader method. Cope with nil column. + def define_read_method(symbol, attr_name, column) + cast_code = column.type_cast_code('v') if column + access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" + + unless attr_name.to_s == self.primary_key.to_s + access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") + end + + if cache_attribute?(attr_name) + access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" + end + generated_attribute_methods.module_eval("def #{symbol}; #{access_code}; end", __FILE__, __LINE__) + end + end + + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, + # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). + def read_attribute(attr_name) + attr_name = attr_name.to_s + attr_name = self.class.primary_key if attr_name == 'id' + if !(value = @attributes[attr_name]).nil? + if column = column_for_attribute(attr_name) + if unserializable_attribute?(attr_name, column) + unserialize_attribute(attr_name) + else + column.type_cast(value) + end + else + value + end + else + nil + end + end + + # Returns true if the attribute is of a text column and marked for serialization. + def unserializable_attribute?(attr_name, column) + column.text? && self.class.serialized_attributes[attr_name] + end + + # Returns the unserialized object of the attribute. + def unserialize_attribute(attr_name) + unserialized_object = object_from_yaml(@attributes[attr_name]) + + if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil? + @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object + else + raise SerializationTypeMismatch, + "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" + end + end + + private + def attribute(attribute_name) + read_attribute(attribute_name) + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb new file mode 100644 index 0000000000..a8e3e28a7a --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -0,0 +1,60 @@ +module ActiveRecord + module AttributeMethods + module TimeZoneConversion + extend ActiveSupport::Concern + + included do + cattr_accessor :time_zone_aware_attributes, :instance_writer => false + self.time_zone_aware_attributes = false + + class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false + self.skip_time_zone_conversion_for_attributes = [] + end + + module ClassMethods + protected + # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. + # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone. + def define_method_attribute(attr_name) + if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) + method_body = <<-EOV + def #{attr_name}(reload = false) + cached = @attributes_cache['#{attr_name}'] + return cached if cached && !reload + time = read_attribute('#{attr_name}') + @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time + end + EOV + generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__) + else + super + end + end + + # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. + # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. + def define_method_attribute=(attr_name) + if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) + method_body = <<-EOV + def #{attr_name}=(time) + unless time.acts_like?(:time) + time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time + end + time = time.in_time_zone rescue nil if time + write_attribute(:#{attr_name}, time) + end + EOV + generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__) + else + super + end + end + + private + def create_time_zone_conversion_attribute?(name, column) + time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) + end + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb new file mode 100644 index 0000000000..e31acac050 --- /dev/null +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -0,0 +1,37 @@ +module ActiveRecord + module AttributeMethods + module Write + extend ActiveSupport::Concern + + included do + attribute_method_suffix "=" + end + + module ClassMethods + protected + def define_method_attribute=(attr_name) + generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) + end + end + + # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float + # columns are turned into +nil+. + def write_attribute(attr_name, value) + attr_name = attr_name.to_s + attr_name = self.class.primary_key if attr_name == 'id' + @attributes_cache.delete(attr_name) + if (column = column_for_attribute(attr_name)) && column.number? + @attributes[attr_name] = convert_number_column_value(value) + else + @attributes[attr_name] = value + end + end + + private + # Handle *= for method_missing. + def attribute=(attribute_name, value) + write_attribute(attribute_name, value) + end + end + end +end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 5a36ff5ba2..531a698f77 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -148,11 +148,6 @@ module ActiveRecord #:nodoc: class DangerousAttributeError < ActiveRecordError end - # Raised when you've tried to access a column which wasn't loaded by your finder. - # Typically this is because <tt>:select</tt> has been specified. - class MissingAttributeError < NoMethodError - end - # Raised when unknown attributes are supplied via mass assignment. class UnknownAttributeError < NoMethodError end @@ -1225,29 +1220,6 @@ module ActiveRecord #:nodoc: name end - # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the - # primary_key_prefix_type setting, though. - def primary_key - reset_primary_key - end - - def reset_primary_key #:nodoc: - key = get_primary_key(base_class.name) - set_primary_key(key) - key - end - - def get_primary_key(base_name) #:nodoc: - key = 'id' - case primary_key_prefix_type - when :table_name - key = base_name.to_s.foreign_key(false) - when :table_name_with_underscore - key = base_name.to_s.foreign_key - end - key - end - # Defines the column name for use with single table inheritance # -- can be set in subclasses like so: self.inheritance_column = "type_id" def inheritance_column @@ -1277,18 +1249,6 @@ module ActiveRecord #:nodoc: end alias :table_name= :set_table_name - # Sets the name of the primary key column to use to the given value, - # or (if the value is nil or false) to the value returned by the given - # block. - # - # class Project < ActiveRecord::Base - # set_primary_key "sysid" - # end - def set_primary_key(value = nil, &block) - define_attr_method :primary_key, value, &block - end - alias :primary_key= :set_primary_key - # Sets the name of the inheritance column to use to the given value, # or (if the value # is nil or false) to the value returned by the # given block. @@ -1401,8 +1361,8 @@ module ActiveRecord #:nodoc: # end # end def reset_column_information - generated_methods.each { |name| undef_method(name) } - @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @generated_methods = @inheritance_column = nil + undefine_attribute_methods + @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil end def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc: @@ -2077,36 +2037,6 @@ module ActiveRecord #:nodoc: end end - # Defines an "attribute" method (like +inheritance_column+ or - # +table_name+). A new (class) method will be created with the - # given name. If a value is specified, the new method will - # return that value (as a string). Otherwise, the given block - # will be used to compute the value of the method. - # - # The original method will be aliased, with the new name being - # prefixed with "original_". This allows the new method to - # access the original value. - # - # Example: - # - # class A < ActiveRecord::Base - # define_attr_method :primary_key, "sysid" - # define_attr_method( :inheritance_column ) do - # original_inheritance_column + "_id" - # end - # end - def define_attr_method(name, value=nil, &block) - sing = metaclass - sing.send :alias_method, "original_#{name}", name - if block_given? - sing.send :define_method, name, &block - else - # use eval instead of a block to work around a memory leak in dev - # mode in fcgi - sing.class_eval "def #{name}; #{value.to_s.inspect}; end" - end - end - protected # Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash. # method_name may be <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameters may include the <tt>:conditions</tt>, <tt>:joins</tt>, @@ -2508,18 +2438,6 @@ module ActiveRecord #:nodoc: result end - # A model instance's primary key is always available as model.id - # whether you name it the default 'id' or set it to something else. - def id - attr_name = self.class.primary_key - column = column_for_attribute(attr_name) - - self.class.send(:define_read_method, :id, attr_name, column) - # now that the method exists, call it - self.send attr_name.to_sym - - end - # Returns a String, which Action Pack uses for constructing an URL to this # object. The default implementation returns this record's id as a String, # or nil if this record's unsaved. @@ -2565,24 +2483,20 @@ module ActiveRecord #:nodoc: end end - def id_before_type_cast #:nodoc: - read_attribute_before_type_cast(self.class.primary_key) - end - def quoted_id #:nodoc: quote_value(id, column_for_attribute(self.class.primary_key)) end - # Sets the primary ID. - def id=(value) - write_attribute(self.class.primary_key, value) - end - # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet; otherwise, returns false. def new_record? @new_record || false end + # Returns true if this object has been destroyed, otherwise returns false. + def destroyed? + @destroyed || false + end + # :call-seq: # save(perform_validation = true) # @@ -2633,6 +2547,7 @@ module ActiveRecord #:nodoc: # options, use <tt>#destroy</tt>. def delete self.class.delete(id) unless new_record? + @destroyed = true freeze end @@ -2647,6 +2562,7 @@ module ActiveRecord #:nodoc: ) end + @destroyed = true freeze end @@ -2822,14 +2738,6 @@ module ActiveRecord #:nodoc: end end - # Returns a hash of attributes before typecasting and deserialization. - def attributes_before_type_cast - self.attribute_names.inject({}) do |attrs, name| - attrs[name] = read_attribute_before_type_cast(name) - attrs - end - end - # Returns an <tt>#inspect</tt>-like string for the value of the # attribute +attr_name+. String attributes are elided after 50 # characters, and Date and Time attributes are returned in the @@ -3212,7 +3120,10 @@ module ActiveRecord #:nodoc: include Validations include Locking::Optimistic, Locking::Pessimistic include AttributeMethods - include Dirty + include AttributeMethods::Read, AttributeMethods::Write, AttributeMethods::BeforeTypeCast, AttributeMethods::Query + include AttributeMethods::PrimaryKey + include AttributeMethods::TimeZoneConversion + include AttributeMethods::Dirty include Callbacks, ActiveModel::Observing, Timestamp include Associations, AssociationPreload, NamedScope include ActiveModel::Conversion diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 727f4c1dc6..4a88c43dff 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -197,6 +197,8 @@ module ActiveRecord sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group] if options[:from] sql << " FROM #{options[:from]} " + elsif scope && scope[:from] + sql << " FROM #{scope[:from]} " else sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround sql << " FROM #{connection.quote_table_name(table_name)} " diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 720fba29e9..8649f96498 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -60,7 +60,12 @@ module ActiveRecord end def quoted_date(value) - value.to_s(:db) + if value.acts_like?(:time) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value + else + value + end.to_s(:db) end def quoted_string_prefix 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 24c734cddb..f346e3ebc8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -277,7 +277,6 @@ module ActiveRecord add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key column_sql end - alias to_s :to_sql private @@ -508,7 +507,7 @@ module ActiveRecord # concatenated together. This string can then be prepended and appended to # to generate the final SQL to create the table. def to_sql - @columns * ', ' + @columns.map(&:to_sql) * ', ' end private diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 83cb9cff15..2b882a1f25 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -587,6 +587,10 @@ module ActiveRecord @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) end + @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] + @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] + @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] + @connection.real_connect(*@connection_options) # reconnect must be set after real_connect is called, because real_connect sets it to false internally diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 5e5e30776a..c0f5046bff 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -27,7 +27,6 @@ module ActiveRecord private def parse_sqlite_config!(config) - config[:database] ||= config[:dbfile] # Require database. unless config[:database] raise ArgumentError, "No database file specified. Missing argument: database" diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb deleted file mode 100644 index 178767e0c3..0000000000 --- a/activerecord/lib/active_record/dirty.rb +++ /dev/null @@ -1,186 +0,0 @@ -module ActiveRecord - # Track unsaved attribute changes. - # - # A newly instantiated object is unchanged: - # person = Person.find_by_name('uncle bob') - # person.changed? # => false - # - # Change the name: - # person.name = 'Bob' - # person.changed? # => true - # person.name_changed? # => true - # person.name_was # => 'uncle bob' - # person.name_change # => ['uncle bob', 'Bob'] - # person.name = 'Bill' - # person.name_change # => ['uncle bob', 'Bill'] - # - # Save the changes: - # person.save - # person.changed? # => false - # person.name_changed? # => false - # - # Assigning the same value leaves the attribute unchanged: - # person.name = 'Bill' - # person.name_changed? # => false - # person.name_change # => nil - # - # Which attributes have changed? - # person.name = 'bob' - # person.changed # => ['name'] - # person.changes # => { 'name' => ['Bill', 'bob'] } - # - # Before modifying an attribute in-place: - # person.name_will_change! - # person.name << 'by' - # person.name_change # => ['uncle bob', 'uncle bobby'] - module Dirty - extend ActiveSupport::Concern - - DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was'] - - included do - attribute_method_suffix *DIRTY_SUFFIXES - - alias_method_chain :write_attribute, :dirty - alias_method_chain :save, :dirty - alias_method_chain :save!, :dirty - alias_method_chain :update, :dirty - alias_method_chain :reload, :dirty - - superclass_delegating_accessor :partial_updates - self.partial_updates = true - end - - # Do any attributes have unsaved changes? - # person.changed? # => false - # person.name = 'bob' - # person.changed? # => true - def changed? - !changed_attributes.empty? - end - - # List of attributes with unsaved changes. - # person.changed # => [] - # person.name = 'bob' - # person.changed # => ['name'] - def changed - changed_attributes.keys - end - - # Map of changed attrs => [original value, new value]. - # person.changes # => {} - # person.name = 'bob' - # person.changes # => { 'name' => ['bill', 'bob'] } - def changes - changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h } - end - - # Attempts to +save+ the record and clears changed attributes if successful. - def save_with_dirty(*args) #:nodoc: - if status = save_without_dirty(*args) - changed_attributes.clear - end - status - end - - # Attempts to <tt>save!</tt> the record and clears changed attributes if successful. - def save_with_dirty!(*args) #:nodoc: - status = save_without_dirty!(*args) - changed_attributes.clear - status - end - - # <tt>reload</tt> the record and clears changed attributes. - def reload_with_dirty(*args) #:nodoc: - record = reload_without_dirty(*args) - changed_attributes.clear - record - end - - private - # Map of change <tt>attr => original value</tt>. - def changed_attributes - @changed_attributes ||= {} - end - - # Handle <tt>*_changed?</tt> for +method_missing+. - def attribute_changed?(attr) - changed_attributes.include?(attr) - end - - # Handle <tt>*_change</tt> for +method_missing+. - def attribute_change(attr) - [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) - end - - # Handle <tt>*_was</tt> for +method_missing+. - def attribute_was(attr) - attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) - end - - # Handle <tt>*_will_change!</tt> for +method_missing+. - def attribute_will_change!(attr) - changed_attributes[attr] = clone_attribute_value(:read_attribute, attr) - end - - # Wrap write_attribute to remember original attribute value. - def write_attribute_with_dirty(attr, value) - attr = attr.to_s - - # The attribute already has an unsaved change. - if changed_attributes.include?(attr) - old = changed_attributes[attr] - changed_attributes.delete(attr) unless field_changed?(attr, old, value) - else - old = clone_attribute_value(:read_attribute, attr) - changed_attributes[attr] = old if field_changed?(attr, old, value) - end - - # Carry on. - write_attribute_without_dirty(attr, value) - end - - def update_with_dirty - if partial_updates? - # Serialized attributes should always be written in case they've been - # changed in place. - update_without_dirty(changed | self.class.serialized_attributes.keys) - else - update_without_dirty - end - end - - def field_changed?(attr, old, value) - if column = column_for_attribute(attr) - if column.number? && column.null && (old.nil? || old == 0) && value.blank? - # For nullable numeric 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 - else - value = column.type_cast(value) - end - end - - old != value - end - - module ClassMethods - def self.extended(base) - class << base - alias_method_chain :alias_attribute, :dirty - end - end - - def alias_attribute_with_dirty(new_name, old_name) - alias_attribute_without_dirty(new_name, old_name) - DIRTY_SUFFIXES.each do |suffix| - module_eval <<-STR, __FILE__, __LINE__+1 - def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end - STR - end - end - end - end -end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 3963baa6b8..adb3a3f75e 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -388,13 +388,11 @@ module ActiveRecord end def rollback(migrations_path, steps=1) - migrator = self.new(:down, migrations_path) - start_index = migrator.migrations.index(migrator.current_migration) - - return unless start_index - - finish = migrator.migrations[start_index + steps] - down(migrations_path, finish ? finish.version : 0) + move(:down, migrations_path, steps) + end + + def forward(migrations_path, steps=1) + move(:up, migrations_path, steps) end def up(migrations_path, target_version = nil) @@ -430,6 +428,19 @@ module ActiveRecord # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}" end + + private + + def move(direction, migrations_path, steps) + migrator = self.new(direction, migrations_path) + start_index = migrator.migrations.index(migrator.current_migration) + + if start_index + finish = migrator.migrations[start_index + steps] + version = finish ? finish.version : 0 + send(direction, migrations_path, version) + end + end end def initialize(direction, migrations_path, target_version = nil) diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 5d88012e4f..c8e1b4f53a 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -84,7 +84,6 @@ HEADER elsif @connection.respond_to?(:primary_key) pk = @connection.primary_key(table) end - pk ||= 'id' tbl.print " create_table #{table.inspect}" if columns.detect { |c| c.name == pk } diff --git a/activerecord/lib/active_record/state_machine.rb b/activerecord/lib/active_record/state_machine.rb new file mode 100644 index 0000000000..aebd03344a --- /dev/null +++ b/activerecord/lib/active_record/state_machine.rb @@ -0,0 +1,24 @@ +module ActiveRecord + module StateMachine #:nodoc: + extend ActiveSupport::Concern + include ActiveModel::StateMachine + + included do + before_validation :set_initial_state + validates_presence_of :state + end + + protected + def write_state(state_machine, state) + update_attributes! :state => state.to_s + end + + def read_state(state_machine) + self.state.to_sym + end + + def set_initial_state + self.state ||= self.class.state_machine.initial_state.to_s + end + end +end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 80530194ff..88136597e3 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -112,23 +112,14 @@ class AdapterTest < ActiveRecord::TestCase def test_add_limit_offset_should_sanitize_sql_injection_for_limit_without_comas sql_inject = "1 select * from schema" - assert_equal " LIMIT 1", @connection.add_limit_offset!("", :limit=>sql_inject) - if current_adapter?(:MysqlAdapter) - assert_equal " LIMIT 7, 1", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) - else - assert_equal " LIMIT 1 OFFSET 7", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) - end + assert_no_match /schema/, @connection.add_limit_offset!("", :limit=>sql_inject) + assert_no_match /schema/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) end def test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas sql_inject = "1, 7 procedure help()" - if current_adapter?(:MysqlAdapter) - assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit=>sql_inject) - assert_equal " LIMIT 7, 1", @connection.add_limit_offset!("", :limit=> '1 ; DROP TABLE USERS', :offset=>7) - else - assert_equal " LIMIT 1,7", @connection.add_limit_offset!("", :limit=>sql_inject) - assert_equal " LIMIT 1,7 OFFSET 7", @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) - end + assert_no_match /procedure/, @connection.add_limit_offset!("", :limit=>sql_inject) + assert_no_match /procedure/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) end def test_uniqueness_violations_are_translated_to_specific_exception @@ -141,7 +132,13 @@ class AdapterTest < ActiveRecord::TestCase def test_foreign_key_violations_are_translated_to_specific_exception unless @connection.adapter_name == 'SQLite' assert_raises(ActiveRecord::InvalidForeignKey) do - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method + if @connection.prefetch_primary_key? + id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) + @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" + else + @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + end end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index ab6f752243..784c484178 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -293,7 +293,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_new_record_with_foreign_key_but_no_object c = Client.new("firm_id" => 1) - assert_equal Firm.find(:first), c.firm_with_basic_id + # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first + assert_equal Firm.find(:first, :order => "id"), c.firm_with_basic_id end def test_forgetting_the_load_when_foreign_key_enters_late @@ -301,7 +302,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_nil c.firm_with_basic_id c.firm_id = 1 - assert_equal Firm.find(:first), c.firm_with_basic_id + # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first + assert_equal Firm.find(:first, :order => "id"), c.firm_with_basic_id end def test_field_name_same_as_foreign_key diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 4cf49be668..811ebfbe3f 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -813,7 +813,12 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_include_has_many_using_primary_key expected = Firm.find(1).clients_using_primary_key.sort_by &:name - firm = Firm.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name' + # Oracle adapter truncates alias to 30 characters + if current_adapter?(:OracleAdapter) + firm = Firm.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies'[0,30]+'.name' + else + firm = Firm.find 1, :include => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name' + end assert_no_queries do assert_equal expected, firm.clients_using_primary_key end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 14b96caaae..11a159686e 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -284,12 +284,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_creation_respects_hash_condition - post = categories(:general).post_with_conditions.build(:body => '') + # in Oracle '' is saved as null therefore need to save ' ' in not null column + post = categories(:general).post_with_conditions.build(:body => ' ') assert post.save assert_equal 'Yet Another Testing Title', post.title - another_post = categories(:general).post_with_conditions.create(:body => '') + # in Oracle '' is saved as null therefore need to save ' ' in not null column + another_post = categories(:general).post_with_conditions.create(:body => ' ') assert !another_post.new_record? assert_equal 'Yet Another Testing Title', another_post.title diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 15919e2289..a3d92c3bdb 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -24,28 +24,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase companies(:first_firm).clients_of_firm.each {|f| } end + # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first def test_counting_with_counter_sql - assert_equal 2, Firm.find(:first).clients.count + assert_equal 2, Firm.find(:first, :order => "id").clients.count end def test_counting - assert_equal 2, Firm.find(:first).plain_clients.count + assert_equal 2, Firm.find(:first, :order => "id").plain_clients.count end def test_counting_with_empty_hash_conditions - assert_equal 2, Firm.find(:first).plain_clients.count(:conditions => {}) + assert_equal 2, Firm.find(:first, :order => "id").plain_clients.count(:conditions => {}) end def test_counting_with_single_conditions - assert_equal 1, Firm.find(:first).plain_clients.count(:conditions => ['name=?', "Microsoft"]) + assert_equal 1, Firm.find(:first, :order => "id").plain_clients.count(:conditions => ['name=?', "Microsoft"]) end def test_counting_with_single_hash - assert_equal 1, Firm.find(:first).plain_clients.count(:conditions => {:name => "Microsoft"}) + assert_equal 1, Firm.find(:first, :order => "id").plain_clients.count(:conditions => {:name => "Microsoft"}) end def test_counting_with_column_name_and_hash - assert_equal 2, Firm.find(:first).plain_clients.count(:name) + assert_equal 2, Firm.find(:first, :order => "id").plain_clients.count(:name) end def test_counting_with_association_limit @@ -55,12 +56,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding - assert_equal 2, Firm.find(:first).clients.length + assert_equal 2, Firm.find(:first, :order => "id").clients.length end def test_find_with_blank_conditions [[], {}, nil, ""].each do |blank| - assert_equal 2, Firm.find(:first).clients.find(:all, :conditions => blank).size + assert_equal 2, Firm.find(:first, :order => "id").clients.find(:all, :conditions => blank).size end end @@ -115,52 +116,53 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_triple_equality - assert !(Array === Firm.find(:first).clients) - assert Firm.find(:first).clients === Array + # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first + assert !(Array === Firm.find(:first, :order => "id").clients) + assert Firm.find(:first, :order => "id").clients === Array end def test_finding_default_orders - assert_equal "Summit", Firm.find(:first).clients.first.name + assert_equal "Summit", Firm.find(:first, :order => "id").clients.first.name end def test_finding_with_different_class_name_and_order - assert_equal "Microsoft", Firm.find(:first).clients_sorted_desc.first.name + assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_sorted_desc.first.name end def test_finding_with_foreign_key - assert_equal "Microsoft", Firm.find(:first).clients_of_firm.first.name + assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_of_firm.first.name end def test_finding_with_condition - assert_equal "Microsoft", Firm.find(:first).clients_like_ms.first.name + assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_like_ms.first.name end def test_finding_with_condition_hash - assert_equal "Microsoft", Firm.find(:first).clients_like_ms_with_hash_conditions.first.name + assert_equal "Microsoft", Firm.find(:first, :order => "id").clients_like_ms_with_hash_conditions.first.name end def test_finding_using_primary_key - assert_equal "Summit", Firm.find(:first).clients_using_primary_key.first.name + assert_equal "Summit", Firm.find(:first, :order => "id").clients_using_primary_key.first.name end def test_finding_using_sql - firm = Firm.find(:first) + firm = Firm.find(:first, :order => "id") first_client = firm.clients_using_sql.first assert_not_nil first_client assert_equal "Microsoft", first_client.name assert_equal 1, firm.clients_using_sql.size - assert_equal 1, Firm.find(:first).clients_using_sql.size + assert_equal 1, Firm.find(:first, :order => "id").clients_using_sql.size end def test_counting_using_sql - assert_equal 1, Firm.find(:first).clients_using_counter_sql.size - assert Firm.find(:first).clients_using_counter_sql.any? - assert_equal 0, Firm.find(:first).clients_using_zero_counter_sql.size - assert !Firm.find(:first).clients_using_zero_counter_sql.any? + assert_equal 1, Firm.find(:first, :order => "id").clients_using_counter_sql.size + assert Firm.find(:first, :order => "id").clients_using_counter_sql.any? + assert_equal 0, Firm.find(:first, :order => "id").clients_using_zero_counter_sql.size + assert !Firm.find(:first, :order => "id").clients_using_zero_counter_sql.any? end def test_counting_non_existant_items_using_sql - assert_equal 0, Firm.find(:first).no_clients_using_counter_sql.size + assert_equal 0, Firm.find(:first, :order => "id").no_clients_using_counter_sql.size end def test_counting_using_finder_sql @@ -183,7 +185,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_ids - firm = Firm.find(:first) + firm = Firm.find(:first, :order => "id") assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find } @@ -203,7 +205,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_string_ids_when_using_finder_sql - firm = Firm.find(:first) + firm = Firm.find(:first, :order => "id") client = firm.clients_using_finder_sql.find("2") assert_kind_of Client, client @@ -219,7 +221,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_all - firm = Firm.find(:first) + firm = Firm.find(:first, :order => "id") assert_equal 2, firm.clients.find(:all, :conditions => "#{QUOTED_TYPE} = 'Client'").length assert_equal 1, firm.clients.find(:all, :conditions => "name = 'Summit'").length end @@ -264,24 +266,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_all_sanitized - firm = Firm.find(:first) + # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first + firm = Firm.find(:first, :order => "id") summit = firm.clients.find(:all, :conditions => "name = 'Summit'") assert_equal summit, firm.clients.find(:all, :conditions => ["name = ?", "Summit"]) assert_equal summit, firm.clients.find(:all, :conditions => ["name = :name", { :name => "Summit" }]) end def test_find_first - firm = Firm.find(:first) + firm = Firm.find(:first, :order => "id") client2 = Client.find(2) - assert_equal firm.clients.first, firm.clients.find(:first) - assert_equal client2, firm.clients.find(:first, :conditions => "#{QUOTED_TYPE} = 'Client'") + assert_equal firm.clients.first, firm.clients.find(:first, :order => "id") + assert_equal client2, firm.clients.find(:first, :conditions => "#{QUOTED_TYPE} = 'Client'", :order => "id") end def test_find_first_sanitized - firm = Firm.find(:first) + firm = Firm.find(:first, :order => "id") client2 = Client.find(2) - assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = ?", 'Client']) - assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }]) + assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = ?", 'Client'], :order => "id") + assert_equal client2, firm.clients.find(:first, :conditions => ["#{QUOTED_TYPE} = :type", { :type => 'Client' }], :order => "id") end def test_find_in_collection @@ -341,7 +344,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_with_bang_on_has_many_raises_when_record_not_saved assert_raise(ActiveRecord::RecordInvalid) do - firm = Firm.find(:first) + firm = Firm.find(:first, :order => "id") firm.plain_clients.create! end end @@ -731,7 +734,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_destroy_dependent_when_deleted_from_association - firm = Firm.find(:first) + # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first + firm = Firm.find(:first, :order => "id") assert_equal 2, firm.clients.size client = firm.clients.first @@ -798,7 +802,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_replace_with_less - firm = Firm.find(:first) + firm = Firm.find(:first, :order => "id") firm.clients = [companies(:first_client)] assert firm.save, "Could not save firm" firm.reload @@ -812,7 +816,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_replace_with_new - firm = Firm.find(:first) + firm = Firm.find(:first, :order => "id") firm.clients = [companies(:second_client), Client.new("name" => "New Client")] firm.save firm.reload @@ -1104,7 +1108,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_creating_using_primary_key - firm = Firm.find(:first) + firm = Firm.find(:first, :order => "id") client = firm.clients_using_primary_key.create!(:name => 'test') assert_equal firm.name, client.firm_name end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 8529ff0285..799ab52025 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -169,6 +169,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal peeps + 1, posts(:thinking).people.count end + def test_create_on_new_record + p = Post.new + + assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(:first_name => "mew") } + assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") } + end + def test_clear_associations assert_queries(2) { posts(:welcome);posts(:welcome).people(true) } @@ -292,4 +299,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys assert_equal 1, owners(:blackbeard).toys.count end + + def test_find_on_has_many_association_collection_with_include_and_conditions + post_with_no_comments = people(:michael).posts_with_no_comments.first + assert_equal post_with_no_comments, posts(:authorless) + end end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index b1060d01af..9da7fc2639 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -14,7 +14,9 @@ require 'models/citation' class AssociationsJoinModelTest < ActiveRecord::TestCase self.use_transactional_fixtures = false - fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books + fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books, + # Reload edges table from fixtures as otherwise repeated test was failing + :edges def test_has_many assert authors(:david).categories.include?(categories(:general)) @@ -343,14 +345,16 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_polymorphic_with_source_type - assert_equal posts(:welcome, :thinking), tags(:general).tagged_posts + # added sort by ID as otherwise Oracle select sometimes returned rows in different order + assert_equal posts(:welcome, :thinking).sort_by(&:id), tags(:general).tagged_posts.sort_by(&:id) end def test_eager_has_many_polymorphic_with_source_type tag_with_include = Tag.find(tags(:general).id, :include => :tagged_posts) desired = posts(:welcome, :thinking) assert_no_queries do - assert_equal desired, tag_with_include.tagged_posts + # added sort by ID as otherwise test using JRuby was failing as array elements were in different order + assert_equal desired.sort_by(&:id), tag_with_include.tagged_posts.sort_by(&:id) end assert_equal 5, tag_with_include.taggings.length end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 183be1e2f9..055590da0a 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -4,40 +4,43 @@ require 'models/minimalistic' class AttributeMethodsTest < ActiveRecord::TestCase fixtures :topics + def setup - @old_suffixes = ActiveRecord::Base.send(:attribute_method_suffixes).dup + @old_matchers = ActiveRecord::Base.send(:attribute_method_matchers).dup @target = Class.new(ActiveRecord::Base) @target.table_name = 'topics' end def teardown - ActiveRecord::Base.send(:attribute_method_suffixes).clear - ActiveRecord::Base.attribute_method_suffix *@old_suffixes + ActiveRecord::Base.send(:attribute_method_matchers).clear + ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers) end - def test_match_attribute_method_query_returns_match_data - assert_not_nil md = @target.match_attribute_method?('title=') - assert_equal 'title', md.pre_match - assert_equal ['='], md.captures - - %w(_hello_world ist! _maybe?).each do |suffix| - @target.class_eval "def attribute#{suffix}(*args) args end" - @target.attribute_method_suffix suffix - - assert_not_nil md = @target.match_attribute_method?("title#{suffix}") - assert_equal 'title', md.pre_match - assert_equal [suffix], md.captures - end - end - - def test_declared_attribute_method_affects_respond_to_and_method_missing + def test_undeclared_attribute_method_does_not_affect_respond_to_and_method_missing topic = @target.new(:title => 'Budget') assert topic.respond_to?('title') assert_equal 'Budget', topic.title assert !topic.respond_to?('title_hello_world') assert_raise(NoMethodError) { topic.title_hello_world } + end - %w(_hello_world _it! _candidate= able?).each do |suffix| + def test_declared_prefixed_attribute_method_affects_respond_to_and_method_missing + topic = @target.new(:title => 'Budget') + %w(default_ title_).each do |prefix| + @target.class_eval "def #{prefix}attribute(*args) args end" + @target.attribute_method_prefix prefix + + meth = "#{prefix}title" + assert topic.respond_to?(meth) + assert_equal ['title'], topic.send(meth) + assert_equal ['title', 'a'], topic.send(meth, 'a') + assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3) + end + end + + def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing + topic = @target.new(:title => 'Budget') + %w(_default _title_default _it! _candidate= able?).each do |suffix| @target.class_eval "def attribute#{suffix}(*args) args end" @target.attribute_method_suffix suffix @@ -49,6 +52,20 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing + topic = @target.new(:title => 'Budget') + [['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix| + @target.class_eval "def #{prefix}attribute#{suffix}(*args) args end" + @target.attribute_method_affix({ :prefix => prefix, :suffix => suffix }) + + meth = "#{prefix}title#{suffix}" + assert topic.respond_to?(meth) + assert_equal ['title'], topic.send(meth) + assert_equal ['title', 'a'], topic.send(meth, 'a') + assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3) + end + end + def test_should_unserialize_attributes_for_frozen_records myobj = {:value1 => :value2} topic = Topic.create("content" => myobj) @@ -58,13 +75,23 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_typecast_attribute_from_select_to_false topic = Topic.create(:title => 'Budget') - topic = Topic.find(:first, :select => "topics.*, 1=2 as is_test") + # Oracle does not support boolean expressions in SELECT + if current_adapter?(:OracleAdapter) + topic = Topic.find(:first, :select => "topics.*, 0 as is_test") + else + topic = Topic.find(:first, :select => "topics.*, 1=2 as is_test") + end assert !topic.is_test? end def test_typecast_attribute_from_select_to_true topic = Topic.create(:title => 'Budget') - topic = Topic.find(:first, :select => "topics.*, 2=2 as is_test") + # Oracle does not support boolean expressions in SELECT + if current_adapter?(:OracleAdapter) + topic = Topic.find(:first, :select => "topics.*, 1 as is_test") + else + topic = Topic.find(:first, :select => "topics.*, 2=2 as is_test") + end assert topic.is_test? end @@ -74,10 +101,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_primary_key_implemented - assert Class.new(ActiveRecord::Base).instance_method_already_implemented?('id') - end - def test_defined_kernel_methods_implemented_in_model %w(test name display y).each do |method| klass = Class.new ActiveRecord::Base diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index ddca5e962d..271086af8e 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -154,7 +154,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test end def test_save_fails_for_invalid_belongs_to - assert log = AuditLog.create(:developer_id => 0, :message => "") + # Oracle saves empty string as NULL therefore :message changed to one space + assert log = AuditLog.create(:developer_id => 0, :message => " ") log.developer = Developer.new assert !log.developer.valid? @@ -164,7 +165,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test end def test_save_succeeds_for_invalid_belongs_to_with_validate_false - assert log = AuditLog.create(:developer_id => 0, :message=> "") + # Oracle saves empty string as NULL therefore :message changed to one space + assert log = AuditLog.create(:developer_id => 0, :message=> " ") log.unvalidated_developer = Developer.new assert !log.unvalidated_developer.valid? @@ -666,7 +668,12 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase @pirate.catchphrase = '' @pirate.ship.name = '' @pirate.save(false) - assert_equal ['', ''], [@pirate.reload.catchphrase, @pirate.ship.name] + # Oracle saves empty string as NULL + if current_adapter?(:OracleAdapter) + assert_equal [nil, nil], [@pirate.reload.catchphrase, @pirate.ship.name] + else + assert_equal ['', ''], [@pirate.reload.catchphrase, @pirate.ship.name] + end end def test_should_allow_to_bypass_validations_on_associated_models_at_any_depth @@ -678,7 +685,12 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase @pirate.save(false) values = [@pirate.reload.catchphrase, @pirate.ship.name, *@pirate.ship.parts.map(&:name)] - assert_equal ['', '', '', ''], values + # Oracle saves empty string as NULL + if current_adapter?(:OracleAdapter) + assert_equal [nil, nil, nil, nil], values + else + assert_equal ['', '', '', ''], values + end end def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that @@ -756,7 +768,12 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase @ship.pirate.catchphrase = '' @ship.name = '' @ship.save(false) - assert_equal ['', ''], [@ship.reload.name, @ship.pirate.catchphrase] + # Oracle saves empty string as NULL + if current_adapter?(:OracleAdapter) + assert_equal [nil, nil], [@ship.reload.name, @ship.pirate.catchphrase] + else + assert_equal ['', ''], [@ship.reload.name, @ship.pirate.catchphrase] + end end def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that @@ -837,11 +854,20 @@ module AutosaveAssociationOnACollectionAssociationTests @pirate.send(@association_name).each { |child| child.name = '' } assert @pirate.save(false) - assert_equal ['', '', ''], [ - @pirate.reload.catchphrase, - @pirate.send(@association_name).first.name, - @pirate.send(@association_name).last.name - ] + # Oracle saves empty string as NULL + if current_adapter?(:OracleAdapter) + assert_equal [nil, nil, nil], [ + @pirate.reload.catchphrase, + @pirate.send(@association_name).first.name, + @pirate.send(@association_name).last.name + ] + else + assert_equal ['', '', ''], [ + @pirate.reload.catchphrase, + @pirate.send(@association_name).first.name, + @pirate.send(@association_name).last.name + ] + end end def test_should_validation_the_associated_models_on_create diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index e47f898485..16364141df 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -422,11 +422,6 @@ class BasicsTest < ActiveRecord::TestCase end - def test_reader_for_invalid_column_names - Topic.send(:define_read_method, "mumub-jumbo".to_sym, "mumub-jumbo", nil) - assert !Topic.generated_methods.include?("mumub-jumbo") - end - def test_non_attribute_access_and_assignment topic = Topic.new assert !topic.respond_to?("mumbo") @@ -469,6 +464,60 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc + with_env_tz 'America/New_York' do + with_active_record_default_timezone :utc do + time = Time.local(2000) + topic = Topic.create('written_on' => time) + saved_time = Topic.find(topic.id).written_on + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "EST"], time.to_a + assert_equal [0, 0, 5, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a + end + end + end + + def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc + with_env_tz 'America/New_York' do + with_active_record_default_timezone :utc do + Time.use_zone 'Central Time (US & Canada)' do + time = Time.zone.local(2000) + topic = Topic.create('written_on' => time) + saved_time = Topic.find(topic.id).written_on + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a + assert_equal [0, 0, 6, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a + end + end + end + end + + def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local + with_env_tz 'America/New_York' do + time = Time.utc(2000) + topic = Topic.create('written_on' => time) + saved_time = Topic.find(topic.id).written_on + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a + assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a + end + end + + def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local + with_env_tz 'America/New_York' do + with_active_record_default_timezone :local do + Time.use_zone 'Central Time (US & Canada)' do + time = Time.zone.local(2000) + topic = Topic.create('written_on' => time) + saved_time = Topic.find(topic.id).written_on + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a + assert_equal [0, 0, 1, 1, 1, 2000, 6, 1, false, "EST"], saved_time.to_a + end + end + end + end + def test_custom_mutator topic = Topic.find(1) # This mutator is protected in the class definition @@ -1222,6 +1271,23 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Topic.find(1).new_record?, false end + def test_destroyed_returns_boolean + developer = Developer.new + assert_equal developer.destroyed?, false + developer.destroy + assert_equal developer.destroyed?, true + + developer = Developer.first + assert_equal developer.destroyed?, false + developer.destroy + assert_equal developer.destroyed?, true + + developer = Developer.last + assert_equal developer.destroyed?, false + developer.delete + assert_equal developer.destroyed?, true + end + def test_clone topic = Topic.find(1) cloned_topic = nil @@ -2120,4 +2186,19 @@ class BasicsTest < ActiveRecord::TestCase def test_dup assert !Minimalistic.new.freeze.dup.frozen? end + + protected + def with_env_tz(new_tz = 'US/Eastern') + old_tz, ENV['TZ'] = ENV['TZ'], new_tz + yield + ensure + old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + end + + def with_active_record_default_timezone(zone) + old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone + yield + ensure + ActiveRecord::Base.default_timezone = old_zone + end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 75f52dfa4a..c2e02763f6 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -2,6 +2,8 @@ require "cases/helper" require 'models/company' require 'models/topic' require 'models/edge' +require 'models/club' +require 'models/organization' Company.has_many :accounts @@ -223,6 +225,10 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 15, companies(:rails_core).companies.sum(:id) end + def test_should_sum_scoped_field_with_from + assert_equal Club.count, Organization.clubs.count + end + def test_should_sum_scoped_field_with_conditions assert_equal 8, companies(:rails_core).companies.sum(:id, :conditions => 'id > 7') end @@ -298,7 +304,12 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_sum_expression - assert_equal '636', Account.sum("2 * credit_limit") + # Oracle adapter returns floating point value 636.0 after SUM + if current_adapter?(:OracleAdapter) + assert_equal 636, Account.sum("2 * credit_limit") + else + assert_equal '636', Account.sum("2 * credit_limit") + end end def test_count_with_from_option diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb index 6274d5250f..c689e97d83 100644 --- a/activerecord/test/cases/database_statements_test.rb +++ b/activerecord/test/cases/database_statements_test.rb @@ -6,7 +6,14 @@ class DatabaseStatementsTest < ActiveRecord::TestCase end def test_insert_should_return_the_inserted_id - id = @connection.insert("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") + # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method + if current_adapter?(:OracleAdapter) + sequence_name = "accounts_seq" + id_value = @connection.next_sequence_value(sequence_name) + id = @connection.insert("INSERT INTO accounts (id, firm_id,credit_limit) VALUES (accounts_seq.nextval,42,5000)", nil, :id, id_value, sequence_name) + else + id = @connection.insert("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") + end assert_not_nil id end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index ac95bac4ad..74571d923a 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -62,6 +62,16 @@ class DirtyTest < ActiveRecord::TestCase assert_equal parrot.name_change, parrot.title_change end + def test_reset_attribute! + pirate = Pirate.create!(:catchphrase => 'Yar!') + pirate.catchphrase = 'Ahoy!' + + pirate.reset_catchphrase! + assert_equal "Yar!", pirate.catchphrase + assert_equal Hash.new, pirate.changes + assert !pirate.catchphrase_changed? + end + def test_nullable_number_not_marked_as_changed_if_new_value_is_blank pirate = Pirate.new @@ -288,6 +298,16 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_save_should_not_save_serialized_attribute_with_partial_updates_if_not_present + with_partial_updates(Topic) do + Topic.create!(:author_name => 'Bill', :content => {:a => "a"}) + topic = Topic.first(:select => 'id, author_name') + topic.update_attribute :author_name, 'John' + topic = Topic.first + assert_not_nil topic.content + end + end + private def with_partial_updates(klass, on = true) old = klass.partial_updates? diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index d8f5695a0f..7b6bf597a8 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -156,10 +156,8 @@ class FinderTest < ActiveRecord::TestCase end def test_find_all_with_limit - entrants = Entrant.find(:all, :order => "id ASC", :limit => 2) - - assert_equal(2, entrants.size) - assert_equal(entrants(:first).name, entrants.first.name) + assert_equal(2, Entrant.find(:all, :limit => 2).size) + assert_equal(0, Entrant.find(:all, :limit => 0).size) end def test_find_all_with_prepared_limit_and_offset @@ -168,22 +166,23 @@ class FinderTest < ActiveRecord::TestCase assert_equal(2, entrants.size) assert_equal(entrants(:second).name, entrants.first.name) + assert_equal 3, Entrant.count entrants = Entrant.find(:all, :order => "id ASC", :limit => 2, :offset => 2) assert_equal(1, entrants.size) assert_equal(entrants(:third).name, entrants.first.name) end - def test_find_all_with_limit_and_offset_and_multiple_orderings - developers = Developer.find(:all, :order => "salary ASC, id DESC", :limit => 3, :offset => 1) - assert_equal ["David", "fixture_10", "fixture_9"], developers.collect {|d| d.name} - end + def test_find_all_with_limit_and_offset_and_multiple_order_clauses + first_three_posts = Post.find :all, :order => 'author_id, id', :limit => 3, :offset => 0 + second_three_posts = Post.find :all, :order => ' author_id,id ', :limit => 3, :offset => 3 + last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6 - def test_find_with_limit_and_condition - developers = Developer.find(:all, :order => "id DESC", :conditions => "salary = 100000", :limit => 3, :offset =>7) - assert_equal(1, developers.size) - assert_equal("fixture_3", developers.first.name) + assert_equal [[0,3],[1,1],[1,2]], first_three_posts.map { |p| [p.author_id, p.id] } + assert_equal [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] } + assert_equal [[2,7]], last_posts.map { |p| [p.author_id, p.id] } end + def test_find_with_group developers = Developer.find(:all, :group => "salary", :select => "salary") assert_equal 4, developers.size @@ -251,7 +250,7 @@ class FinderTest < ActiveRecord::TestCase def test_find_only_some_columns topic = Topic.find(1, :select => "author_name") - assert_raise(ActiveRecord::MissingAttributeError) {topic.title} + assert_raise(ActiveModel::MissingAttributeError) {topic.title} assert_equal "David", topic.author_name assert !topic.attribute_present?("title") #assert !topic.respond_to?("title") @@ -423,6 +422,42 @@ class FinderTest < ActiveRecord::TestCase assert_equal customers(:david), found_customer end + def test_condition_utc_time_interpolation_with_default_timezone_local + with_env_tz 'America/New_York' do + with_active_record_default_timezone :local do + topic = Topic.first + assert_equal topic, Topic.find(:first, :conditions => ['written_on = ?', topic.written_on.getutc]) + end + end + end + + def test_hash_condition_utc_time_interpolation_with_default_timezone_local + with_env_tz 'America/New_York' do + with_active_record_default_timezone :local do + topic = Topic.first + assert_equal topic, Topic.find(:first, :conditions => {:written_on => topic.written_on.getutc}) + end + end + end + + def test_condition_local_time_interpolation_with_default_timezone_utc + with_env_tz 'America/New_York' do + with_active_record_default_timezone :utc do + topic = Topic.first + assert_equal topic, Topic.find(:first, :conditions => ['written_on = ?', topic.written_on.getlocal]) + end + end + end + + def test_hash_condition_local_time_interpolation_with_default_timezone_utc + with_env_tz 'America/New_York' do + with_active_record_default_timezone :utc do + topic = Topic.first + assert_equal topic, Topic.find(:first, :conditions => {:written_on => topic.written_on.getlocal}) + end + end + end + def test_bind_variables assert_kind_of Firm, Company.find(:first, :conditions => ["name = ?", "37signals"]) assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"]) @@ -942,40 +977,6 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" } end - def test_find_all_with_limit - first_five_developers = Developer.find :all, :order => 'id ASC', :limit => 5 - assert_equal 5, first_five_developers.length - assert_equal 'David', first_five_developers.first.name - assert_equal 'fixture_5', first_five_developers.last.name - - no_developers = Developer.find :all, :order => 'id ASC', :limit => 0 - assert_equal 0, no_developers.length - end - - def test_find_all_with_limit_and_offset - first_three_developers = Developer.find :all, :order => 'id ASC', :limit => 3, :offset => 0 - second_three_developers = Developer.find :all, :order => 'id ASC', :limit => 3, :offset => 3 - last_two_developers = Developer.find :all, :order => 'id ASC', :limit => 2, :offset => 8 - - assert_equal 3, first_three_developers.length - assert_equal 3, second_three_developers.length - assert_equal 2, last_two_developers.length - - assert_equal 'David', first_three_developers.first.name - assert_equal 'fixture_4', second_three_developers.first.name - assert_equal 'fixture_9', last_two_developers.first.name - end - - def test_find_all_with_limit_and_offset_and_multiple_order_clauses - first_three_posts = Post.find :all, :order => 'author_id, id', :limit => 3, :offset => 0 - second_three_posts = Post.find :all, :order => ' author_id,id ', :limit => 3, :offset => 3 - last_posts = Post.find :all, :order => ' author_id, id ', :limit => 3, :offset => 6 - - assert_equal [[0,3],[1,1],[1,2]], first_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[1,4],[1,5],[1,6]], second_three_posts.map { |p| [p.author_id, p.id] } - assert_equal [[2,7]], last_posts.map { |p| [p.author_id, p.id] } - end - def test_find_all_with_join developers_on_project_one = Developer.find( :all, @@ -991,7 +992,7 @@ class FinderTest < ActiveRecord::TestCase def test_joins_dont_clobber_id first = Firm.find( :first, - :joins => 'INNER JOIN companies AS clients ON clients.firm_id = companies.id', + :joins => 'INNER JOIN companies clients ON clients.firm_id = companies.id', :conditions => 'companies.id = 1' ) assert_equal 1, first.id @@ -1087,4 +1088,18 @@ class FinderTest < ActiveRecord::TestCase ActiveRecord::Base.send(:replace_bind_variables, statement, vars) end end + + def with_env_tz(new_tz = 'US/Eastern') + old_tz, ENV['TZ'] = ENV['TZ'], new_tz + yield + ensure + old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + end + + def with_active_record_default_timezone(zone) + old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone + yield + ensure + ActiveRecord::Base.default_timezone = old_zone + end end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index b07d4f3521..eb3f03c91d 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -185,7 +185,7 @@ class FixturesTest < ActiveRecord::TestCase def test_binary_in_fixtures assert_equal 1, @binaries.size - data = File.read(ASSETS_ROOT + "/flowers.jpg") + data = File.open(ASSETS_ROOT + "/flowers.jpg", 'rb') { |f| f.read } data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding) data.freeze assert_equal data, @flowers.data diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 167d3abad9..5cd11e9799 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -137,7 +137,8 @@ class InheritanceTest < ActiveRecord::TestCase def test_update_all_within_inheritance Client.update_all "name = 'I am a client'" assert_equal "I am a client", Client.find(:all).first.name - assert_equal "37signals", Firm.find(:all).first.name + # Order by added as otherwise Oracle tests were failing because of different order of results + assert_equal "37signals", Firm.find(:all, :order => "id").first.name end def test_alt_update_all_within_inheritance diff --git a/activerecord/test/cases/invalid_date_test.rb b/activerecord/test/cases/invalid_date_test.rb index e2bb17c37f..99af7d2986 100644 --- a/activerecord/test/cases/invalid_date_test.rb +++ b/activerecord/test/cases/invalid_date_test.rb @@ -11,13 +11,23 @@ class InvalidDateTest < Test::Unit::TestCase valid_dates.each do |date_src| topic = Topic.new("last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s) - assert_equal(topic.last_read, Date.new(*date_src)) + # Oracle DATE columns are datetime columns and Oracle adapter returns Time value + if current_adapter?(:OracleAdapter) + assert_equal(topic.last_read.to_date, Date.new(*date_src)) + else + assert_equal(topic.last_read, Date.new(*date_src)) + end end invalid_dates.each do |date_src| assert_nothing_raised do topic = Topic.new({"last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s}) - assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behaviour of the Time object") + # Oracle DATE columns are datetime columns and Oracle adapter returns Time value + if current_adapter?(:OracleAdapter) + assert_equal(topic.last_read.to_date, Time.local(*date_src).to_date, "The date should be modified according to the behaviour of the Time object") + else + assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behaviour of the Time object") + end end end end diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index d8246f49b8..35f7bc5443 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -379,7 +379,8 @@ class NestedScopingTest < ActiveRecord::TestCase poor_jamis = developers(:poor_jamis) Developer.with_scope(:find => { :conditions => "salary < 100000" }) do Developer.with_scope(:find => { :offset => 1, :order => 'id asc' }) do - assert_sql /ORDER BY id asc / do + # Oracle adapter does not generated space after asc therefore trailing space removed from regex + assert_sql /ORDER BY id asc/ do assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc')) end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 215b5a427a..f0f21615e0 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -446,18 +446,22 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal Date, bob.favorite_day.class end - # Test DateTime column and defaults, including timezone. - # FIXME: moment of truth may be Time on 64-bit platforms. - if bob.moment_of_truth.is_a?(DateTime) - - with_env_tz 'US/Eastern' do - assert_equal DateTime.local_offset, bob.moment_of_truth.offset - assert_not_equal 0, bob.moment_of_truth.offset - assert_not_equal "Z", bob.moment_of_truth.zone - # US/Eastern is -5 hours from GMT - assert_equal Rational(-5, 24), bob.moment_of_truth.offset - assert_match /\A-05:?00\Z/, bob.moment_of_truth.zone #ruby 1.8.6 uses HH:MM, prior versions use HHMM - assert_equal DateTime::ITALY, bob.moment_of_truth.start + # Oracle adapter stores Time or DateTime with timezone value already in _before_type_cast column + # therefore no timezone change is done afterwards when default timezone is changed + unless current_adapter?(:OracleAdapter) + # Test DateTime column and defaults, including timezone. + # FIXME: moment of truth may be Time on 64-bit platforms. + if bob.moment_of_truth.is_a?(DateTime) + + with_env_tz 'US/Eastern' do + assert_equal DateTime.local_offset, bob.moment_of_truth.offset + assert_not_equal 0, bob.moment_of_truth.offset + assert_not_equal "Z", bob.moment_of_truth.zone + # US/Eastern is -5 hours from GMT + assert_equal Rational(-5, 24), bob.moment_of_truth.offset + assert_match /\A-05:?00\Z/, bob.moment_of_truth.zone #ruby 1.8.6 uses HH:MM, prior versions use HHMM + assert_equal DateTime::ITALY, bob.moment_of_truth.start + end end end @@ -571,7 +575,7 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Base.connection.create_table(:hats) do |table| table.column :hat_name, :string, :default => nil end - exception = if current_adapter?(:PostgreSQLAdapter) + exception = if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) ActiveRecord::StatementInvalid else ActiveRecord::ActiveRecordError @@ -625,7 +629,13 @@ if ActiveRecord::Base.connection.supports_migrations? table.column :hat_size, :integer table.column :hat_style, :string, :limit => 100 end - ActiveRecord::Base.connection.add_index "hats", ["hat_style", "hat_size"], :unique => true + # Oracle index names should be 30 or less characters + if current_adapter?(:OracleAdapter) + ActiveRecord::Base.connection.add_index "hats", ["hat_style", "hat_size"], :unique => true, + :name => 'index_hats_on_hat_style_size' + else + ActiveRecord::Base.connection.add_index "hats", ["hat_style", "hat_size"], :unique => true + end assert_nothing_raised { Person.connection.remove_column("hats", "hat_size") } ensure @@ -727,19 +737,20 @@ if ActiveRecord::Base.connection.supports_migrations? def test_change_column Person.connection.add_column 'people', 'age', :integer - old_columns = Person.connection.columns(Person.table_name, "#{name} Columns") + label = "test_change_column Columns" + old_columns = Person.connection.columns(Person.table_name, label) assert old_columns.find { |c| c.name == 'age' and c.type == :integer } assert_nothing_raised { Person.connection.change_column "people", "age", :string } - new_columns = Person.connection.columns(Person.table_name, "#{name} Columns") + new_columns = Person.connection.columns(Person.table_name, label) assert_nil new_columns.find { |c| c.name == 'age' and c.type == :integer } assert new_columns.find { |c| c.name == 'age' and c.type == :string } - old_columns = Topic.connection.columns(Topic.table_name, "#{name} Columns") + old_columns = Topic.connection.columns(Topic.table_name, label) assert old_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true } assert_nothing_raised { Topic.connection.change_column :topics, :approved, :boolean, :default => false } - new_columns = Topic.connection.columns(Topic.table_name, "#{name} Columns") + new_columns = Topic.connection.columns(Topic.table_name, label) assert_nil new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true } assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false } assert_nothing_raised { Topic.connection.change_column :topics, :approved, :boolean, :default => true } @@ -783,7 +794,12 @@ if ActiveRecord::Base.connection.supports_migrations? assert_nothing_raised { Person.connection.change_column :testings, :select, :string, :limit => 10 } - assert_nothing_raised { Person.connection.execute "insert into testings (#{Person.connection.quote_column_name('select')}) values ('7 chars')" } + # Oracle needs primary key value from sequence + if current_adapter?(:OracleAdapter) + assert_nothing_raised { Person.connection.execute "insert into testings (id, #{Person.connection.quote_column_name('select')}) values (testings_seq.nextval, '7 chars')" } + else + assert_nothing_raised { Person.connection.execute "insert into testings (#{Person.connection.quote_column_name('select')}) values ('7 chars')" } + end ensure Person.connection.drop_table :testings rescue nil end @@ -799,7 +815,12 @@ if ActiveRecord::Base.connection.supports_migrations? person_klass.reset_column_information assert_equal 99, person_klass.columns_hash["wealth"].default assert_equal false, person_klass.columns_hash["wealth"].null - assert_nothing_raised {person_klass.connection.execute("insert into testings (title) values ('tester')")} + # Oracle needs primary key value from sequence + if current_adapter?(:OracleAdapter) + assert_nothing_raised {person_klass.connection.execute("insert into testings (id, title) values (testings_seq.nextval, 'tester')")} + else + assert_nothing_raised {person_klass.connection.execute("insert into testings (title) values ('tester')")} + end # change column default to see that column doesn't lose its not null definition person_klass.connection.change_column_default "testings", "wealth", 100 @@ -1054,7 +1075,12 @@ if ActiveRecord::Base.connection.supports_migrations? end def test_migrator_db_has_no_schema_migrations_table - ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations;") + # Oracle adapter raises error if semicolon is present as last character + if current_adapter?(:OracleAdapter) + ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations") + else + ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations;") + end assert_nothing_raised do ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1) end @@ -1110,6 +1136,17 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal(0, ActiveRecord::Migrator.current_version) end + def test_migrator_forward + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1) + assert_equal(1, ActiveRecord::Migrator.current_version) + + ActiveRecord::Migrator.forward(MIGRATIONS_ROOT + "/valid", 2) + assert_equal(3, ActiveRecord::Migrator.current_version) + + ActiveRecord::Migrator.forward(MIGRATIONS_ROOT + "/valid") + assert_equal(3, ActiveRecord::Migrator.current_version) + end + def test_schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" @@ -1412,6 +1449,8 @@ if ActiveRecord::Base.connection.supports_migrations? def string_column if current_adapter?(:PostgreSQLAdapter) "character varying(255)" + elsif current_adapter?(:OracleAdapter) + 'VARCHAR2(255)' else 'varchar(255)' end @@ -1420,6 +1459,8 @@ if ActiveRecord::Base.connection.supports_migrations? def integer_column if current_adapter?(:MysqlAdapter) 'int(11)' + elsif current_adapter?(:OracleAdapter) + 'NUMBER(38)' else 'integer' end diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 283333fc04..4f559bcaa5 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -4,6 +4,23 @@ require 'models/company_in_module' class ModulesTest < ActiveRecord::TestCase fixtures :accounts, :companies, :projects, :developers + def setup + # need to make sure Object::Firm and Object::Client are not defined, + # so that constantize will not be able to cheat when having to load namespaced classes + @undefined_consts = {} + + [:Firm, :Client].each do |const| + @undefined_consts.merge! const => Object.send(:remove_const, const) if Object.const_defined?(const) + end + end + + def teardown + # reinstate the constants that we undefined in the setup + @undefined_consts.each do |constant, value| + Object.send :const_set, constant, value unless value.nil? + end + end + def test_module_spanning_associations firm = MyApplication::Business::Firm.find(:first) assert !firm.clients.empty?, "Firm should have clients" @@ -36,4 +53,29 @@ class ModulesTest < ActiveRecord::TestCase assert_equal 'companies', MyApplication::Business::Client.table_name, 'table_name for ActiveRecord model subclass' assert_equal 'company_contacts', MyApplication::Business::Client::Contact.table_name, 'table_name for ActiveRecord model enclosed by another ActiveRecord model' end + + def test_assign_ids + firm = MyApplication::Business::Firm.first + + assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do + firm.client_ids = [MyApplication::Business::Client.first.id] + end + end + + # need to add an eager loading condition to force the eager loading model into + # the old join model, to test that. See http://dev.rubyonrails.org/ticket/9640 + def test_eager_loading_in_modules + clients = [] + + assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do + clients << MyApplication::Business::Client.find(3, :include => {:firm => :account}, :conditions => 'accounts.id IS NOT NULL') + clients << MyApplication::Business::Client.find(3, :include => {:firm => :account}) + end + + clients.each do |client| + assert_no_queries do + assert_not_nil(client.firm.account) + end + end + end end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index f4fdc9a39d..2a729f0678 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -154,7 +154,8 @@ class NamedScopeTest < ActiveRecord::TestCase assert !authors(:david).posts.ranked_by_comments.limit(5).empty? assert_not_equal Post.ranked_by_comments.limit(5), authors(:david).posts.ranked_by_comments.limit(5) assert_not_equal Post.top(5), authors(:david).posts.top(5) - assert_equal authors(:david).posts.ranked_by_comments.limit(5), authors(:david).posts.top(5) + # Oracle sometimes sorts differently if WHERE condition is changed + assert_equal authors(:david).posts.ranked_by_comments.limit(5).sort_by(&:id), authors(:david).posts.top(5).sort_by(&:id) assert_equal Post.ranked_by_comments.limit(5), Post.top(5) end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index f31275163d..d033c1e760 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -4,6 +4,8 @@ require "models/ship" require "models/bird" require "models/parrot" require "models/treasure" +require "models/man" +require "models/interest" require 'active_support/hash_with_indifferent_access' module AssertRaiseWithMessage @@ -470,6 +472,41 @@ module NestedAttributesOnACollectionAssociationTests assert Pirate.reflect_on_association(@association_name).options[:autosave] end + def test_validate_presence_of_parent__works_with_inverse_of + Man.accepts_nested_attributes_for(:interests) + assert_equal :man, Man.reflect_on_association(:interests).options[:inverse_of] + assert_equal :interests, Interest.reflect_on_association(:man).options[:inverse_of] + + repair_validations(Interest) do + Interest.validates_presence_of(:man) + assert_difference 'Man.count' do + assert_difference 'Interest.count', 2 do + man = Man.create!(:name => 'John', + :interests_attributes => [{:topic=>'Cars'}, {:topic=>'Sports'}]) + assert_equal 2, man.interests.count + end + end + end + end + + def test_validate_presence_of_parent__fails_without_inverse_of + Man.accepts_nested_attributes_for(:interests) + Man.reflect_on_association(:interests).options.delete(:inverse_of) + Interest.reflect_on_association(:man).options.delete(:inverse_of) + + repair_validations(Interest) do + Interest.validates_presence_of(:man) + assert_no_difference ['Man.count', 'Interest.count'] do + man = Man.create(:name => 'John', + :interests_attributes => [{:topic=>'Cars'}, {:topic=>'Sports'}]) + assert !man.errors[:interests_man].empty? + end + end + # restore :inverse_of + Man.reflect_on_association(:interests).options[:inverse_of] = :man + Interest.reflect_on_association(:man).options[:inverse_of] = :interests + end + private def association_setter diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index f90a66d1dc..2af6a56b6a 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -50,7 +50,12 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_does_not_wrap_string_results_in_arrays Task.cache do - assert_instance_of String, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") + # Oracle adapter returns count() as Fixnum or Float + if current_adapter?(:OracleAdapter) + assert Task.connection.select_value("SELECT count(*) AS count_all FROM tasks").is_a?(Numeric) + else + assert_instance_of String, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") + end end end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 9612b0beb6..e6a77f626b 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -114,6 +114,11 @@ class SchemaDumperTest < ActiveRecord::TestCase 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 + elsif current_adapter?(:OracleAdapter) + 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 @@ -193,6 +198,19 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_keeps_large_precision_integer_columns_as_decimal output = standard_dump - assert_match %r{t.decimal\s+"atoms_in_universe",\s+:precision => 55,\s+:scale => 0}, output + # Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers + if current_adapter?(:OracleAdapter) + assert_match %r{t.integer\s+"atoms_in_universe",\s+:precision => 38,\s+:scale => 0}, output + else + assert_match %r{t.decimal\s+"atoms_in_universe",\s+:precision => 55,\s+:scale => 0}, output + end + end + + def test_schema_dump_keeps_id_column_when_id_is_false_and_id_column_added + output = standard_dump + match = output.match(%r{create_table "goofy_string_id"(.*)do.*\n(.*)\n}) + assert_not_nil(match, "goofy_string_id table not found") + assert_match %r(:id => false), match[1], "no table id not preserved" + assert_match %r{t.string[[:space:]]+"id",[[:space:]]+:null => false$}, match[2], "non-primary key id column not preserved" end end diff --git a/activerecord/test/cases/state_machine_test.rb b/activerecord/test/cases/state_machine_test.rb new file mode 100644 index 0000000000..5d13668bab --- /dev/null +++ b/activerecord/test/cases/state_machine_test.rb @@ -0,0 +1,42 @@ +require 'cases/helper' +require 'models/traffic_light' + +class StateMachineTest < ActiveRecord::TestCase + def setup + @light = TrafficLight.create! + end + + test "states initial state" do + assert @light.off? + assert_equal :off, @light.current_state + end + + test "transition to a valid state" do + @light.reset + assert @light.red? + assert_equal :red, @light.current_state + + @light.green_on + assert @light.green? + assert_equal :green, @light.current_state + end + + test "transition does not persist state" do + @light.reset + assert_equal :red, @light.current_state + @light.reload + assert_equal "off", @light.state + end + + test "transition does persists state" do + @light.reset! + assert_equal :red, @light.current_state + @light.reload + assert_equal "red", @light.state + end + + test "transition to an invalid state" do + assert_raise(ActiveModel::StateMachine::InvalidTransition) { @light.yellow_on } + assert_equal :off, @light.current_state + end +end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index b1203c12ed..278a7a6a06 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -3,6 +3,9 @@ require "cases/helper" require 'models/topic' require 'models/reply' require 'models/owner' +require 'models/pet' +require 'models/man' +require 'models/interest' class AssociationValidationTest < ActiveRecord::TestCase fixtures :topics, :owners @@ -98,4 +101,24 @@ class AssociationValidationTest < ActiveRecord::TestCase end end end + + def test_validates_presence_of_belongs_to_association__parent_is_new_record + repair_validations(Interest) do + # Note that Interest and Man have the :inverse_of option set + Interest.validates_presence_of(:man) + man = Man.new(:name => 'John') + interest = man.interests.build(:topic => 'Airplanes') + assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated" + end + end + + def test_validates_presence_of_belongs_to_association__existing_parent + repair_validations(Interest) do + Interest.validates_presence_of(:man) + man = Man.create!(:name => 'John') + interest = man.interests.build(:topic => 'Airplanes') + assert interest.valid?, "Expected interest to be valid, but was not. Interest should have a man object associated" + end + end + end diff --git a/activerecord/test/connections/native_oracle/connection.rb b/activerecord/test/connections/native_oracle/connection.rb index 0954b27f87..c8183dc0fb 100644 --- a/activerecord/test/connections/native_oracle/connection.rb +++ b/activerecord/test/connections/native_oracle/connection.rb @@ -1,27 +1,68 @@ +# gem "rsim-activerecord-oracle_enhanced-adapter" +# gem "activerecord-oracle_enhanced-adapter", ">=1.2.1" +# uses local copy of oracle_enhanced adapter +$:.unshift("../../oracle-enhanced/lib") +require 'active_record/connection_adapters/oracle_enhanced_adapter' +# gem "activerecord-jdbc-adapter" +# require 'active_record/connection_adapters/jdbc_adapter' + +# otherwise failed with silence_warnings method missing exception +require 'active_support/core_ext/kernel/reporting' + print "Using Oracle\n" require_dependency 'models/course' require 'logger' -ActiveRecord::Base.logger = Logger.new STDOUT -ActiveRecord::Base.logger.level = Logger::WARN +# ActiveRecord::Base.logger = Logger.new STDOUT +# ActiveRecord::Base.logger.level = Logger::WARN +ActiveRecord::Base.logger = Logger.new("debug.log") # Set these to your database connection strings -db = ENV['ARUNIT_DB'] || 'activerecord_unittest' +db = ENV['ARUNIT_DB_NAME'] = 'orcl' ActiveRecord::Base.configurations = { 'arunit' => { - :adapter => 'oracle', + :adapter => 'oracle_enhanced', + :database => db, + :host => "localhost", # used just by JRuby to construct JDBC connect string + # :adapter => "jdbc", + # :driver => "oracle.jdbc.driver.OracleDriver", + # :url => "jdbc:oracle:thin:@localhost:1521:#{db}", :username => 'arunit', :password => 'arunit', - :database => db, + :emulate_oracle_adapter => true }, 'arunit2' => { - :adapter => 'oracle', + :adapter => 'oracle_enhanced', + :database => db, + :host => "localhost", # used just by JRuby to construct JDBC connect string + # :adapter => "jdbc", + # :driver => "oracle.jdbc.driver.OracleDriver", + # :url => "jdbc:oracle:thin:@localhost:1521:#{db}", :username => 'arunit2', :password => 'arunit2', - :database => db + :emulate_oracle_adapter => true } } ActiveRecord::Base.establish_connection 'arunit' Course.establish_connection 'arunit2' + +# ActiveRecord::Base.connection.execute %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'} +# ActiveRecord::Base.connection.execute %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'} rescue nil + +# for assert_queries test helper +ActiveRecord::Base.connection.class.class_eval do + IGNORED_SELECT_SQL = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^\s*select .* from all_tab_columns/im] + + def select_with_query_record(sql, name = nil, return_column_names = false) + $queries_executed ||= [] + $queries_executed << sql unless IGNORED_SELECT_SQL.any? { |r| sql =~ r } + select_without_query_record(sql, name, return_column_names) + end + + alias_method_chain :select, :query_record +end + +# For JRuby Set default $KCODE to UTF8 +$KCODE = "UTF8" if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 22168468a6..1c05e523e0 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -73,12 +73,16 @@ class Firm < Company has_one :unvalidated_account, :foreign_key => "firm_id", :class_name => 'Account', :validate => false has_one :account_with_select, :foreign_key => "firm_id", :select => "id, firm_id", :class_name=>'Account' has_one :readonly_account, :foreign_key => "firm_id", :class_name => "Account", :readonly => true - has_one :account_using_primary_key, :primary_key => "firm_id", :class_name => "Account" + # added order by id as in fixtures there are two accounts for Rails Core + # Oracle tests were failing because of that as the second fixture was selected + has_one :account_using_primary_key, :primary_key => "firm_id", :class_name => "Account", :order => "id" has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete end class DependentFirm < Company - has_one :account, :foreign_key => "firm_id", :dependent => :nullify + # added order by id as in fixtures there are two accounts for Rails Core + # Oracle tests were failing because of that as the second fixture was selected + has_one :account, :foreign_key => "firm_id", :dependent => :nullify, :order => "id" has_many :companies, :foreign_key => 'client_of', :order => "id", :dependent => :nullify end diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 8b84c2fb5e..cdda7a44d4 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -13,7 +13,7 @@ module MyApplication has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id" has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' - has_one :account, :dependent => :destroy + has_one :account, :class_name => 'MyApplication::Billing::Account', :dependent => :destroy end class Client < Company diff --git a/activerecord/test/models/organization.rb b/activerecord/test/models/organization.rb index d79d5037c8..c85726169e 100644 --- a/activerecord/test/models/organization.rb +++ b/activerecord/test/models/organization.rb @@ -1,4 +1,6 @@ class Organization < ActiveRecord::Base has_many :member_details has_many :members, :through => :member_details + + named_scope :clubs, { :from => 'clubs' } end
\ No newline at end of file diff --git a/activerecord/test/models/subject.rb b/activerecord/test/models/subject.rb index 3502943f3a..1b9d8107f8 100644 --- a/activerecord/test/models/subject.rb +++ b/activerecord/test/models/subject.rb @@ -1,4 +1,12 @@ -# used for OracleSynonymTest, see test/synonym_test_oci.rb +# used for OracleSynonymTest, see test/synonym_test_oracle.rb # class Subject < ActiveRecord::Base + protected + # added initialization of author_email_address in the same way as in Topic class + # as otherwise synonym test was failing + def after_initialize + if self.new_record? + self.author_email_address = 'test@test.com' + end + end end diff --git a/activerecord/test/models/traffic_light.rb b/activerecord/test/models/traffic_light.rb new file mode 100644 index 0000000000..f8cfddbef9 --- /dev/null +++ b/activerecord/test/models/traffic_light.rb @@ -0,0 +1,27 @@ +class TrafficLight < ActiveRecord::Base + include ActiveRecord::StateMachine + + state_machine do + state :off + + state :red + state :green + state :yellow + + event :red_on do + transitions :to => :red, :from => [:yellow] + end + + event :green_on do + transitions :to => :green, :from => [:red] + end + + event :yellow_on do + transitions :to => :yellow, :from => [:green] + end + + event :reset do + transitions :to => :red, :from => [:off] + end + end +end diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb index 2d87f34625..3314687445 100644 --- a/activerecord/test/schema/oracle_specific_schema.rb +++ b/activerecord/test/schema/oracle_specific_schema.rb @@ -2,6 +2,10 @@ ActiveRecord::Schema.define do execute "drop table test_oracle_defaults" rescue nil execute "drop sequence test_oracle_defaults_seq" rescue nil + execute "drop sequence companies_nonstd_seq" rescue nil + execute "drop synonym subjects" rescue nil + execute "drop table defaults" rescue nil + execute "drop sequence defaults_seq" rescue nil execute <<-SQL create table test_oracle_defaults ( @@ -16,4 +20,27 @@ create table test_oracle_defaults ( create sequence test_oracle_defaults_seq minvalue 10000 SQL + execute "create sequence companies_nonstd_seq minvalue 10000" + + execute "create synonym subjects for topics" + + execute <<-SQL + CREATE TABLE defaults ( + id integer not null, + modified_date date default sysdate, + modified_date_function date default sysdate, + fixed_date date default to_date('2004-01-01', 'YYYY-MM-DD'), + modified_time date default sysdate, + modified_time_function date default sysdate, + fixed_time date default TO_DATE('2004-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS'), + char1 varchar2(1) default 'Y', + char2 varchar2(50) default 'a varchar field', + char3 clob default 'a text field', + positive_integer integer default 1, + negative_integer integer default -1, + decimal_number number(3,2) default 2.78 + ) + SQL + execute "create sequence defaults_seq minvalue 10000" + end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 2b7d3856b7..5f60d5e137 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -104,7 +104,13 @@ ActiveRecord::Schema.define do create_table :comments, :force => true do |t| t.integer :post_id, :null => false - t.text :body, :null => false + # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in + # Oracle SELECT WHERE clause which causes many unit test failures + if current_adapter?(:OracleAdapter) + t.string :body, :null => false, :limit => 4000 + else + t.text :body, :null => false + end t.string :type end @@ -279,7 +285,12 @@ ActiveRecord::Schema.define do t.decimal :my_house_population, :precision => 2, :scale => 0 t.decimal :decimal_number_with_default, :precision => 3, :scale => 2, :default => 2.78 t.float :temperature - t.decimal :atoms_in_universe, :precision => 55, :scale => 0 + # Oracle supports precision up to 38 + if current_adapter?(:OracleAdapter) + t.decimal :atoms_in_universe, :precision => 38, :scale => 0 + else + t.decimal :atoms_in_universe, :precision => 55, :scale => 0 + end end create_table :orders, :force => true do |t| @@ -350,7 +361,13 @@ ActiveRecord::Schema.define do create_table :posts, :force => true do |t| t.integer :author_id t.string :title, :null => false - t.text :body, :null => false + # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in + # Oracle SELECT WHERE clause which causes many unit test failures + if current_adapter?(:OracleAdapter) + t.string :body, :null => false, :limit => 4000 + else + t.text :body, :null => false + end t.string :type t.integer :comments_count, :default => 0 t.integer :taggings_count, :default => 0 @@ -423,7 +440,13 @@ ActiveRecord::Schema.define do t.datetime :written_on t.time :bonus_time t.date :last_read - t.text :content + # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in + # Oracle SELECT WHERE clause which causes many unit test failures + if current_adapter?(:OracleAdapter) + t.string :content, :limit => 4000 + else + t.text :content + end t.boolean :approved, :default => true t.integer :replies_count, :default => 0 t.integer :parent_id @@ -448,6 +471,13 @@ ActiveRecord::Schema.define do t.integer :pet_id, :integer end + create_table :traffic_lights, :force => true do |t| + t.string :location + t.string :state + t.datetime :created_at + t.datetime :updated_at + end + create_table :treasures, :force => true do |t| t.column :name, :string t.column :looter_id, :integer |