diff options
Diffstat (limited to 'activerecord/lib/active_record')
21 files changed, 663 insertions, 163 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 99c3ce5e62..e4ab69aa1b 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -43,7 +43,7 @@ module ActiveRecord # loading in a more high-level (application developer-friendly) manner. module ClassMethods protected - + # Eager loads the named associations for the given ActiveRecord record(s). # # In this description, 'association name' shall refer to the name passed @@ -94,8 +94,8 @@ module ActiveRecord raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol) preload_associations(records, parent, preload_options) reflection = reflections[parent] - parents = records.map {|record| record.send(reflection.name)}.flatten - unless parents.empty? || parents.first.nil? + parents = records.map {|record| record.send(reflection.name)}.flatten.compact + unless parents.empty? parents.first.class.preload_associations(parents, child) end end @@ -113,7 +113,7 @@ module ActiveRecord # unnecessarily records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records| raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection - + # 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus, # the following could call 'preload_belongs_to_association', # 'preload_has_many_association', etc. @@ -128,7 +128,7 @@ module ActiveRecord association_proxy.target.push(*[associated_record].flatten) end end - + def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) parent_records.each do |parent_record| parent_record.send("set_#{reflection_name}_target", associated_record) @@ -183,18 +183,19 @@ module ActiveRecord conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" conditions << append_conditions(reflection, preload_options) - associated_records = reflection.klass.find(:all, :conditions => [conditions, ids], - :include => options[:include], - :joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}", - :select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id", - :order => options[:order]) - + associated_records = reflection.klass.with_exclusive_scope do + reflection.klass.find(:all, :conditions => [conditions, ids], + :include => options[:include], + :joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}", + :select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id", + :order => options[:order]) + end set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id') end def preload_has_one_association(records, reflection, preload_options={}) return if records.first.send("loaded_#{reflection.name}?") - id_to_record_map, ids = construct_id_map(records) + id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) options = reflection.options records.each {|record| record.send("set_#{reflection.name}_target", nil)} if options[:through] @@ -204,9 +205,18 @@ module ActiveRecord unless through_records.empty? source = reflection.source_reflection.name through_records.first.class.preload_associations(through_records, source) - through_records.each do |through_record| - add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s], - reflection.name, through_record.send(source)) + if through_reflection.macro == :belongs_to + rev_id_to_record_map, rev_ids = construct_id_map(records, through_primary_key) + rev_primary_key = through_reflection.klass.primary_key + through_records.each do |through_record| + add_preloaded_record_to_collection(rev_id_to_record_map[through_record[rev_primary_key].to_s], + reflection.name, through_record.send(source)) + end + else + through_records.each do |through_record| + add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s], + reflection.name, through_record.send(source)) + end end end else @@ -219,7 +229,7 @@ module ActiveRecord options = reflection.options primary_key_name = reflection.through_reflection_primary_key_name - id_to_record_map, ids = construct_id_map(records, primary_key_name) + id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key]) records.each {|record| record.send(reflection.name).loaded} if options[:through] @@ -239,7 +249,7 @@ module ActiveRecord reflection.primary_key_name) end end - + def preload_through_records(records, reflection, through_association) through_reflection = reflections[through_association] through_primary_key = through_reflection.primary_key_name @@ -307,6 +317,7 @@ module ActiveRecord klasses_and_ids.each do |klass_and_id| klass_name, id_map = *klass_and_id + next if id_map.empty? klass = klass_name.constantize table_name = klass.quoted_table_name @@ -323,11 +334,13 @@ module ActiveRecord end conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}" conditions << append_conditions(reflection, preload_options) - associated_records = klass.find(:all, :conditions => [conditions, ids], + associated_records = klass.with_exclusive_scope do + klass.find(:all, :conditions => [conditions, ids], :include => options[:include], :select => options[:select], :joins => options[:joins], :order => options[:order]) + end set_association_single_records(id_map, reflection.name, associated_records, primary_key) end end @@ -345,13 +358,15 @@ module ActiveRecord conditions << append_conditions(reflection, preload_options) - reflection.klass.find(:all, + reflection.klass.with_exclusive_scope do + reflection.klass.find(:all, :select => (preload_options[:select] || options[:select] || "#{table_name}.*"), :include => preload_options[:include] || options[:include], :conditions => [conditions, ids], :joins => options[:joins], :group => preload_options[:group] || options[:group], :order => preload_options[:order] || options[:order]) + end end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 3fbbea43ed..86616abf52 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -22,7 +22,7 @@ module ActiveRecord through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect } - super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :connector => 'or'} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :connector => 'or'}?") + super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '}?") end end @@ -153,7 +153,7 @@ module ActiveRecord # #others.destroy_all | X | X | X # #others.find(*args) | X | X | X # #others.find_first | X | | - # #others.exist? | X | X | X + # #others.exists? | X | X | X # #others.uniq | X | X | X # #others.reset | X | X | X # @@ -652,7 +652,7 @@ module ActiveRecord # Returns the number of associated objects. # [collection.find(...)] # Finds an associated object according to the same rules as ActiveRecord::Base.find. - # [collection.exist?(...)] + # [collection.exists?(...)] # Checks whether an associated object with the given conditions exists. # Uses the same rules as ActiveRecord::Base.exists?. # [collection.build(attributes = {}, ...)] @@ -682,7 +682,7 @@ module ActiveRecord # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>) # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>) # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>) - # * <tt>Firm#clients.exist?(:name => 'ACME')</tt> (similar to <tt>Client.exist?(:name => 'ACME', :firm_id => firm.id)</tt>) + # * <tt>Firm#clients.exists?(:name => 'ACME')</tt> (similar to <tt>Client.exists?(:name => 'ACME', :firm_id => firm.id)</tt>) # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) # The declaration can also include an options hash to specialize the behavior of the association. @@ -1107,7 +1107,7 @@ module ActiveRecord # Finds an associated object responding to the +id+ and that # meets the condition that it has to be associated with this object. # Uses the same rules as ActiveRecord::Base.find. - # [collection.exist?(...)] + # [collection.exists?(...)] # Checks whether an associated object with the given conditions exists. # Uses the same rules as ActiveRecord::Base.exists?. # [collection.build(attributes = {})] @@ -1133,7 +1133,7 @@ module ActiveRecord # * <tt>Developer#projects.empty?</tt> # * <tt>Developer#projects.size</tt> # * <tt>Developer#projects.find(id)</tt> - # * <tt>Developer#clients.exist?(...)</tt> + # * <tt>Developer#clients.exists?(...)</tt> # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>) # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>) # The declaration may include an options hash to specialize the behavior of the association. @@ -1216,11 +1216,11 @@ module ActiveRecord # callbacks will be executed after the association is wiped out. old_method = "destroy_without_habtm_shim_for_#{reflection.name}" class_eval <<-end_eval unless method_defined?(old_method) - alias_method :#{old_method}, :destroy_without_callbacks - def destroy_without_callbacks - #{reflection.name}.clear - #{old_method} - end + alias_method :#{old_method}, :destroy_without_callbacks # alias_method :destroy_without_habtm_shim_for_posts, :destroy_without_callbacks + def destroy_without_callbacks # def destroy_without_callbacks + #{reflection.name}.clear # posts.clear + #{old_method} # destroy_without_habtm_shim_for_posts + end # end end_eval add_association_callbacks(reflection.name, options) @@ -1453,7 +1453,7 @@ module ActiveRecord dependent_conditions << sanitize_sql(reflection.options[:conditions]) if reflection.options[:conditions] dependent_conditions << extra_conditions if extra_conditions dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ") - + dependent_conditions = dependent_conditions.gsub('@', '\@') case reflection.options[:dependent] when :destroy method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym @@ -1463,22 +1463,22 @@ module ActiveRecord before_destroy method_name when :delete_all module_eval %Q{ - before_destroy do |record| - delete_all_has_many_dependencies(record, - "#{reflection.name}", - #{reflection.class_name}, - "#{dependent_conditions}") - end + before_destroy do |record| # before_destroy do |record| + delete_all_has_many_dependencies(record, # delete_all_has_many_dependencies(record, + "#{reflection.name}", # "posts", + #{reflection.class_name}, # Post, + %@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...) + end # end } when :nullify module_eval %Q{ - before_destroy do |record| - nullify_has_many_dependencies(record, - "#{reflection.name}", - #{reflection.class_name}, - "#{reflection.primary_key_name}", - "#{dependent_conditions}") - end + before_destroy do |record| # before_destroy do |record| + nullify_has_many_dependencies(record, # nullify_has_many_dependencies(record, + "#{reflection.name}", # "posts", + #{reflection.class_name}, # Post, + "#{reflection.primary_key_name}", # "user_id", + %@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...) + end # end } else raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, or :nullify (#{reflection.options[:dependent].inspect})" @@ -1731,6 +1731,11 @@ module ActiveRecord return sanitize_sql(sql) end + def tables_in_string(string) + return [] if string.blank? + string.scan(/([\.a-zA-Z_]+).?\./).flatten + end + def conditions_tables(options) # look in both sets of conditions conditions = [scope(:find, :conditions), options[:conditions]].inject([]) do |all, cond| @@ -1741,37 +1746,55 @@ module ActiveRecord else all << cond end end - conditions.join(' ').scan(/([\.a-zA-Z_]+).?\./).flatten + tables_in_string(conditions.join(' ')) end def order_tables(options) order = [options[:order], scope(:find, :order) ].join(", ") return [] unless order && order.is_a?(String) - order.scan(/([\.a-zA-Z_]+).?\./).flatten + tables_in_string(order) end def selects_tables(options) select = options[:select] return [] unless select && select.is_a?(String) - select.scan(/"?([\.a-zA-Z_]+)"?.?\./).flatten + tables_in_string(select) + end + + def joined_tables(options) + scope = scope(:find) + joins = options[:joins] + merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins]) + [table_name] + case merged_joins + when Symbol, Hash, Array + if array_of_strings?(merged_joins) + tables_in_string(merged_joins.join(' ')) + else + join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil) + join_dependency.join_associations.collect {|join_association| [join_association.aliased_join_table_name, join_association.aliased_table_name]}.flatten.compact + end + else + tables_in_string(merged_joins) + end end # Checks if the conditions reference a table other than the current model table - def include_eager_conditions?(options, tables = nil) - ((tables || conditions_tables(options)) - [table_name]).any? + def include_eager_conditions?(options, tables = nil, joined_tables = nil) + ((tables || conditions_tables(options)) - (joined_tables || joined_tables(options))).any? end # Checks if the query order references a table other than the current model's table. - def include_eager_order?(options, tables = nil) - ((tables || order_tables(options)) - [table_name]).any? + def include_eager_order?(options, tables = nil, joined_tables = nil) + ((tables || order_tables(options)) - (joined_tables || joined_tables(options))).any? end - def include_eager_select?(options) - (selects_tables(options) - [table_name]).any? + def include_eager_select?(options, joined_tables = nil) + (selects_tables(options) - (joined_tables || joined_tables(options))).any? end def references_eager_loaded_tables?(options) - include_eager_order?(options) || include_eager_conditions?(options) || include_eager_select?(options) + joined_tables = joined_tables(options) + include_eager_order?(options, nil, joined_tables) || include_eager_conditions?(options, nil, joined_tables) || include_eager_select?(options, joined_tables) end def using_limitable_reflections?(reflections) @@ -2148,7 +2171,7 @@ module ActiveRecord aliased_table_name, foreign_key, parent.aliased_table_name, - parent.primary_key + reflection.options[:primary_key] || parent.primary_key ] end when :belongs_to @@ -2175,7 +2198,7 @@ module ActiveRecord protected def aliased_table_name_for(name, suffix = nil) - if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{name.downcase}\son} + if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{active_record.connection.quote_table_name name.downcase}\son} @join_dependency.table_aliases[name] += 1 end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 0ff91fbdf8..0fefec1216 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -83,7 +83,11 @@ module ActiveRecord def to_ary load_target - @target.to_ary + if @target.is_a?(Array) + @target.to_ary + else + Array(@target) + end end def reset diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 59f1d3b867..676c4ace61 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -180,7 +180,10 @@ module ActiveRecord record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record? record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s else - record[@reflection.primary_key_name] = @owner.id unless @owner.new_record? + unless @owner.new_record? + primary_key = @reflection.options[:primary_key] || :id + record[@reflection.primary_key_name] = @owner.send(primary_key) + 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 a0bb3a45b0..2eeeb28d1f 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -160,9 +160,9 @@ module ActiveRecord end "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [ - @reflection.through_reflection.table_name, - @reflection.table_name, reflection_primary_key, - @reflection.through_reflection.table_name, source_primary_key, + @reflection.through_reflection.quoted_table_name, + @reflection.quoted_table_name, reflection_primary_key, + @reflection.through_reflection.quoted_table_name, source_primary_key, polymorphic_join ] end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 5d614442c3..cca012ed55 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -811,8 +811,7 @@ module ActiveRecord #:nodoc: # # ==== Parameters # - # * +updates+ - A string of column and value pairs that will be set on any records that match conditions. - # What goes into the SET clause. + # * +updates+ - A string of column and value pairs that will be set on any records that match conditions. This creates the SET clause of the generated SQL. # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info. # * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage. # @@ -1417,8 +1416,8 @@ module ActiveRecord #:nodoc: def benchmark(title, log_level = Logger::DEBUG, use_silence = true) if logger && logger.level <= log_level result = nil - seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield } - logger.add(log_level, "#{title} (#{'%.1f' % (seconds * 1000)}ms)") + ms = Benchmark.ms { result = use_silence ? silence { yield } : yield } + logger.add(log_level, '%s (%.1fms)' % [title, ms]) result else yield @@ -1457,7 +1456,10 @@ module ActiveRecord #:nodoc: def respond_to?(method_id, include_private = false) if match = DynamicFinderMatch.match(method_id) return true if all_attributes_exists?(match.attribute_names) + elsif match = DynamicScopeMatch.match(method_id) + return true if all_attributes_exists?(match.attribute_names) end + super end @@ -1495,11 +1497,16 @@ module ActiveRecord #:nodoc: end if scoped?(:find, :order) - scoped_order = reverse_sql_order(scope(:find, :order)) - scoped_methods.select { |s| s[:find].update(:order => scoped_order) } + scope = scope(:find) + original_scoped_order = scope[:order] + scope[:order] = reverse_sql_order(original_scoped_order) end - find_initial(options.merge({ :order => order })) + begin + find_initial(options.merge({ :order => order })) + ensure + scope[:order] = original_scoped_order if original_scoped_order + end end def reverse_sql_order(order_query) @@ -1805,7 +1812,11 @@ module ActiveRecord #:nodoc: # This also enables you to initialize a record if it is not found, such as find_or_initialize_by_amount(amount) # or find_or_create_by_user_and_password(user, password). # - # Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future + # Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that + # are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password]) + # respectively. + # + # Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future # attempts to use it do not run through method_missing. def method_missing(method_id, *arguments, &block) if match = DynamicFinderMatch.match(method_id) @@ -1814,10 +1825,31 @@ module ActiveRecord #:nodoc: if match.finder? finder = match.finder bang = match.bang? + # def self.find_by_login_and_activated(*args) + # options = args.extract_options! + # attributes = construct_attributes_from_arguments( + # [:login,:activated], + # args + # ) + # finder_options = { :conditions => attributes } + # validate_find_options(options) + # set_readonly_option!(options) + # + # if options[:conditions] + # with_scope(:find => finder_options) do + # find(:first, options) + # end + # else + # find(:first, options.merge(finder_options)) + # end + # end self.class_eval %{ def self.#{method_id}(*args) options = args.extract_options! - attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) + attributes = construct_attributes_from_arguments( + [:#{attribute_names.join(',:')}], + args + ) finder_options = { :conditions => attributes } validate_find_options(options) set_readonly_option!(options) @@ -1829,12 +1861,37 @@ module ActiveRecord #:nodoc: else find(:#{finder}, options.merge(finder_options)) end - #{'result || raise(RecordNotFound)' if bang} + #{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang} end }, __FILE__, __LINE__ send(method_id, *arguments) elsif match.instantiator? instantiator = match.instantiator + # def self.find_or_create_by_user_id(*args) + # guard_protected_attributes = false + # + # if args[0].is_a?(Hash) + # guard_protected_attributes = true + # attributes = args[0].with_indifferent_access + # find_attributes = attributes.slice(*[:user_id]) + # else + # find_attributes = attributes = construct_attributes_from_arguments([:user_id], args) + # end + # + # options = { :conditions => find_attributes } + # set_readonly_option!(options) + # + # record = find(:first, options) + # + # if record.nil? + # record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) } + # yield(record) if block_given? + # record.save + # record + # else + # record + # end + # end self.class_eval %{ def self.#{method_id}(*args) guard_protected_attributes = false @@ -1864,6 +1921,22 @@ module ActiveRecord #:nodoc: }, __FILE__, __LINE__ send(method_id, *arguments, &block) end + elsif match = DynamicScopeMatch.match(method_id) + attribute_names = match.attribute_names + super unless all_attributes_exists?(attribute_names) + if match.scope? + self.class_eval %{ + def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) + options = args.extract_options! # options = args.extract_options! + attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments( + [:#{attribute_names.join(',:')}], args # [:user_name, :password], args + ) # ) + # + scoped(:conditions => attributes) # scoped(:conditions => attributes) + end # end + }, __FILE__, __LINE__ + send(method_id, *arguments) + end else super end @@ -2052,10 +2125,10 @@ module ActiveRecord #:nodoc: end # Sets the default options for the model. The format of the - # <tt>method_scoping</tt> argument is the same as in with_scope. + # <tt>options</tt> argument is the same as in find. # # class Person < ActiveRecord::Base - # default_scope :find => { :order => 'last_name, first_name' } + # default_scope :order => 'last_name, first_name' # end def default_scope(options = {}) self.default_scoping << { :find => options, :create => (options.is_a?(Hash) && options.has_key?(:conditions)) ? options[:conditions] : {} } @@ -2402,9 +2475,9 @@ module ActiveRecord #:nodoc: 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. + # 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? - defined?(@new_record) && @new_record + @new_record || false end # :call-seq: @@ -3011,7 +3084,7 @@ module ActiveRecord #:nodoc: end Base.class_eval do - extend QueryCache + extend QueryCache::ClassMethods include Validations include Locking::Optimistic, Locking::Pessimistic include AttributeMethods diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index ccb79f547a..bbc290f721 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -123,6 +123,7 @@ module ActiveRecord connection_handler.retrieve_connection(self) end + # Returns true if +ActiveRecord+ is connected. def connected? connection_handler.connected?(self) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 950bd72101..00c71090f3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -14,12 +14,12 @@ module ActiveRecord def dirties_query_cache(base, *method_names) method_names.each do |method_name| base.class_eval <<-end_code, __FILE__, __LINE__ - def #{method_name}_with_query_dirty(*args) - clear_query_cache if @query_cache_enabled - #{method_name}_without_query_dirty(*args) - end - - alias_method_chain :#{method_name}, :query_dirty + def #{method_name}_with_query_dirty(*args) # def update_with_query_dirty(*args) + clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled + #{method_name}_without_query_dirty(*args) # update_without_query_dirty(*args) + end # end + # + alias_method_chain :#{method_name}, :query_dirty # alias_method_chain :update, :query_dirty end_code end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 58992f91da..273f823e7f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -32,10 +32,12 @@ module ActiveRecord @primary = nil end + # Returns +true+ if the column is either of type string or text. def text? type == :string || type == :text end + # Returns +true+ if the column is either of type integer, float or decimal. def number? type == :integer || type == :float || type == :decimal end @@ -295,7 +297,7 @@ module ActiveRecord # puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition" # end # end - # + # # def self.down # ... # end @@ -474,12 +476,12 @@ module ActiveRecord %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| class_eval <<-EOV - def #{column_type}(*args) - options = args.extract_options! - column_names = args - - column_names.each { |name| column(name, '#{column_type}', options) } - end + def #{column_type}(*args) # def string(*args) + options = args.extract_options! # options = args.extract_options! + column_names = args # column_names = args + # + column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) } + end # end EOV end @@ -674,24 +676,24 @@ module ActiveRecord # t.string(:goat, :sheep) %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| class_eval <<-EOV - def #{column_type}(*args) - options = args.extract_options! - column_names = args - - column_names.each do |name| - column = ColumnDefinition.new(@base, name, '#{column_type}') - if options[:limit] - column.limit = options[:limit] - elsif native['#{column_type}'.to_sym].is_a?(Hash) - column.limit = native['#{column_type}'.to_sym][:limit] - end - column.precision = options[:precision] - column.scale = options[:scale] - column.default = options[:default] - column.null = options[:null] - @base.add_column(@table_name, name, column.sql_type, options) - end - end + def #{column_type}(*args) # def string(*args) + options = args.extract_options! # options = args.extract_options! + column_names = args # column_names = args + # + column_names.each do |name| # column_names.each do |name| + column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string') + if options[:limit] # if options[:limit] + column.limit = options[:limit] # column.limit = options[:limit] + elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash) + column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit] + end # end + column.precision = options[:precision] # column.precision = options[:precision] + column.scale = options[:scale] # column.scale = options[:scale] + column.default = options[:default] # column.default = options[:default] + column.null = options[:null] # column.null = options[:null] + @base.add_column(@table_name, name, column.sql_type, options) # @base.add_column(@table_name, name, column.sql_type, options) + end # end + end # end EOV end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index b26185580c..5137b0f78c 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -184,9 +184,9 @@ module ActiveRecord # for more information about the effect of this option. attr_accessor :transactional_fixtures - def log_info(sql, name, seconds) + def log_info(sql, name, ms) if @logger && @logger.debug? - name = "#{name.nil? ? "SQL" : name} (#{sprintf("%.1f", seconds * 1000)}ms)" + name = '%s (%.1fms)' % [name || 'SQL', ms] @logger.debug(format_log_entry(name, sql.squeeze(' '))) end end @@ -195,9 +195,9 @@ module ActiveRecord def log(sql, name) if block_given? result = nil - seconds = Benchmark.realtime { result = yield } - @runtime += seconds - log_info(sql, name, seconds) + ms = Benchmark.ms { result = yield } + @runtime += ms + log_info(sql, name, ms) result else log_info(sql, name, 0) diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 3fbbb46f73..b2345fd571 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -13,23 +13,25 @@ module MysqlCompat #:nodoc: # C driver >= 2.7 returns null values in each_hash if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700) target.class_eval <<-'end_eval' - def all_hashes - rows = [] - each_hash { |row| rows << row } - rows - end + def all_hashes # def all_hashes + rows = [] # rows = [] + each_hash { |row| rows << row } # each_hash { |row| rows << row } + rows # rows + end # end end_eval # adapters before 2.7 don't have a version constant # and don't return null values in each_hash else target.class_eval <<-'end_eval' - def all_hashes - rows = [] - all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields } - each_hash { |row| rows << all_fields.dup.update(row) } - rows - end + def all_hashes # def all_hashes + rows = [] # rows = [] + all_fields = fetch_fields.inject({}) { |fields, f| # all_fields = fetch_fields.inject({}) { |fields, f| + fields[f.name] = nil; fields # fields[f.name] = nil; fields + } # } + each_hash { |row| rows << all_fields.dup.update(row) } # each_hash { |row| rows << all_fields.dup.update(row) } + rows # rows + end # end end_eval end @@ -312,6 +314,7 @@ module ActiveRecord rows end + # Executes a SQL query and returns a MySQL::Result object. Note that you have to free the Result object after you're done using it. def execute(sql, name = nil) #:nodoc: log(sql, name) { @connection.query(sql) } rescue ActiveRecord::StatementInvalid => exception @@ -429,7 +432,9 @@ module ActiveRecord def tables(name = nil) #:nodoc: tables = [] - execute("SHOW TABLES", name).each { |field| tables << field[0] } + result = execute("SHOW TABLES", name) + result.each { |field| tables << field[0] } + result.free tables end @@ -440,7 +445,8 @@ module ActiveRecord def indexes(table_name, name = nil)#:nodoc: indexes = [] current_index = nil - execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name).each do |row| + result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name) + result.each do |row| if current_index != row[2] next if row[2] == "PRIMARY" # skip the primary key current_index = row[2] @@ -449,13 +455,16 @@ module ActiveRecord indexes.last.columns << row[4] end + result.free indexes end def columns(table_name, name = nil)#:nodoc: sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" columns = [] - execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } + result = execute(sql, name) + result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } + result.free columns end @@ -536,9 +545,11 @@ module ActiveRecord # Returns a table's primary key and belonging sequence. def pk_and_sequence_for(table) #:nodoc: keys = [] - execute("describe #{quote_table_name(table)}").each_hash do |h| + result = execute("describe #{quote_table_name(table)}") + result.each_hash do |h| keys << h["Field"]if h["Key"] == "PRI" end + result.free keys.length == 1 ? [keys.first, nil] : nil end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index c4ef2be82e..5a8d99924d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -937,13 +937,13 @@ module ActiveRecord # should know about this but can't detect it there, so deal with it here. money_precision = (postgresql_version >= 80300) ? 19 : 10 PostgreSQLColumn.module_eval(<<-end_eval) - def extract_precision(sql_type) - if sql_type =~ /^money$/ - #{money_precision} - else - super - end - end + def extract_precision(sql_type) # def extract_precision(sql_type) + if sql_type =~ /^money$/ # if sql_type =~ /^money$/ + #{money_precision} # 19 + else # else + super # super + end # end + end # end end_eval configure_connection diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 84f8c0284e..9387cf8827 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -402,6 +402,10 @@ module ActiveRecord end def add_column(table_name, column_name, type, options = {}) #:nodoc: + if @connection.respond_to?(:transaction_active?) && @connection.transaction_active? + raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction' + end + alter_table(table_name) do |definition| definition.column(column_name, type, options) end diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb index a1760875ba..4c899f58e5 100644 --- a/activerecord/lib/active_record/dirty.rb +++ b/activerecord/lib/active_record/dirty.rb @@ -174,7 +174,7 @@ module ActiveRecord 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 #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end STR end end diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb new file mode 100644 index 0000000000..f796ba669a --- /dev/null +++ b/activerecord/lib/active_record/dynamic_scope_match.rb @@ -0,0 +1,25 @@ +module ActiveRecord + class DynamicScopeMatch + def self.match(method) + ds_match = self.new(method) + ds_match.scope ? ds_match : nil + end + + def initialize(method) + @scope = true + case method.to_s + when /^scoped_by_([_a-zA-Z]\w*)$/ + names = $1 + else + @scope = nil + end + @attribute_names = names && names.split('_and_') + end + + attr_reader :scope, :attribute_names + + def scope? + !@scope.nil? + end + end +end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index a8af89fcb9..eb92bc2545 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,20 +1,32 @@ module ActiveRecord - module QueryCache - # Enable the query cache within the block if Active Record is configured. - def cache(&block) - if ActiveRecord::Base.configurations.blank? - yield - else - connection.cache(&block) + class QueryCache + module ClassMethods + # Enable the query cache within the block if Active Record is configured. + def cache(&block) + if ActiveRecord::Base.configurations.blank? + yield + else + connection.cache(&block) + end end + + # Disable the query cache within the block if Active Record is configured. + def uncached(&block) + if ActiveRecord::Base.configurations.blank? + yield + else + connection.uncached(&block) + end + end + end + + def initialize(app) + @app = app end - # Disable the query cache within the block if Active Record is configured. - def uncached(&block) - if ActiveRecord::Base.configurations.blank? - yield - else - connection.uncached(&block) + def call(env) + ActiveRecord::Base.cache do + @app.call(env) end end end diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index d171b742f5..4749823b94 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -23,11 +23,12 @@ module ActiveRecord #:nodoc: # </topic> # # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>, - # <tt>:skip_instruct</tt>, <tt>:skip_types</tt> and <tt>:dasherize</tt>. + # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> . # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the # +attributes+ method. The default is to dasherize all column names, but you - # can disable this setting <tt>:dasherize</tt> to +false+. To not have the - # column type included in the XML output set <tt>:skip_types</tt> to +true+. + # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt> + # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>. + # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+. # # For instance: # @@ -178,13 +179,22 @@ module ActiveRecord #:nodoc: def root root = (options[:root] || @record.class.to_s.underscore).to_s - dasherize? ? root.dasherize : root + reformat_name(root) end def dasherize? !options.has_key?(:dasherize) || options[:dasherize] end + def camelize? + options.has_key?(:camelize) && options[:camelize] + end + + def reformat_name(name) + name = name.camelize if camelize? + dasherize? ? name.dasherize : name + end + def serializable_attributes serializable_attribute_names.collect { |name| Attribute.new(name, @record) } end @@ -212,7 +222,7 @@ module ActiveRecord #:nodoc: def add_tag(attribute) builder.tag!( - dasherize? ? attribute.name.dasherize : attribute.name, + reformat_name(attribute.name), attribute.value.to_s, attribute.decorations(!options[:skip_types]) ) @@ -220,8 +230,7 @@ module ActiveRecord #:nodoc: def add_associations(association, records, opts) if records.is_a?(Enumerable) - tag = association.to_s - tag = tag.dasherize if dasherize? + tag = reformat_name(association.to_s) if records.empty? builder.tag!(tag, :type => :array) else diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb new file mode 100644 index 0000000000..bd198c03b2 --- /dev/null +++ b/activerecord/lib/active_record/session_store.rb @@ -0,0 +1,319 @@ +module ActiveRecord + # A session store backed by an Active Record class. A default class is + # provided, but any object duck-typing to an Active Record Session class + # with text +session_id+ and +data+ attributes is sufficient. + # + # The default assumes a +sessions+ tables with columns: + # +id+ (numeric primary key), + # +session_id+ (text, or longtext if your session data exceeds 65K), and + # +data+ (text or longtext; careful if your session data exceeds 65KB). + # The +session_id+ column should always be indexed for speedy lookups. + # Session data is marshaled to the +data+ column in Base64 format. + # If the data you write is larger than the column's size limit, + # ActionController::SessionOverflowError will be raised. + # + # You may configure the table name, primary key, and data column. + # For example, at the end of <tt>config/environment.rb</tt>: + # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table' + # ActiveRecord::SessionStore::Session.primary_key = 'session_id' + # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data' + # Note that setting the primary key to the +session_id+ frees you from + # having a separate +id+ column if you don't want it. However, you must + # set <tt>session.model.id = session.session_id</tt> by hand! A before filter + # on ApplicationController is a good place. + # + # Since the default class is a simple Active Record, you get timestamps + # for free if you add +created_at+ and +updated_at+ datetime columns to + # the +sessions+ table, making periodic session expiration a snap. + # + # You may provide your own session class implementation, whether a + # feature-packed Active Record or a bare-metal high-performance SQL + # store, by setting + # ActiveRecord::SessionStore.session_class = MySessionClass + # You must implement these methods: + # self.find_by_session_id(session_id) + # initialize(hash_of_session_id_and_data) + # attr_reader :session_id + # attr_accessor :data + # save + # destroy + # + # The example SqlBypass class is a generic SQL session store. You may + # use it as a basis for high-performance database-specific stores. + class SessionStore < ActionController::Session::AbstractStore + # The default Active Record class. + class Session < ActiveRecord::Base + ## + # :singleton-method: + # Customizable data column name. Defaults to 'data'. + cattr_accessor :data_column_name + self.data_column_name = 'data' + + before_save :marshal_data! + before_save :raise_on_session_data_overflow! + + class << self + # Don't try to reload ARStore::Session in dev mode. + def reloadable? #:nodoc: + false + end + + def data_column_size_limit + @data_column_size_limit ||= columns_hash[@@data_column_name].limit + end + + # Hook to set up sessid compatibility. + def find_by_session_id(session_id) + setup_sessid_compatibility! + find_by_session_id(session_id) + end + + def marshal(data) + ActiveSupport::Base64.encode64(Marshal.dump(data)) if data + end + + def unmarshal(data) + Marshal.load(ActiveSupport::Base64.decode64(data)) if data + end + + def create_table! + connection.execute <<-end_sql + CREATE TABLE #{table_name} ( + id INTEGER PRIMARY KEY, + #{connection.quote_column_name('session_id')} TEXT UNIQUE, + #{connection.quote_column_name(@@data_column_name)} TEXT(255) + ) + end_sql + end + + def drop_table! + connection.execute "DROP TABLE #{table_name}" + end + + private + # Compatibility with tables using sessid instead of session_id. + def setup_sessid_compatibility! + # Reset column info since it may be stale. + reset_column_information + if columns_hash['sessid'] + def self.find_by_session_id(*args) + find_by_sessid(*args) + end + + define_method(:session_id) { sessid } + define_method(:session_id=) { |session_id| self.sessid = session_id } + else + def self.find_by_session_id(session_id) + find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id] + end + end + end + end + + # Lazy-unmarshal session state. + def data + @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {} + end + + attr_writer :data + + # Has the session been loaded yet? + def loaded? + !!@data + end + + private + def marshal_data! + return false if !loaded? + write_attribute(@@data_column_name, self.class.marshal(self.data)) + end + + # Ensures that the data about to be stored in the database is not + # larger than the data storage column. Raises + # ActionController::SessionOverflowError. + def raise_on_session_data_overflow! + return false if !loaded? + limit = self.class.data_column_size_limit + if loaded? and limit and read_attribute(@@data_column_name).size > limit + raise ActionController::SessionOverflowError + end + end + end + + # A barebones session store which duck-types with the default session + # store but bypasses Active Record and issues SQL directly. This is + # an example session model class meant as a basis for your own classes. + # + # The database connection, table name, and session id and data columns + # are configurable class attributes. Marshaling and unmarshaling + # are implemented as class methods that you may override. By default, + # marshaling data is + # + # ActiveSupport::Base64.encode64(Marshal.dump(data)) + # + # and unmarshaling data is + # + # Marshal.load(ActiveSupport::Base64.decode64(data)) + # + # This marshaling behavior is intended to store the widest range of + # binary session data in a +text+ column. For higher performance, + # store in a +blob+ column instead and forgo the Base64 encoding. + class SqlBypass + ## + # :singleton-method: + # Use the ActiveRecord::Base.connection by default. + cattr_accessor :connection + + ## + # :singleton-method: + # The table name defaults to 'sessions'. + cattr_accessor :table_name + @@table_name = 'sessions' + + ## + # :singleton-method: + # The session id field defaults to 'session_id'. + cattr_accessor :session_id_column + @@session_id_column = 'session_id' + + ## + # :singleton-method: + # The data field defaults to 'data'. + cattr_accessor :data_column + @@data_column = 'data' + + class << self + def connection + @@connection ||= ActiveRecord::Base.connection + end + + # Look up a session by id and unmarshal its data if found. + def find_by_session_id(session_id) + if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}") + new(:session_id => session_id, :marshaled_data => record['data']) + end + end + + def marshal(data) + ActiveSupport::Base64.encode64(Marshal.dump(data)) if data + end + + def unmarshal(data) + Marshal.load(ActiveSupport::Base64.decode64(data)) if data + end + + def create_table! + @@connection.execute <<-end_sql + CREATE TABLE #{table_name} ( + id INTEGER PRIMARY KEY, + #{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE, + #{@@connection.quote_column_name(data_column)} TEXT + ) + end_sql + end + + def drop_table! + @@connection.execute "DROP TABLE #{table_name}" + end + end + + attr_reader :session_id + attr_writer :data + + # Look for normal and marshaled data, self.find_by_session_id's way of + # telling us to postpone unmarshaling until the data is requested. + # We need to handle a normal data attribute in case of a new record. + def initialize(attributes) + @session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data] + @new_record = @marshaled_data.nil? + end + + def new_record? + @new_record + end + + # Lazy-unmarshal session state. + def data + unless @data + if @marshaled_data + @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil + else + @data = {} + end + end + @data + end + + def loaded? + !!@data + end + + def save + return false if !loaded? + marshaled_data = self.class.marshal(data) + + if @new_record + @new_record = false + @@connection.update <<-end_sql, 'Create session' + INSERT INTO #{@@table_name} ( + #{@@connection.quote_column_name(@@session_id_column)}, + #{@@connection.quote_column_name(@@data_column)} ) + VALUES ( + #{@@connection.quote(session_id)}, + #{@@connection.quote(marshaled_data)} ) + end_sql + else + @@connection.update <<-end_sql, 'Update session' + UPDATE #{@@table_name} + SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)} + WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)} + end_sql + end + end + + def destroy + unless @new_record + @@connection.delete <<-end_sql, 'Destroy session' + DELETE FROM #{@@table_name} + WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)} + end_sql + end + end + end + + # The class used for session storage. Defaults to + # ActiveRecord::SessionStore::Session + cattr_accessor :session_class + self.session_class = Session + + SESSION_RECORD_KEY = 'rack.session.record'.freeze + + private + def get_session(env, sid) + Base.silence do + sid ||= generate_sid + session = @@session_class.find_by_session_id(sid) + session ||= @@session_class.new(:session_id => sid, :data => {}) + env[SESSION_RECORD_KEY] = session + [sid, session.data] + end + end + + def set_session(env, sid, session_data) + Base.silence do + record = env[SESSION_RECORD_KEY] + record.data = session_data + return false unless record.save + + session_data = record.data + if session_data && session_data.respond_to?(:each_value) + session_data.each_value do |obj| + obj.clear_association_cache if obj.respond_to?(:clear_association_cache) + end + end + end + + return true + end + end +end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index a9e0efa6fe..8dbe80a01a 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -23,8 +23,8 @@ module ActiveRecord write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil? write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil? - write_attribute('updated_at', t) if respond_to?(:updated_at) - write_attribute('updated_on', t) if respond_to?(:updated_on) + write_attribute('updated_at', t) if respond_to?(:updated_at) && updated_at.nil? + write_attribute('updated_on', t) if respond_to?(:updated_on) && updated_on.nil? end create_without_timestamps end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 1e0185d39c..aaa298dc49 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -200,7 +200,7 @@ module ActiveRecord end def save_with_transactions! #:nodoc: - rollback_active_record_state! { transaction { save_without_transactions! } } + rollback_active_record_state! { self.class.transaction { save_without_transactions! } } end # Reset id and @new_record if the transaction rolls back. @@ -228,7 +228,7 @@ module ActiveRecord # instance. def with_transaction_returning_status(method, *args) status = nil - transaction do + self.class.transaction do status = send(method, *args) raise ActiveRecord::Rollback unless status end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 4b275ddd70..6a9690ba85 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -203,7 +203,6 @@ module ActiveRecord if attr == "base" full_messages << message else - #key = :"activerecord.att.#{@base.class.name.underscore.to_sym}.#{attr}" attr_name = @base.class.human_attribute_name(attr) full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message end @@ -1049,15 +1048,15 @@ module ActiveRecord protected # Overwrite this method for validation checks on all saves and use <tt>Errors.add(field, msg)</tt> for invalid attributes. - def validate #:doc: + def validate end # Overwrite this method for validation checks used only on creation. - def validate_on_create #:doc: + def validate_on_create end # Overwrite this method for validation checks used only on updates. - def validate_on_update # :doc: + def validate_on_update end end end |