aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG4
-rw-r--r--activerecord/Rakefile4
-rw-r--r--activerecord/lib/active_record.rb12
-rwxr-xr-xactiverecord/lib/active_record/associations.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb372
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb33
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb204
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb44
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb37
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb116
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb60
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb37
-rwxr-xr-xactiverecord/lib/active_record/base.rb115
-rw-r--r--activerecord/lib/active_record/calculations.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb1
-rw-r--r--activerecord/lib/active_record/dirty.rb186
-rw-r--r--activerecord/lib/active_record/migration.rb25
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb1
-rw-r--r--activerecord/lib/active_record/state_machine.rb24
-rw-r--r--activerecord/test/cases/adapter_test.rb25
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb6
-rw-r--r--activerecord/test/cases/associations/eager_test.rb7
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb6
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb80
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb12
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb10
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb75
-rw-r--r--activerecord/test/cases/autosave_association_test.rb46
-rwxr-xr-xactiverecord/test/cases/base_test.rb91
-rw-r--r--activerecord/test/cases/calculations_test.rb13
-rw-r--r--activerecord/test/cases/database_statements_test.rb9
-rw-r--r--activerecord/test/cases/dirty_test.rb20
-rw-r--r--activerecord/test/cases/finder_test.rb111
-rw-r--r--activerecord/test/cases/fixtures_test.rb2
-rw-r--r--activerecord/test/cases/inheritance_test.rb3
-rw-r--r--activerecord/test/cases/invalid_date_test.rb14
-rw-r--r--activerecord/test/cases/method_scoping_test.rb3
-rw-r--r--activerecord/test/cases/migration_test.rb83
-rw-r--r--activerecord/test/cases/modules_test.rb42
-rw-r--r--activerecord/test/cases/named_scope_test.rb3
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb37
-rw-r--r--activerecord/test/cases/query_cache_test.rb7
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb20
-rw-r--r--activerecord/test/cases/state_machine_test.rb42
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb23
-rw-r--r--activerecord/test/connections/native_oracle/connection.rb55
-rw-r--r--activerecord/test/models/company.rb8
-rw-r--r--activerecord/test/models/company_in_module.rb2
-rw-r--r--activerecord/test/models/organization.rb2
-rw-r--r--activerecord/test/models/subject.rb10
-rw-r--r--activerecord/test/models/traffic_light.rb27
-rw-r--r--activerecord/test/schema/oracle_specific_schema.rb27
-rw-r--r--activerecord/test/schema/schema.rb38
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