activerecord 3.1.11 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. data/CHANGELOG.md +6294 -97
  2. data/README.rdoc +2 -2
  3. data/examples/performance.rb +55 -31
  4. data/lib/active_record/aggregations.rb +2 -2
  5. data/lib/active_record/associations/association.rb +2 -42
  6. data/lib/active_record/associations/association_scope.rb +3 -30
  7. data/lib/active_record/associations/builder/association.rb +6 -4
  8. data/lib/active_record/associations/builder/belongs_to.rb +3 -3
  9. data/lib/active_record/associations/builder/collection_association.rb +2 -2
  10. data/lib/active_record/associations/builder/has_many.rb +4 -4
  11. data/lib/active_record/associations/builder/has_one.rb +5 -6
  12. data/lib/active_record/associations/builder/singular_association.rb +3 -16
  13. data/lib/active_record/associations/collection_association.rb +55 -28
  14. data/lib/active_record/associations/collection_proxy.rb +1 -35
  15. data/lib/active_record/associations/has_many_association.rb +5 -1
  16. data/lib/active_record/associations/has_many_through_association.rb +11 -8
  17. data/lib/active_record/associations/join_dependency.rb +1 -1
  18. data/lib/active_record/associations/preloader/association.rb +3 -1
  19. data/lib/active_record/associations.rb +82 -69
  20. data/lib/active_record/attribute_assignment.rb +221 -0
  21. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  22. data/lib/active_record/attribute_methods/dirty.rb +3 -3
  23. data/lib/active_record/attribute_methods/primary_key.rb +62 -25
  24. data/lib/active_record/attribute_methods/read.rb +72 -83
  25. data/lib/active_record/attribute_methods/serialization.rb +93 -0
  26. data/lib/active_record/attribute_methods/time_zone_conversion.rb +9 -14
  27. data/lib/active_record/attribute_methods/write.rb +27 -5
  28. data/lib/active_record/attribute_methods.rb +209 -30
  29. data/lib/active_record/autosave_association.rb +23 -8
  30. data/lib/active_record/base.rb +217 -1709
  31. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +98 -132
  32. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +82 -29
  33. data/lib/active_record/connection_adapters/abstract/database_statements.rb +13 -42
  34. data/lib/active_record/connection_adapters/abstract/query_cache.rb +1 -1
  35. data/lib/active_record/connection_adapters/abstract/quoting.rb +9 -12
  36. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +36 -25
  37. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +43 -22
  38. data/lib/active_record/connection_adapters/abstract_adapter.rb +78 -43
  39. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
  40. data/lib/active_record/connection_adapters/column.rb +2 -2
  41. data/lib/active_record/connection_adapters/mysql2_adapter.rb +138 -578
  42. data/lib/active_record/connection_adapters/mysql_adapter.rb +86 -658
  43. data/lib/active_record/connection_adapters/postgresql_adapter.rb +144 -94
  44. data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
  45. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -6
  46. data/lib/active_record/connection_adapters/sqlite_adapter.rb +43 -22
  47. data/lib/active_record/counter_cache.rb +4 -3
  48. data/lib/active_record/dynamic_matchers.rb +79 -0
  49. data/lib/active_record/errors.rb +11 -1
  50. data/lib/active_record/explain.rb +83 -0
  51. data/lib/active_record/explain_subscriber.rb +21 -0
  52. data/lib/active_record/fixtures/file.rb +65 -0
  53. data/lib/active_record/fixtures.rb +31 -76
  54. data/lib/active_record/identity_map.rb +4 -11
  55. data/lib/active_record/inheritance.rb +167 -0
  56. data/lib/active_record/integration.rb +49 -0
  57. data/lib/active_record/locking/optimistic.rb +30 -25
  58. data/lib/active_record/locking/pessimistic.rb +23 -1
  59. data/lib/active_record/log_subscriber.rb +3 -3
  60. data/lib/active_record/migration/command_recorder.rb +8 -8
  61. data/lib/active_record/migration.rb +47 -30
  62. data/lib/active_record/model_schema.rb +366 -0
  63. data/lib/active_record/nested_attributes.rb +3 -2
  64. data/lib/active_record/persistence.rb +51 -9
  65. data/lib/active_record/querying.rb +58 -0
  66. data/lib/active_record/railtie.rb +24 -28
  67. data/lib/active_record/railties/controller_runtime.rb +3 -1
  68. data/lib/active_record/railties/databases.rake +134 -77
  69. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  70. data/lib/active_record/readonly_attributes.rb +26 -0
  71. data/lib/active_record/reflection.rb +7 -15
  72. data/lib/active_record/relation/batches.rb +5 -2
  73. data/lib/active_record/relation/calculations.rb +27 -6
  74. data/lib/active_record/relation/delegation.rb +49 -0
  75. data/lib/active_record/relation/finder_methods.rb +6 -5
  76. data/lib/active_record/relation/predicate_builder.rb +12 -19
  77. data/lib/active_record/relation/query_methods.rb +76 -10
  78. data/lib/active_record/relation/spawn_methods.rb +11 -2
  79. data/lib/active_record/relation.rb +77 -34
  80. data/lib/active_record/result.rb +1 -1
  81. data/lib/active_record/sanitization.rb +194 -0
  82. data/lib/active_record/schema_dumper.rb +5 -2
  83. data/lib/active_record/scoping/default.rb +142 -0
  84. data/lib/active_record/scoping/named.rb +202 -0
  85. data/lib/active_record/scoping.rb +152 -0
  86. data/lib/active_record/serialization.rb +1 -43
  87. data/lib/active_record/serializers/xml_serializer.rb +2 -44
  88. data/lib/active_record/session_store.rb +15 -15
  89. data/lib/active_record/store.rb +50 -0
  90. data/lib/active_record/test_case.rb +11 -7
  91. data/lib/active_record/timestamp.rb +16 -3
  92. data/lib/active_record/transactions.rb +5 -5
  93. data/lib/active_record/translation.rb +22 -0
  94. data/lib/active_record/validations/associated.rb +5 -4
  95. data/lib/active_record/validations/uniqueness.rb +4 -4
  96. data/lib/active_record/validations.rb +1 -1
  97. data/lib/active_record/version.rb +2 -2
  98. data/lib/active_record.rb +28 -2
  99. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -1
  100. data/lib/rails/generators/active_record/migration/templates/migration.rb +9 -3
  101. data/lib/rails/generators/active_record/model/model_generator.rb +5 -1
  102. data/lib/rails/generators/active_record/model/templates/migration.rb +3 -5
  103. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
  104. metadata +50 -40
  105. checksums.yaml +0 -7
  106. data/lib/active_record/named_scope.rb +0 -200
@@ -0,0 +1,79 @@
1
+ module ActiveRecord
2
+ module DynamicMatchers
3
+ def respond_to?(method_id, include_private = false)
4
+ if match = DynamicFinderMatch.match(method_id)
5
+ return true if all_attributes_exists?(match.attribute_names)
6
+ elsif match = DynamicScopeMatch.match(method_id)
7
+ return true if all_attributes_exists?(match.attribute_names)
8
+ end
9
+
10
+ super
11
+ end
12
+
13
+ private
14
+
15
+ # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
16
+ # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders
17
+ # section at the top of this file for more detailed information.
18
+ #
19
+ # It's even possible to use all the additional parameters to +find+. For example, the
20
+ # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
21
+ #
22
+ # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
23
+ # is first invoked, so that future attempts to use it do not run through method_missing.
24
+ def method_missing(method_id, *arguments, &block)
25
+ if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id))
26
+ attribute_names = match.attribute_names
27
+ super unless all_attributes_exists?(attribute_names)
28
+ if arguments.size < attribute_names.size
29
+ method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'"
30
+ backtrace = [method_trace] + caller
31
+ raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace
32
+ end
33
+ if match.respond_to?(:scope?) && match.scope?
34
+ self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
35
+ def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
36
+ attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
37
+ #
38
+ scoped(:conditions => attributes) # scoped(:conditions => attributes)
39
+ end # end
40
+ METHOD
41
+ send(method_id, *arguments)
42
+ elsif match.finder?
43
+ options = arguments.extract_options!
44
+ relation = options.any? ? scoped(options) : scoped
45
+ relation.send :find_by_attributes, match, attribute_names, *arguments, &block
46
+ elsif match.instantiator?
47
+ scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
48
+ end
49
+ else
50
+ super
51
+ end
52
+ end
53
+
54
+ # Similar in purpose to +expand_hash_conditions_for_aggregates+.
55
+ def expand_attribute_names_for_aggregates(attribute_names)
56
+ attribute_names.map { |attribute_name|
57
+ unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
58
+ aggregate_mapping(aggregation).map do |field_attr, _|
59
+ field_attr.to_sym
60
+ end
61
+ else
62
+ attribute_name.to_sym
63
+ end
64
+ }.flatten
65
+ end
66
+
67
+ def all_attributes_exists?(attribute_names)
68
+ (expand_attribute_names_for_aggregates(attribute_names) -
69
+ column_methods_hash.keys).empty?
70
+ end
71
+
72
+ def aggregate_mapping(reflection)
73
+ mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
74
+ mapping.first.is_a?(Array) ? mapping : [mapping]
75
+ end
76
+
77
+
78
+ end
79
+ end
@@ -87,7 +87,7 @@ module ActiveRecord
87
87
  #
88
88
  # For example, in
89
89
  #
90
- # Location.find :all, :conditions => ["lat = ? AND lng = ?", 53.7362]
90
+ # Location.where("lat = ? AND lng = ?", 53.7362)
91
91
  #
92
92
  # two placeholders are given but only one variable to fill them.
93
93
  class PreparedStatementInvalid < ActiveRecordError
@@ -99,6 +99,16 @@ module ActiveRecord
99
99
  #
100
100
  # Read more about optimistic locking in ActiveRecord::Locking module RDoc.
101
101
  class StaleObjectError < ActiveRecordError
102
+ attr_reader :record, :attempted_action
103
+
104
+ def initialize(record, attempted_action)
105
+ @record = record
106
+ @attempted_action = attempted_action
107
+ end
108
+
109
+ def message
110
+ "Attempted to #{attempted_action} a stale object: #{record.class.name}"
111
+ end
102
112
  end
103
113
 
104
114
  # Raised when association is being configured improperly or
@@ -0,0 +1,83 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+
3
+ module ActiveRecord
4
+ module Explain
5
+ def self.extended(base)
6
+ base.class_eval do
7
+ # If a query takes longer than these many seconds we log its query plan
8
+ # automatically. nil disables this feature.
9
+ class_attribute :auto_explain_threshold_in_seconds, :instance_writer => false
10
+ self.auto_explain_threshold_in_seconds = nil
11
+ end
12
+ end
13
+
14
+ # If auto explain is enabled, this method triggers EXPLAIN logging for the
15
+ # queries triggered by the block if it takes more than the threshold as a
16
+ # whole. That is, the threshold is not checked against each individual
17
+ # query, but against the duration of the entire block. This approach is
18
+ # convenient for relations.
19
+ #
20
+ # The available_queries_for_explain thread variable collects the queries
21
+ # to be explained. If the value is nil, it means queries are not being
22
+ # currently collected. A false value indicates collecting is turned
23
+ # off. Otherwise it is an array of queries.
24
+ def logging_query_plan # :nodoc:
25
+ threshold = auto_explain_threshold_in_seconds
26
+ current = Thread.current
27
+ if threshold && current[:available_queries_for_explain].nil?
28
+ begin
29
+ queries = current[:available_queries_for_explain] = []
30
+ start = Time.now
31
+ result = yield
32
+ logger.warn(exec_explain(queries)) if Time.now - start > threshold
33
+ result
34
+ ensure
35
+ current[:available_queries_for_explain] = nil
36
+ end
37
+ else
38
+ yield
39
+ end
40
+ end
41
+
42
+ # Relation#explain needs to be able to collect the queries regardless of
43
+ # whether auto explain is enabled. This method serves that purpose.
44
+ def collecting_queries_for_explain # :nodoc:
45
+ current = Thread.current
46
+ original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
47
+ return yield, current[:available_queries_for_explain]
48
+ ensure
49
+ # Note that the return value above does not depend on this assigment.
50
+ current[:available_queries_for_explain] = original
51
+ end
52
+
53
+ # Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
54
+ # Returns a formatted string ready to be logged.
55
+ def exec_explain(queries) # :nodoc:
56
+ queries && queries.map do |sql, bind|
57
+ [].tap do |msg|
58
+ msg << "EXPLAIN for: #{sql}"
59
+ unless bind.empty?
60
+ bind_msg = bind.map {|col, val| [col.name, val]}.inspect
61
+ msg.last << " #{bind_msg}"
62
+ end
63
+ msg << connection.explain(sql, bind)
64
+ end.join("\n")
65
+ end.join("\n")
66
+ end
67
+
68
+ # Silences automatic EXPLAIN logging for the duration of the block.
69
+ #
70
+ # This has high priority, no EXPLAINs will be run even if downwards
71
+ # the threshold is set to 0.
72
+ #
73
+ # As the name of the method suggests this only applies to automatic
74
+ # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run.
75
+ def silence_auto_explain
76
+ current = Thread.current
77
+ original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false
78
+ yield
79
+ ensure
80
+ current[:available_queries_for_explain] = original
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,21 @@
1
+ require 'active_support/notifications'
2
+
3
+ module ActiveRecord
4
+ class ExplainSubscriber # :nodoc:
5
+ def call(*args)
6
+ if queries = Thread.current[:available_queries_for_explain]
7
+ payload = args.last
8
+ queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload)
9
+ end
10
+ end
11
+
12
+ # SCHEMA queries cannot be EXPLAINed, also we do not want to run EXPLAIN on
13
+ # our own EXPLAINs now matter how loopingly beautiful that would be.
14
+ IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN)
15
+ def ignore_payload?(payload)
16
+ payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name])
17
+ end
18
+
19
+ ActiveSupport::Notifications.subscribe("sql.active_record", new)
20
+ end
21
+ end
@@ -0,0 +1,65 @@
1
+ begin
2
+ require 'psych'
3
+ rescue LoadError
4
+ end
5
+
6
+ require 'erb'
7
+ require 'yaml'
8
+
9
+ module ActiveRecord
10
+ class Fixtures
11
+ class File
12
+ include Enumerable
13
+
14
+ ##
15
+ # Open a fixture file named +file+. When called with a block, the block
16
+ # is called with the filehandle and the filehandle is automatically closed
17
+ # when the block finishes.
18
+ def self.open(file)
19
+ x = new file
20
+ block_given? ? yield(x) : x
21
+ end
22
+
23
+ def initialize(file)
24
+ @file = file
25
+ @rows = nil
26
+ end
27
+
28
+ def each(&block)
29
+ rows.each(&block)
30
+ end
31
+
32
+ RESCUE_ERRORS = [ ArgumentError ] # :nodoc:
33
+
34
+ private
35
+ if defined?(Psych) && defined?(Psych::SyntaxError)
36
+ RESCUE_ERRORS << Psych::SyntaxError
37
+ end
38
+
39
+ def rows
40
+ return @rows if @rows
41
+
42
+ begin
43
+ data = YAML.load(render(IO.read(@file)))
44
+ rescue *RESCUE_ERRORS => error
45
+ raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
46
+ end
47
+ @rows = data ? validate(data).to_a : []
48
+ end
49
+
50
+ def render(content)
51
+ ERB.new(content).result
52
+ end
53
+
54
+ # Validate our unmarshalled data.
55
+ def validate(data)
56
+ unless Hash === data || YAML::Omap === data
57
+ raise Fixture::FormatError, 'fixture is not a hash'
58
+ end
59
+
60
+ raise Fixture::FormatError unless data.all? { |name, row| Hash === row }
61
+ data
62
+ end
63
+ end
64
+ end
65
+ end
@@ -6,14 +6,13 @@ rescue LoadError
6
6
  end
7
7
 
8
8
  require 'yaml'
9
- require 'csv'
10
9
  require 'zlib'
11
10
  require 'active_support/dependencies'
12
11
  require 'active_support/core_ext/array/wrap'
13
12
  require 'active_support/core_ext/object/blank'
14
13
  require 'active_support/core_ext/logger'
15
14
  require 'active_support/ordered_hash'
16
- require 'active_support/core_ext/module/deprecation'
15
+ require 'active_record/fixtures/file'
17
16
 
18
17
  if defined? ActiveRecord
19
18
  class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
@@ -23,8 +22,6 @@ else
23
22
  end
24
23
  end
25
24
 
26
- class FixturesFileNotFound < StandardError; end
27
-
28
25
  module ActiveRecord
29
26
  # \Fixtures are a way of organizing data that you want to test against; in short, sample data.
30
27
  #
@@ -467,7 +464,7 @@ module ActiveRecord
467
464
  connection,
468
465
  table_name,
469
466
  class_names[table_name.to_sym] || table_name.classify,
470
- File.join(fixtures_directory, path))
467
+ ::File.join(fixtures_directory, path))
471
468
  end
472
469
 
473
470
  all_loaded_fixtures.update(fixtures_map)
@@ -645,80 +642,25 @@ module ActiveRecord
645
642
  end
646
643
 
647
644
  def read_fixture_files
648
- if File.file?(yaml_file_path)
649
- read_yaml_fixture_files
650
- elsif File.file?(csv_file_path)
651
- read_csv_fixture_files
652
- else
653
- raise FixturesFileNotFound, "Could not find #{yaml_file_path} or #{csv_file_path}"
654
- end
655
- end
656
-
657
- def read_yaml_fixture_files
658
- yaml_string = (Dir["#{@fixture_path}/**/*.yml"].select { |f|
659
- File.file?(f)
660
- } + [yaml_file_path]).map { |file_path| IO.read(file_path) }.join
661
-
662
- if yaml = parse_yaml_string(yaml_string)
663
- # If the file is an ordered map, extract its children.
664
- yaml_value =
665
- if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
666
- yaml.value
667
- else
668
- [yaml]
669
- end
670
-
671
- yaml_value.each do |fixture|
672
- raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{fixture}" unless fixture.respond_to?(:each)
673
- fixture.each do |name, data|
674
- unless data
675
- raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
676
- end
677
-
678
- fixtures[name] = ActiveRecord::Fixture.new(data, model_class)
645
+ yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f|
646
+ ::File.file?(f)
647
+ } + [yaml_file_path]
648
+
649
+ yaml_files.each do |file|
650
+ Fixtures::File.open(file) do |fh|
651
+ fh.each do |name, row|
652
+ fixtures[name] = ActiveRecord::Fixture.new(row, model_class)
679
653
  end
680
654
  end
681
655
  end
682
656
  end
683
657
 
684
- def read_csv_fixture_files
685
- reader = CSV.parse(erb_render(IO.read(csv_file_path)))
686
- header = reader.shift
687
- i = 0
688
- reader.each do |row|
689
- data = {}
690
- row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
691
- fixtures["#{@class_name.to_s.underscore}_#{i+=1}"] = ActiveRecord::Fixture.new(data, model_class)
692
- end
693
- end
694
- deprecate :read_csv_fixture_files
695
-
696
658
  def yaml_file_path
697
659
  "#{@fixture_path}.yml"
698
660
  end
699
661
 
700
- def csv_file_path
701
- @fixture_path + ".csv"
702
- end
703
-
704
662
  def yaml_fixtures_key(path)
705
- File.basename(@fixture_path).split(".").first
706
- end
707
-
708
- RESCUE_ERRORS = [ ArgumentError ]
709
-
710
- if defined?(Psych) && defined?(Psych::SyntaxError)
711
- RESCUE_ERRORS << Psych::SyntaxError
712
- end
713
-
714
- def parse_yaml_string(fixture_content)
715
- YAML::load(erb_render(fixture_content))
716
- rescue *RESCUE_ERRORS => error
717
- raise Fixture::FormatError, "a YAML error occurred parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
718
- end
719
-
720
- def erb_render(fixture_content)
721
- ERB.new(fixture_content).result
663
+ ::File.basename(@fixture_path).split(".").first
722
664
  end
723
665
  end
724
666
 
@@ -794,7 +736,7 @@ module ActiveRecord
794
736
 
795
737
  def fixtures(*fixture_names)
796
738
  if fixture_names.first == :all
797
- fixture_names = Dir["#{fixture_path}/**/*.{yml,csv}"]
739
+ fixture_names = Dir["#{fixture_path}/**/*.{yml}"]
798
740
  fixture_names.map! { |f| f[(fixture_path.size + 1)..-5] }
799
741
  else
800
742
  fixture_names = fixture_names.flatten.map { |n| n.to_s }
@@ -880,6 +822,7 @@ module ActiveRecord
880
822
  end
881
823
 
882
824
  @fixture_cache = {}
825
+ @fixture_connections = []
883
826
  @@already_loaded_fixtures ||= {}
884
827
 
885
828
  # Load fixtures once and begin transaction.
@@ -890,9 +833,12 @@ module ActiveRecord
890
833
  @loaded_fixtures = load_fixtures
891
834
  @@already_loaded_fixtures[self.class] = @loaded_fixtures
892
835
  end
893
- ActiveRecord::Base.connection.increment_open_transactions
894
- ActiveRecord::Base.connection.transaction_joinable = false
895
- ActiveRecord::Base.connection.begin_db_transaction
836
+ @fixture_connections = enlist_fixture_connections
837
+ @fixture_connections.each do |connection|
838
+ connection.increment_open_transactions
839
+ connection.transaction_joinable = false
840
+ connection.begin_db_transaction
841
+ end
896
842
  # Load fixtures for every test.
897
843
  else
898
844
  ActiveRecord::Fixtures.reset_cache
@@ -912,13 +858,22 @@ module ActiveRecord
912
858
  end
913
859
 
914
860
  # Rollback changes if a transaction is active.
915
- if run_in_transaction? && ActiveRecord::Base.connection.open_transactions != 0
916
- ActiveRecord::Base.connection.rollback_db_transaction
917
- ActiveRecord::Base.connection.decrement_open_transactions
861
+ if run_in_transaction?
862
+ @fixture_connections.each do |connection|
863
+ if connection.open_transactions != 0
864
+ connection.rollback_db_transaction
865
+ connection.decrement_open_transactions
866
+ end
867
+ end
868
+ @fixture_connections.clear
918
869
  end
919
870
  ActiveRecord::Base.clear_active_connections!
920
871
  end
921
872
 
873
+ def enlist_fixture_connections
874
+ ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection)
875
+ end
876
+
922
877
  private
923
878
  def load_fixtures
924
879
  fixtures = ActiveRecord::Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
@@ -90,7 +90,7 @@ module ActiveRecord
90
90
  end
91
91
 
92
92
  def add(record)
93
- repository[record.class.symbolized_sti_name][record.id] = record if contain_all_columns?(record)
93
+ repository[record.class.symbolized_sti_name][record.id] = record
94
94
  end
95
95
 
96
96
  def remove(record)
@@ -104,12 +104,6 @@ module ActiveRecord
104
104
  def clear
105
105
  repository.clear
106
106
  end
107
-
108
- private
109
-
110
- def contain_all_columns?(record)
111
- (record.class.column_names - record.attribute_names).empty?
112
- end
113
107
  end
114
108
 
115
109
  # Reinitialize an Identity Map model object from +coder+.
@@ -117,13 +111,12 @@ module ActiveRecord
117
111
  # model object.
118
112
  def reinit_with(coder)
119
113
  @attributes_cache = {}
120
- dirty = @changed_attributes.keys
121
- @attributes.update(coder['attributes'].except(*dirty))
114
+ dirty = @changed_attributes.keys
115
+ attributes = self.class.initialize_attributes(coder['attributes'].except(*dirty))
116
+ @attributes.update(attributes)
122
117
  @changed_attributes.update(coder['attributes'].slice(*dirty))
123
118
  @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]}
124
119
 
125
- set_serialized_attributes
126
-
127
120
  run_callbacks :find
128
121
 
129
122
  self
@@ -0,0 +1,167 @@
1
+ require 'active_support/concern'
2
+
3
+ module ActiveRecord
4
+ module Inheritance
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Determine whether to store the full constant name including namespace when using STI
9
+ class_attribute :store_full_sti_class
10
+ self.store_full_sti_class = true
11
+ end
12
+
13
+ module ClassMethods
14
+ # True if this isn't a concrete subclass needing a STI type condition.
15
+ def descends_from_active_record?
16
+ if superclass.abstract_class?
17
+ superclass.descends_from_active_record?
18
+ else
19
+ superclass == Base || !columns_hash.include?(inheritance_column)
20
+ end
21
+ end
22
+
23
+ def finder_needs_type_condition? #:nodoc:
24
+ # This is like this because benchmarking justifies the strange :false stuff
25
+ :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
26
+ end
27
+
28
+ def symbolized_base_class
29
+ @symbolized_base_class ||= base_class.to_s.to_sym
30
+ end
31
+
32
+ def symbolized_sti_name
33
+ @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
34
+ end
35
+
36
+ # Returns the base AR subclass that this class descends from. If A
37
+ # extends AR::Base, A.base_class will return A. If B descends from A
38
+ # through some arbitrarily deep hierarchy, B.base_class will return A.
39
+ #
40
+ # If B < A and C < B and if A is an abstract_class then both B.base_class
41
+ # and C.base_class would return B as the answer since A is an abstract_class.
42
+ def base_class
43
+ class_of_active_record_descendant(self)
44
+ end
45
+
46
+ # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
47
+ attr_accessor :abstract_class
48
+
49
+ # Returns whether this class is an abstract class or not.
50
+ def abstract_class?
51
+ defined?(@abstract_class) && @abstract_class == true
52
+ end
53
+
54
+ def sti_name
55
+ store_full_sti_class ? name : name.demodulize
56
+ end
57
+
58
+ # Finder methods must instantiate through this method to work with the
59
+ # single-table inheritance model that makes it possible to create
60
+ # objects of different types from the same table.
61
+ def instantiate(record)
62
+ sti_class = find_sti_class(record[inheritance_column])
63
+ record_id = sti_class.primary_key && record[sti_class.primary_key]
64
+
65
+ if ActiveRecord::IdentityMap.enabled? && record_id
66
+ if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number?
67
+ record_id = record_id.to_i
68
+ end
69
+ if instance = IdentityMap.get(sti_class, record_id)
70
+ instance.reinit_with('attributes' => record)
71
+ else
72
+ instance = sti_class.allocate.init_with('attributes' => record)
73
+ IdentityMap.add(instance)
74
+ end
75
+ else
76
+ instance = sti_class.allocate.init_with('attributes' => record)
77
+ end
78
+
79
+ instance
80
+ end
81
+
82
+ protected
83
+
84
+ # Returns the class descending directly from ActiveRecord::Base or an
85
+ # abstract class, if any, in the inheritance hierarchy.
86
+ def class_of_active_record_descendant(klass)
87
+ if klass == Base || klass.superclass == Base || klass.superclass.abstract_class?
88
+ klass
89
+ elsif klass.superclass.nil?
90
+ raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
91
+ else
92
+ class_of_active_record_descendant(klass.superclass)
93
+ end
94
+ end
95
+
96
+ # Returns the class type of the record using the current module as a prefix. So descendants of
97
+ # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
98
+ def compute_type(type_name)
99
+ if type_name.match(/^::/)
100
+ # If the type is prefixed with a scope operator then we assume that
101
+ # the type_name is an absolute reference.
102
+ ActiveSupport::Dependencies.constantize(type_name)
103
+ else
104
+ # Build a list of candidates to search for
105
+ candidates = []
106
+ name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
107
+ candidates << type_name
108
+
109
+ candidates.each do |candidate|
110
+ begin
111
+ constant = ActiveSupport::Dependencies.constantize(candidate)
112
+ return constant if candidate == constant.to_s
113
+ rescue NameError => e
114
+ # We don't want to swallow NoMethodError < NameError errors
115
+ raise e unless e.instance_of?(NameError)
116
+ end
117
+ end
118
+
119
+ raise NameError, "uninitialized constant #{candidates.first}"
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def find_sti_class(type_name)
126
+ if type_name.blank? || !columns_hash.include?(inheritance_column)
127
+ self
128
+ else
129
+ begin
130
+ if store_full_sti_class
131
+ ActiveSupport::Dependencies.constantize(type_name)
132
+ else
133
+ compute_type(type_name)
134
+ end
135
+ rescue NameError
136
+ raise SubclassNotFound,
137
+ "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
138
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
139
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
140
+ "or overwrite #{name}.inheritance_column to use another column for that information."
141
+ end
142
+ end
143
+ end
144
+
145
+ def type_condition(table = arel_table)
146
+ sti_column = table[inheritance_column.to_sym]
147
+ sti_names = ([self] + descendants).map { |model| model.sti_name }
148
+
149
+ sti_column.in(sti_names)
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ # Sets the attribute used for single table inheritance to this class name if this is not the
156
+ # ActiveRecord::Base descendant.
157
+ # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
158
+ # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
159
+ # No such attribute would be set for objects of the Message class in that example.
160
+ def ensure_proper_type
161
+ klass = self.class
162
+ if klass.finder_needs_type_condition?
163
+ write_attribute(klass.inheritance_column, klass.sti_name)
164
+ end
165
+ end
166
+ end
167
+ end