From ce48b3103acd2f58931aa42b93073592b291114e Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Wed, 28 Apr 2010 16:24:47 -0300 Subject: Makes validates_acceptance_of to not override database fields [#4460 state:committed] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/lib/active_record/base.rb | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 2d7cfad80d..8542c52d0c 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -931,6 +931,10 @@ module ActiveRecord #:nodoc: subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } end + def attribute_method?(attribute) + super || column_names.include?(attribute.to_s.sub(/=$/, '')) + end + # Set the lookup ancestors for ActiveModel. def lookup_ancestors #:nodoc: klass = self -- cgit v1.2.3 From 209ab7e05b148b02a2f9598a809eb10588c92259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 28 Apr 2010 23:42:55 +0200 Subject: Fix failing test. --- activerecord/lib/active_record/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 8542c52d0c..9ed53cc4af 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -932,7 +932,7 @@ module ActiveRecord #:nodoc: end def attribute_method?(attribute) - super || column_names.include?(attribute.to_s.sub(/=$/, '')) + super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) end # Set the lookup ancestors for ActiveModel. -- cgit v1.2.3 From e33d304975f5b20b0ba819ab644a2a8f80ff3743 Mon Sep 17 00:00:00 2001 From: Ernie Miller Date: Wed, 28 Apr 2010 23:14:05 -0400 Subject: Fix eager loading of associations causing table name collisions [#4463 state:committed] Signed-off-by: Jeremy Kemper --- activerecord/lib/active_record/associations.rb | 77 +++++++++++++++++----- .../lib/active_record/relation/finder_methods.rb | 1 - .../lib/active_record/relation/query_methods.rb | 73 +++++++++++--------- .../associations/cascaded_eager_loading_test.rb | 9 +++ 4 files changed, 111 insertions(+), 49 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6dbee9f4bf..6c64210c92 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1737,6 +1737,14 @@ module ActiveRecord build(associations) end + def graft(*associations) + associations.each do |association| + join_associations.detect {|a| association == a} || + build(association.reflection.name, association.find_parent_in(self), association.join_class) + end + self + end + def join_associations @joins[1..-1].to_a end @@ -1745,6 +1753,16 @@ module ActiveRecord @joins[0] end + def count_aliases_from_table_joins(name) + quoted_name = join_base.active_record.connection.quote_table_name(name.downcase) + join_sql = join_base.table_joins.to_s.downcase + join_sql.blank? ? 0 : + # Table names + join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size + + # Table aliases + join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size + end + def instantiate(rows) rows.each_with_index do |row, i| primary_id = join_base.record_id(row) @@ -1789,22 +1807,22 @@ module ActiveRecord end protected - def build(associations, parent = nil) + def build(associations, parent = nil, join_class = Arel::InnerJoin) parent ||= @joins.last case associations when Symbol, String reflection = parent.reflections[associations.to_s.intern] or raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" @reflections << reflection - @joins << build_join_association(reflection, parent) + @joins << build_join_association(reflection, parent).with_join_class(join_class) when Array associations.each do |association| - build(association, parent) + build(association, parent, join_class) end when Hash associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| - build(name, parent) - build(associations[name]) + build(name, parent, join_class) + build(associations[name], nil, join_class) end else raise ConfigurationError, associations.inspect @@ -1881,6 +1899,12 @@ module ActiveRecord @table_joins = joins end + def ==(other) + other.is_a?(JoinBase) && + other.active_record == active_record && + other.table_joins == table_joins + end + def aliased_prefix "t0" end @@ -1946,6 +1970,27 @@ module ActiveRecord end end + def ==(other) + other.is_a?(JoinAssociation) && + other.reflection == reflection && + other.parent == parent + end + + def find_parent_in(other_join_dependency) + other_join_dependency.joins.detect do |join| + self.parent == join + end + end + + def join_class + @join_class ||= Arel::InnerJoin + end + + def with_join_class(join_class) + @join_class = join_class + self + end + def association_join return @join if @join @@ -2045,27 +2090,25 @@ module ActiveRecord end def join_relation(joining_relation, join = nil) - if (relations = relation).is_a?(Array) - joining_relation.joins(Relation::JoinOperation.new(relations.first, Arel::OuterJoin, association_join.first)). - joins(Relation::JoinOperation.new(relations.last, Arel::OuterJoin, association_join.last)) - else - joining_relation.joins(Relation::JoinOperation.new(relations, Arel::OuterJoin, association_join)) - end + joining_relation.joins(self.with_join_class(Arel::OuterJoin)) end 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+#{active_record.connection.quote_table_name name.downcase}\son} - @join_dependency.table_aliases[name] += 1 + if @join_dependency.table_aliases[name].zero? + @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name) end - unless @join_dependency.table_aliases[name].zero? - # if the table name has been used, then use an alias + if !@join_dependency.table_aliases[name].zero? # We need an alias name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" - table_index = @join_dependency.table_aliases[name] @join_dependency.table_aliases[name] += 1 - name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0 + if @join_dependency.table_aliases[name] == 1 # First time we've seen this name + # Also need to count the aliases from the table_aliases to avoid incorrect count + @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name) + end + table_index = @join_dependency.table_aliases[name] + name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1 else @join_dependency.table_aliases[name] += 1 end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3514d0a259..d6144dc206 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -188,7 +188,6 @@ module ActiveRecord def construct_relation_for_association_calculations including = (@eager_load_values + @includes_values).uniq join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel)) - relation = except(:includes, :eager_load, :preload) apply_join_dependency(relation, join_dependency) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 58af930446..7bca12d85e 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -80,6 +80,26 @@ module ActiveRecord @arel ||= build_arel end + def custom_join_sql(*joins) + arel = table + joins.each do |join| + next if join.blank? + + @implicit_readonly = true + + case join + when Hash, Array, Symbol + if array_of_strings?(join) + join_string = join.join(' ') + arel = arel.join(join_string) + end + else + arel = arel.join(join) + end + end + arel.joins(arel) + end + def build_arel arel = table @@ -88,50 +108,41 @@ module ActiveRecord joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq - # Build association joins first joins.each do |join| association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join) end - if association_joins.any? - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins.uniq, nil) - to_join = [] + stashed_association_joins = joins.select {|j| j.is_a?(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)} - join_dependency.join_associations.each do |association| - if (association_relation = association.relation).is_a?(Array) - to_join << [association_relation.first, association.association_join.first] - to_join << [association_relation.last, association.association_join.last] - else - to_join << [association_relation, association.association_join] - end - end + non_association_joins = (joins - association_joins - stashed_association_joins).reject {|j| j.blank?} + custom_joins = custom_join_sql(*non_association_joins) - to_join.each do |tj| - unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] } - joined_associations << tj - arel = arel.join(tj[0]).on(*tj[1]) - end - end - end + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins) - joins.each do |join| - next if join.blank? + join_dependency.graft(*stashed_association_joins) - @implicit_readonly = true + @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? - case join - when Relation::JoinOperation - arel = arel.join(join.relation, join.join_class).on(*join.on) - when Hash, Array, Symbol - if array_of_strings?(join) - join_string = join.join(' ') - arel = arel.join(join_string) - end + to_join = [] + + join_dependency.join_associations.each do |association| + if (association_relation = association.relation).is_a?(Array) + to_join << [association_relation.first, association.join_class, association.association_join.first] + to_join << [association_relation.last, association.join_class, association.association_join.last] else - arel = arel.join(join) + to_join << [association_relation, association.join_class, association.association_join] end end + to_join.each do |tj| + unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] } + joined_associations << tj + arel = arel.join(tj[0], tj[1]).on(*tj[2]) + end + end + + arel = arel.join(custom_joins) + @where_values.uniq.each do |where| next if where.blank? diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index ed2e2e9f8f..fe558f9d3b 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -29,6 +29,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 2, authors[1].categorizations.size end + def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations + assert_nothing_raised do + Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all + end + authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).all + assert_equal 1, assert_no_queries { authors.size } + assert_equal 9, assert_no_queries { authors[0].comments.size } + end + def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations authors = Author.find(:all, :include=>{:posts=>[:comments, :categorizations]}, :order=>"authors.id") assert_equal 2, authors.size -- cgit v1.2.3 From 2e9af3638d950ef840e1287f99e323887ec6a4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 29 Apr 2010 12:27:25 +0200 Subject: Move several configuration values from Hash to ActiveSupport::XmlMini, which both Hash and Array depends on. Also, refactored ActiveModel serializers to just use ActiveSupport::XmlMini.to_tag. As consequence, if a serialized attribute is an array or a hash, it's not encoded as yaml, but as a hash or array. --- activerecord/CHANGELOG | 2 + .../active_record/serializers/xml_serializer.rb | 85 +++++++++------------- activerecord/test/cases/base_test.rb | 5 +- activerecord/test/cases/xml_serialization_test.rb | 4 +- 4 files changed, 40 insertions(+), 56 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index fcb0e31f79..7864d735ad 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.0.0 [beta 4/release candidate] (unreleased)* +* Serialized attributes are not converted to YAML if they are any of the formats that can be serialized to XML (like Hash, Array and Strings). [José Valim] + * Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised. #1966 [Curtis Hawthorne] * PostgreSQL: drop support for old postgres driver. Use pg 0.9.0 or later. [Jeremy Kemper] diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 255b03433d..ed5964d923 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -182,16 +182,31 @@ module ActiveRecord #:nodoc: options[:except] |= Array.wrap(@serializable.class.inheritance_column) end + def add_extra_behavior + add_includes + end + + def add_includes + procs = options.delete(:procs) + @serializable.send(:serializable_add_includes, options) do |association, records, opts| + add_associations(association, records, opts) + end + options[:procs] = procs + end + + # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. def add_associations(association, records, opts) + association_name = association.to_s.singularize + merged_options = options.merge(opts).merge!(:root => association_name) + if records.is_a?(Enumerable) - tag = reformat_name(association.to_s) - type = options[:skip_types] ? {} : {:type => "array"} + tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) + type = options[:skip_types] ? { } : {:type => "array"} if records.empty? - builder.tag!(tag, type) + @builder.tag!(tag, type) else - builder.tag!(tag, type) do - association_name = association.to_s.singularize + @builder.tag!(tag, type) do records.each do |record| if options[:skip_types] record_type = {} @@ -200,60 +215,30 @@ module ActiveRecord #:nodoc: record_type = {:type => record_class} end - record.to_xml opts.merge(:root => association_name).merge(record_type) + record.to_xml merged_options.merge(record_type) end end end - else - if record = @serializable.send(association) - record.to_xml(opts.merge(:root => association)) - end - end - end - - def serialize - args = [root] - if options[:namespace] - args << {:xmlns=>options[:namespace]} - end - - if options[:type] - args << {:type=>options[:type]} - end - - builder.tag!(*args) do - add_attributes - procs = options.delete(:procs) - @serializable.send(:serializable_add_includes, options) { |association, records, opts| - add_associations(association, records, opts) - } - options[:procs] = procs - add_procs - yield builder if block_given? + elsif record = @serializable.send(association) + record.to_xml(merged_options) end end class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: - protected - def compute_type - type = @serializable.class.serialized_attributes.has_key?(name) ? :yaml : @serializable.class.columns_hash[name].type - - case type - when :text - :string - when :time - :datetime - else - type - end - end - end + def compute_type + type = @serializable.class.serialized_attributes.has_key?(name) ? + super : @serializable.class.columns_hash[name].type - class MethodAttribute < Attribute #:nodoc: - protected - def compute_type - Hash::XML_TYPE_NAMES[@serializable.send(name).class.name] || :string + case type + when :text + :string + when :time + :datetime + else + type end + end + protected :compute_type end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 2f4243a6aa..3623680de9 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -2085,6 +2085,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "topic", xml.root.name assert_equal "The First Topic" , xml.elements["//title"].text assert_equal "David" , xml.elements["//author-name"].text + assert_match "Have a nice day", xml.elements["//content"].text assert_equal "1", xml.elements["//id"].text assert_equal "integer" , xml.elements["//id"].attributes['type'] @@ -2095,10 +2096,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text assert_equal "datetime" , xml.elements["//written-on"].attributes['type'] - assert_match(/^--- Have a nice day\n/ , xml.elements["//content"].text) - assert_equal 'Have a nice day' , YAML.load(xml.elements["//content"].text) - assert_equal "yaml" , xml.elements["//content"].attributes['type'] - assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text assert_equal nil, xml.elements["//parent-id"].text diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index b1c75ec8cd..bd29ae2612 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -79,8 +79,8 @@ class DefaultXmlSerializationTest < ActiveRecord::TestCase assert_match %r{false}, @xml end - def test_should_serialize_yaml - assert_match %r{---\s?\n:gem: ruby\n}, @xml + def test_should_serialize_hash + assert_match %r{\s*ruby\s*}m, @xml end end -- cgit v1.2.3 From 3dfcb56e4585c8530be6dcc5129ae0c4d16c0dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 29 Apr 2010 13:24:24 +0200 Subject: ActiveRecord middlewares should be inserted before AD::Cascade [#4493 state:resolved]. --- activerecord/lib/active_record/railtie.rb | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index f3d21d4969..898df0a67a 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -61,13 +61,8 @@ module ActiveRecord # Setup database middleware after initializers have run initializer "active_record.initialize_database_middleware", :after => "action_controller.set_configs" do |app| middleware = app.config.middleware - if middleware.include?("ActiveRecord::SessionStore") - middleware.insert_before "ActiveRecord::SessionStore", ActiveRecord::ConnectionAdapters::ConnectionManagement - middleware.insert_before "ActiveRecord::SessionStore", ActiveRecord::QueryCache - else - middleware.use ActiveRecord::ConnectionAdapters::ConnectionManagement - middleware.use ActiveRecord::QueryCache - end + middleware.insert_after "::ActionDispatch::Callbacks", ActiveRecord::QueryCache + middleware.insert_after "::ActionDispatch::Callbacks", ActiveRecord::ConnectionAdapters::ConnectionManagement end initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app| -- cgit v1.2.3 From f4d174b211effd4cd510c1578bdc391852f4d375 Mon Sep 17 00:00:00 2001 From: Neeraj Singh Date: Tue, 20 Apr 2010 11:14:00 -0400 Subject: making rake:migrate VERSION=0 a noop called in succession. [#2137 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/lib/active_record/migration.rb | 10 +++++++--- activerecord/test/cases/migration_test.rb | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index c163fb982a..940f825038 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -384,9 +384,13 @@ module ActiveRecord class << self def migrate(migrations_path, target_version = nil) case - when target_version.nil? then up(migrations_path, target_version) - when current_version > target_version then down(migrations_path, target_version) - else up(migrations_path, target_version) + when target_version.nil? + up(migrations_path, target_version) + when current_version == 0 && target_version == 0 + when current_version > target_version + down(migrations_path, target_version) + else + up(migrations_path, target_version) end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 7a26ee072d..a3d1ceaa1f 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1136,6 +1136,25 @@ if ActiveRecord::Base.connection.supports_migrations? load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb") end + def test_target_version_zero_should_run_only_once + # migrate up to 1 + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 1) + + # migrate down to 0 + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0) + + # now unload the migrations that have been defined + PeopleHaveLastNames.unloadable + ActiveSupport::Dependencies.remove_unloadable_constants! + + # migrate down to 0 again + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", 0) + + assert !defined? PeopleHaveLastNames + ensure + load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb") + end + def test_migrator_db_has_no_schema_migrations_table # Oracle adapter raises error if semicolon is present as last character if current_adapter?(:OracleAdapter) -- cgit v1.2.3 From da840d13da865331297d5287391231b1ed39721b Mon Sep 17 00:00:00 2001 From: Brian Durand Date: Tue, 2 Jun 2009 14:42:22 -0500 Subject: Add after_commit and after_rollback callbacks to ActiveRecord that are called after transactions either commit or rollback on all records saved or destroyed in the transaction. [#2991 state:committed] Signed-off-by: Jeremy Kemper --- activerecord/CHANGELOG | 2 + .../abstract/database_statements.rb | 56 +++++ activerecord/lib/active_record/transactions.rb | 131 +++++++++-- .../test/cases/transaction_callbacks_test.rb | 244 +++++++++++++++++++++ activerecord/test/cases/transactions_test.rb | 61 ++++-- 5 files changed, 466 insertions(+), 28 deletions(-) create mode 100644 activerecord/test/cases/transaction_callbacks_test.rb (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 7864d735ad..ac5bd8e635 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.0.0 [beta 4/release candidate] (unreleased)* +* New callbacks: after_commit and after_rollback. Do expensive operations like image thumbnailing after_commit instead of after_save. #2991 [Brian Durand] + * Serialized attributes are not converted to YAML if they are any of the formats that can be serialized to XML (like Hash, Array and Strings). [José Valim] * Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised. #1966 [Curtis Hawthorne] diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 0c87e052c4..b9fb452eee 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -122,6 +122,8 @@ module ActiveRecord requires_new = options[:requires_new] || !last_transaction_joinable transaction_open = false + @_current_transaction_records ||= [] + begin if block_given? if requires_new || open_transactions == 0 @@ -132,6 +134,7 @@ module ActiveRecord end increment_open_transactions transaction_open = true + @_current_transaction_records.push([]) end yield end @@ -141,8 +144,10 @@ module ActiveRecord decrement_open_transactions if open_transactions == 0 rollback_db_transaction + rollback_transaction_records(true) else rollback_to_savepoint + rollback_transaction_records(false) end end raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback) @@ -157,20 +162,35 @@ module ActiveRecord begin if open_transactions == 0 commit_db_transaction + commit_transaction_records else release_savepoint + save_point_records = @_current_transaction_records.pop + unless save_point_records.blank? + @_current_transaction_records.push([]) if @_current_transaction_records.empty? + @_current_transaction_records.last.concat(save_point_records) + end end rescue Exception => database_transaction_rollback if open_transactions == 0 rollback_db_transaction + rollback_transaction_records(true) else rollback_to_savepoint + rollback_transaction_records(false) end raise end end end + # Register a record with the current transaction so that its after_commit and after_rollback callbacks + # can be called. + def add_transaction_record(record) + last_batch = @_current_transaction_records.last + last_batch << record if last_batch + end + # Begins the transaction (and turns off auto-committing). def begin_db_transaction() end @@ -268,6 +288,42 @@ module ActiveRecord limit.to_i end end + + # Send a rollback message to all records after they have been rolled back. If rollback + # is false, only rollback records since the last save point. + def rollback_transaction_records(rollback) #:nodoc + if rollback + records = @_current_transaction_records.flatten + @_current_transaction_records.clear + else + records = @_current_transaction_records.pop + end + + unless records.blank? + records.uniq.each do |record| + begin + record.rolledback!(rollback) + rescue Exception => e + record.logger.error(e) if record.respond_to?(:logger) + end + end + end + end + + # Send a commit message to all records after they have been committed. + def commit_transaction_records #:nodoc + records = @_current_transaction_records.flatten + @_current_transaction_records.clear + unless records.blank? + records.uniq.each do |record| + begin + record.committed! + rescue Exception => e + record.logger.error(e) if record.respond_to?(:logger) + end + end + end + end end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index cf0fe8934d..9e48228a40 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -12,6 +12,9 @@ module ActiveRecord [:destroy, :save, :save!].each do |method| alias_method_chain method, :transactions end + + define_model_callbacks :commit, :commit_on_update, :commit_on_create, :commit_on_destroy, :only => :after + define_model_callbacks :rollback, :rollback_on_update, :rollback_on_create, :rollback_on_destroy end # Transactions are protective blocks where SQL statements are only permanent @@ -108,7 +111,7 @@ module ActiveRecord # rescue ActiveRecord::StatementInvalid # # ...which we ignore. # end - # + # # # On PostgreSQL, the transaction is now unusable. The following # # statement will cause a PostgreSQL error, even though the unique # # constraint is no longer violated: @@ -132,7 +135,7 @@ module ActiveRecord # raise ActiveRecord::Rollback # end # end - # + # # User.find(:all) # => empty # # It is also possible to requires a sub-transaction by passing @@ -147,7 +150,7 @@ module ActiveRecord # raise ActiveRecord::Rollback # end # end - # + # # User.find(:all) # => Returns only Kotori # # Most databases don't support true nested transactions. At the time of @@ -157,6 +160,26 @@ module ActiveRecord # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html # for more information about savepoints. # + # === Callbacks + # + # There are two types of callbacks associated with committing and rolling back transactions: + # after_commit and after_rollback. + # + # The after_commit callbacks are called on every record saved or destroyed within a + # transaction immediately after the transaction is committed. The after_rollback callbacks + # are called on every record saved or destroyed within a transaction immediately after the + # transaction or savepoint is rolled back. + # + # Additionally, there are callbacks for after_commit_on_create, after_rollback_on_create, + # after_commit_on_update, after_rollback_on_update, after_commit_on_destroy, and + # after_rollback_on_destroy which are only called if a record is created, updated or destroyed + # in the transaction. + # + # These callbacks are useful for interacting with other systems since you will be guaranteed + # that the callback is only executed when the database is in a permanent state. For example, + # after_commit is a good spot to put in a hook to clearing a cache since clearing it from + # within a transaction could trigger the cache to be regenerated before the database is updated. + # # === Caveats # # If you're on MySQL, then do not use DDL operations in nested transactions @@ -166,7 +189,7 @@ module ActiveRecord # is finished and tries to release the savepoint it created earlier, a # database error will occur because the savepoint has already been # automatically released. The following example demonstrates the problem: - # + # # Model.connection.transaction do # BEGIN # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1 # Model.connection.create_table(...) # active_record_1 now automatically released @@ -197,24 +220,55 @@ module ActiveRecord end def save_with_transactions! #:nodoc: - rollback_active_record_state! { self.class.transaction { save_without_transactions! } } + with_transaction_returning_status(:save_without_transactions!) end # Reset id and @new_record if the transaction rolls back. def rollback_active_record_state! - id_present = has_attribute?(self.class.primary_key) - previous_id = id - previous_new_record = new_record? + remember_transaction_record_state yield rescue Exception - @new_record = previous_new_record - if id_present - self.id = previous_id + restore_transaction_record_state + raise + ensure + clear_transaction_record_state + end + + # Call the after_commit callbacks + def committed! #:nodoc: + if transaction_record_state(:new_record) + _run_commit_on_create_callbacks + elsif transaction_record_state(:destroyed) + _run_commit_on_destroy_callbacks else - @attributes.delete(self.class.primary_key) - @attributes_cache.delete(self.class.primary_key) + _run_commit_on_update_callbacks + end + _run_commit_callbacks + ensure + clear_transaction_record_state + end + + # Call the after rollback callbacks. The restore_state argument indicates if the record + # state should be rolled back to the beginning or just to the last savepoint. + def rolledback!(force_restore_state = false) #:nodoc: + if transaction_record_state(:new_record) + _run_rollback_on_create_callbacks + elsif transaction_record_state(:destroyed) + _run_rollback_on_destroy_callbacks + else + _run_rollback_on_update_callbacks + end + _run_rollback_callbacks + ensure + restore_transaction_record_state(force_restore_state) + end + + # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks + # can be called. + def add_to_transaction + if self.class.connection.add_transaction_record(self) + remember_transaction_record_state end - raise end # Executes +method+ within a transaction and captures its return value as a @@ -226,10 +280,59 @@ module ActiveRecord def with_transaction_returning_status(method, *args) status = nil self.class.transaction do + add_to_transaction status = send(method, *args) raise ActiveRecord::Rollback unless status end status end + + protected + + # Save the new record state and id of a record so it can be restored later if a transaction fails. + def remember_transaction_record_state #:nodoc + @_start_transaction_state ||= {} + unless @_start_transaction_state.include?(:new_record) + @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) + @_start_transaction_state[:new_record] = @new_record + end + unless @_start_transaction_state.include?(:destroyed) + @_start_transaction_state[:destroyed] = @new_record + end + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 + end + + # Clear the new record state and id of a record. + def clear_transaction_record_state #:nodoc + if defined?(@_start_transaction_state) + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1 + end + end + + # Restore the new record state and id of a record that was previously saved by a call to save_record_state. + def restore_transaction_record_state(force = false) #:nodoc + if defined?(@_start_transaction_state) + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + if @_start_transaction_state[:level] < 1 + restore_state = remove_instance_variable(:@_start_transaction_state) + if restore_state + @new_record = restore_state[:new_record] + @destroyed = restore_state[:destroyed] + if restore_state[:id] + self.id = restore_state[:id] + else + @attributes.delete(self.class.primary_key) + @attributes_cache.delete(self.class.primary_key) + end + end + end + end + end + + # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. + def transaction_record_state(state) #:nodoc + @_start_transaction_state[state] if defined?(@_start_transaction_state) + end end end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb new file mode 100644 index 0000000000..c2c5fd3b05 --- /dev/null +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -0,0 +1,244 @@ +require "cases/helper" +require 'models/topic' +require 'models/reply' + +class TransactionCallbacksTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + fixtures :topics + + class TopicWithCallbacks < ActiveRecord::Base + set_table_name :topics + + after_commit{|record| record.send(:do_after_commit, nil)} + after_commit_on_create{|record| record.send(:do_after_commit, :create)} + after_commit_on_update{|record| record.send(:do_after_commit, :update)} + after_commit_on_destroy{|record| record.send(:do_after_commit, :destroy)} + after_rollback{|record| record.send(:do_after_rollback, nil)} + after_rollback_on_create{|record| record.send(:do_after_rollback, :create)} + after_rollback_on_update{|record| record.send(:do_after_rollback, :update)} + after_rollback_on_destroy{|record| record.send(:do_after_rollback, :destroy)} + + def history + @history ||= [] + end + + def after_commit_block(on = nil, &block) + @after_commit ||= {} + @after_commit[on] ||= [] + @after_commit[on] << block + end + + def after_rollback_block(on = nil, &block) + @after_rollback ||= {} + @after_rollback[on] ||= [] + @after_rollback[on] << block + end + + def do_after_commit(on) + blocks = @after_commit[on] if defined?(@after_commit) + blocks.each{|b| b.call(self)} if blocks + end + + def do_after_rollback(on) + blocks = @after_rollback[on] if defined?(@after_rollback) + blocks.each{|b| b.call(self)} if blocks + end + end + + def setup + @first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id } + end + + def test_call_after_commit_after_transaction_commits + @first.after_commit_block{|r| r.history << :after_commit} + @first.after_rollback_block{|r| r.history << :after_rollback} + + @first.save! + assert @first.history, [:after_commit] + end + + def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + @first.save! + assert @first.history, [:commit_on_update] + end + + def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + @first.destroy + assert @first.history, [:commit_on_destroy] + end + + def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record + @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) + @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} + @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @new_record.after_commit_block(:create){|r| r.history << :rollback_on_create} + @new_record.after_commit_block(:update){|r| r.history << :rollback_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + @new_record.save! + assert @new_record.history, [:commit_on_create] + end + + def test_call_after_rollback_after_transaction_rollsback + @first.after_commit_block{|r| r.history << :after_commit} + @first.after_rollback_block{|r| r.history << :after_rollback} + + Topic.transaction do + @first.save! + raise ActiveRecord::Rollback + end + + assert @first.history, [:after_rollback] + end + + def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + Topic.transaction do + @first.save! + raise ActiveRecord::Rollback + end + + assert @first.history, [:rollback_on_update] + end + + def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_update} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + Topic.transaction do + @first.destroy + raise ActiveRecord::Rollback + end + + assert @first.history, [:rollback_on_destroy] + end + + def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record + @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) + @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} + @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @new_record.after_commit_block(:create){|r| r.history << :rollback_on_create} + @new_record.after_commit_block(:update){|r| r.history << :rollback_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + Topic.transaction do + @new_record.save! + raise ActiveRecord::Rollback + end + + assert @new_record.history, [:rollback_on_create] + end + + def test_call_after_rollback_when_commit_fails + @first.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction) + begin + @first.connection.class.class_eval do + def commit_db_transaction; raise "boom!"; end + end + + @first.after_commit_block{|r| r.history << :after_commit} + @first.after_rollback_block{|r| r.history << :after_rollback} + + assert !@first.save rescue nil + assert @first.history == [:after_rollback] + ensure + @first.connection.class.send(:remove_method, :commit_db_transaction) + @first.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction) + end + end + + def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint + def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end + def @first.commits(i=0); @commits ||= 0; @commits += i if i; end + @first.after_rollback_block{|r| r.rollbacks(1)} + @first.after_commit_block{|r| r.commits(1)} + + def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end + def @second.commits(i=0); @commits ||= 0; @commits += i if i; end + @second.after_rollback_block{|r| r.rollbacks(1)} + @second.after_commit_block{|r| r.commits(1)} + + Topic.transaction do + @first.save! + Topic.transaction(:requires_new => true) do + @second.save! + raise ActiveRecord::Rollback + end + end + + assert 1, @first.commits + assert 0, @first.rollbacks + assert 1, @second.commits + assert 1, @second.rollbacks + end + + def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails + def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end + def @first.commits(i=0); @commits ||= 0; @commits += i if i; end + + @second.after_rollback_block{|r| r.rollbacks(1)} + @second.after_commit_block{|r| r.commits(1)} + + Topic.transaction do + @first.save + Topic.transaction(:requires_new => true) do + @first.save! + raise ActiveRecord::Rollback + end + Topic.transaction(:requires_new => true) do + @first.save! + raise ActiveRecord::Rollback + end + end + + assert 1, @first.commits + assert 2, @first.rollbacks + end + + def test_after_transaction_callbacks_should_not_raise_errors + def @first.last_after_transaction_error=(e); @last_transaction_error = e; end + def @first.last_after_transaction_error; @last_transaction_error; end + @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";} + @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";} + + @first.save! + assert_equal @first.last_after_transaction_error, :commit + + Topic.transaction do + @first.save! + raise ActiveRecord::Rollback + end + + assert_equal @first.last_after_transaction_error, :rollback + end +end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index c550030329..958a4e4f94 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -262,22 +262,22 @@ class TransactionTest < ActiveRecord::TestCase assert !@first.reload.approved? assert !@second.reload.approved? end if Topic.connection.supports_savepoints? - + def test_many_savepoints Topic.transaction do @first.content = "One" @first.save! - + begin Topic.transaction :requires_new => true do @first.content = "Two" @first.save! - + begin Topic.transaction :requires_new => true do @first.content = "Three" @first.save! - + begin Topic.transaction :requires_new => true do @first.content = "Four" @@ -286,22 +286,22 @@ class TransactionTest < ActiveRecord::TestCase end rescue end - + @three = @first.reload.content raise end rescue end - + @two = @first.reload.content raise end rescue end - + @one = @first.reload.content end - + assert_equal "One", @one assert_equal "Two", @two assert_equal "Three", @three @@ -319,7 +319,34 @@ class TransactionTest < ActiveRecord::TestCase end end end - + + def test_restore_active_record_state_for_all_records_in_a_transaction + topic_1 = Topic.new(:title => 'test_1') + topic_2 = Topic.new(:title => 'test_2') + Topic.transaction do + assert topic_1.save + assert topic_2.save + @first.save + @second.destroy + assert_equal false, topic_1.new_record? + assert_not_nil topic_1.id + assert_equal false, topic_2.new_record? + assert_not_nil topic_2.id + assert_equal false, @first.new_record? + assert_not_nil @first.id + assert_equal true, @second.destroyed? + raise ActiveRecord::Rollback + end + + assert_equal true, topic_1.new_record? + assert_nil topic_1.id + assert_equal true, topic_2.new_record? + assert_nil topic_2.id + assert_equal false, @first.new_record? + assert_not_nil @first.id + assert_equal false, @second.destroyed? + end + if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE) def test_outside_transaction_works assert Topic.connection.outside_transaction? @@ -328,7 +355,7 @@ class TransactionTest < ActiveRecord::TestCase Topic.connection.rollback_db_transaction assert Topic.connection.outside_transaction? end - + def test_rollback_wont_be_executed_if_no_transaction_active assert_raise RuntimeError do Topic.transaction do @@ -338,7 +365,7 @@ class TransactionTest < ActiveRecord::TestCase end end end - + def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active Topic.transaction do Topic.transaction do @@ -358,12 +385,12 @@ class TransactionTest < ActiveRecord::TestCase # # We go back to the connection for the column queries because # Topic.columns is cached and won't report changes to the DB - + assert_nothing_raised do Topic.reset_column_information Topic.connection.add_column('topics', 'stuff', :string) assert Topic.column_names.include?('stuff') - + Topic.reset_column_information Topic.connection.remove_column('topics', 'stuff') assert !Topic.column_names.include?('stuff') @@ -382,6 +409,12 @@ class TransactionTest < ActiveRecord::TestCase end private + def define_callback_method(callback_method) + define_method(callback_method) do + self.history << [callback_method, :method] + end + end + def add_exception_raising_after_save_callback_to_topic Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 remove_method(:after_save_for_transaction) @@ -440,7 +473,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase def test_automatic_savepoint_in_outer_transaction @first = Topic.find(1) - + begin Topic.transaction do @first.approved = true -- cgit v1.2.3 From d2a49e4b1f30c5997e169110eed94a55aee53f56 Mon Sep 17 00:00:00 2001 From: Brian Durand Date: Thu, 29 Apr 2010 15:13:09 -0500 Subject: Update after_commit and after_rollback docs and tests to use new style API with an :on options instead of on_* suffix. [#2991] Signed-off-by: Jeremy Kemper --- activerecord/lib/active_record/transactions.rb | 5 ----- activerecord/test/cases/transaction_callbacks_test.rb | 12 ++++++------ 2 files changed, 6 insertions(+), 11 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 9e48228a40..0a55ef2b53 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -170,11 +170,6 @@ module ActiveRecord # are called on every record saved or destroyed within a transaction immediately after the # transaction or savepoint is rolled back. # - # Additionally, there are callbacks for after_commit_on_create, after_rollback_on_create, - # after_commit_on_update, after_rollback_on_update, after_commit_on_destroy, and - # after_rollback_on_destroy which are only called if a record is created, updated or destroyed - # in the transaction. - # # These callbacks are useful for interacting with other systems since you will be guaranteed # that the callback is only executed when the database is in a permanent state. For example, # after_commit is a good spot to put in a hook to clearing a cache since clearing it from diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index c2c5fd3b05..a07da093f1 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -10,13 +10,13 @@ class TransactionCallbacksTest < ActiveRecord::TestCase set_table_name :topics after_commit{|record| record.send(:do_after_commit, nil)} - after_commit_on_create{|record| record.send(:do_after_commit, :create)} - after_commit_on_update{|record| record.send(:do_after_commit, :update)} - after_commit_on_destroy{|record| record.send(:do_after_commit, :destroy)} + after_commit(:on => :create){|record| record.send(:do_after_commit, :create)} + after_commit(:on => :update){|record| record.send(:do_after_commit, :update)} + after_commit(:on => :destroy){|record| record.send(:do_after_commit, :destroy)} after_rollback{|record| record.send(:do_after_rollback, nil)} - after_rollback_on_create{|record| record.send(:do_after_rollback, :create)} - after_rollback_on_update{|record| record.send(:do_after_rollback, :update)} - after_rollback_on_destroy{|record| record.send(:do_after_rollback, :destroy)} + after_rollback(:on => :create){|record| record.send(:do_after_rollback, :create)} + after_rollback(:on => :update){|record| record.send(:do_after_rollback, :update)} + after_rollback(:on => :destroy){|record| record.send(:do_after_rollback, :destroy)} def history @history ||= [] -- cgit v1.2.3 From cde168edbbe2d95dd8af40524dd9f42220481ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 30 Apr 2010 12:42:12 +0200 Subject: Update generators to use thor 0.13.6 with simpler source_root handling. --- activerecord/lib/rails/generators/active_record.rb | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb index d2b1e86857..5d8a8e81bc 100644 --- a/activerecord/lib/rails/generators/active_record.rb +++ b/activerecord/lib/rails/generators/active_record.rb @@ -8,16 +8,12 @@ module ActiveRecord class Base < Rails::Generators::NamedBase #:nodoc: include Rails::Generators::Migration - def self.source_root - @_ar_source_root ||= begin - if base_name && generator_name - File.expand_path(File.join(base_name, generator_name, 'templates'), File.dirname(__FILE__)) - end - end + # Set the current directory as base for the inherited generators. + def self.base_root + File.dirname(__FILE__) end # Implement the required interface for Rails::Generators::Migration. - # def self.next_migration_number(dirname) #:nodoc: next_migration_number = current_migration_number(dirname) + 1 if ActiveRecord::Base.timestamped_migrations -- cgit v1.2.3 From 883f27aa9a6fae5194400bb8f00e343dcb610270 Mon Sep 17 00:00:00 2001 From: Neeraj Singh Date: Thu, 29 Apr 2010 17:39:05 -0400 Subject: test cases for record.to_xml [#458 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/test/cases/xml_serialization_test.rb | 8 ++++++++ activerecord/test/models/author.rb | 4 ++++ 2 files changed, 12 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index bd29ae2612..751946ffc5 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -234,4 +234,12 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert types.include?('StiPost') end + def test_should_produce_xml_for_methods_returning_array + xml = authors(:david).to_xml(:methods => :social) + array = Hash.from_xml(xml)['author']['social'] + assert_equal 2, array.size + assert array.include? 'twitter' + assert array.include? 'github' + end + end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 025f6207f8..655b45bf57 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -104,6 +104,10 @@ class Author < ActiveRecord::Base "#{id}-#{name}" end + def social + %w(twitter github) + end + private def log_before_adding(object) @post_log << "before_adding#{object.id || ''}" -- cgit v1.2.3 From 60504e62c8e2f5e137a0ac82aed67a6c0fe42447 Mon Sep 17 00:00:00 2001 From: Lawrence Pit Date: Fri, 30 Apr 2010 10:39:52 +1000 Subject: to_xml with :include should skip_instruct on the included records [#4506 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/lib/active_record/serializers/xml_serializer.rb | 2 +- activerecord/test/cases/serialization_test.rb | 7 +++++++ activerecord/test/models/contact.rb | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index ed5964d923..b2d4a48945 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -197,7 +197,7 @@ module ActiveRecord #:nodoc: # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. def add_associations(association, records, opts) association_name = association.to_s.singularize - merged_options = options.merge(opts).merge!(:root => association_name) + merged_options = options.merge(opts).merge!(:root => association_name, :skip_instruct => true) if records.is_a?(Enumerable) tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 8841694271..8c385af97c 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -44,4 +44,11 @@ class SerializationTest < ActiveRecord::TestCase assert_equal @contact_attributes[:awesome], contact.awesome, "For #{format}" end end + + def test_serialize_should_xml_skip_instruct_for_included_records + @contact.alternative = Contact.new(:name => 'Copa Cabana') + @serialized = @contact.to_xml(:include => [ :alternative ]) + assert_equal @serialized.index(' 'Contact' +end -- cgit v1.2.3 From 1ff954aaea764c16e812dadea44a07a3f4119905 Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Fri, 30 Apr 2010 21:30:28 +0200 Subject: after_(commit|rollback) rdoc, edit pass --- activerecord/lib/active_record/transactions.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 0a55ef2b53..796dd99f02 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -163,16 +163,16 @@ module ActiveRecord # === Callbacks # # There are two types of callbacks associated with committing and rolling back transactions: - # after_commit and after_rollback. + # +after_commit+ and +after_rollback+. # - # The after_commit callbacks are called on every record saved or destroyed within a - # transaction immediately after the transaction is committed. The after_rollback callbacks + # +after_commit+ callbacks are called on every record saved or destroyed within a + # transaction immediately after the transaction is committed. +after_rollback+ callbacks # are called on every record saved or destroyed within a transaction immediately after the # transaction or savepoint is rolled back. # # These callbacks are useful for interacting with other systems since you will be guaranteed # that the callback is only executed when the database is in a permanent state. For example, - # after_commit is a good spot to put in a hook to clearing a cache since clearing it from + # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from # within a transaction could trigger the cache to be regenerated before the database is updated. # # === Caveats @@ -180,7 +180,7 @@ module ActiveRecord # If you're on MySQL, then do not use DDL operations in nested transactions # blocks that are emulated with savepoints. That is, do not execute statements # like 'CREATE TABLE' inside such blocks. This is because MySQL automatically - # releases all savepoints upon executing a DDL operation. When #transaction + # releases all savepoints upon executing a DDL operation. When +transaction+ # is finished and tries to release the savepoint it created earlier, a # database error will occur because the savepoint has already been # automatically released. The following example demonstrates the problem: -- cgit v1.2.3