activerecord 3.1.12 → 3.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (99) hide show
  1. data/CHANGELOG.md +6263 -103
  2. data/README.rdoc +2 -2
  3. data/examples/performance.rb +55 -31
  4. data/lib/active_record.rb +28 -2
  5. data/lib/active_record/aggregations.rb +2 -2
  6. data/lib/active_record/associations.rb +82 -69
  7. data/lib/active_record/associations/association.rb +2 -37
  8. data/lib/active_record/associations/association_scope.rb +3 -30
  9. data/lib/active_record/associations/builder/association.rb +6 -4
  10. data/lib/active_record/associations/builder/belongs_to.rb +3 -3
  11. data/lib/active_record/associations/builder/collection_association.rb +2 -2
  12. data/lib/active_record/associations/builder/has_many.rb +4 -4
  13. data/lib/active_record/associations/builder/has_one.rb +5 -6
  14. data/lib/active_record/associations/builder/singular_association.rb +3 -16
  15. data/lib/active_record/associations/collection_association.rb +55 -28
  16. data/lib/active_record/associations/collection_proxy.rb +1 -35
  17. data/lib/active_record/associations/has_many_association.rb +5 -1
  18. data/lib/active_record/associations/has_many_through_association.rb +11 -8
  19. data/lib/active_record/associations/join_dependency.rb +1 -1
  20. data/lib/active_record/associations/preloader/association.rb +3 -1
  21. data/lib/active_record/attribute_assignment.rb +221 -0
  22. data/lib/active_record/attribute_methods.rb +212 -32
  23. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  24. data/lib/active_record/attribute_methods/dirty.rb +3 -3
  25. data/lib/active_record/attribute_methods/primary_key.rb +62 -25
  26. data/lib/active_record/attribute_methods/read.rb +69 -80
  27. data/lib/active_record/attribute_methods/serialization.rb +89 -0
  28. data/lib/active_record/attribute_methods/time_zone_conversion.rb +9 -14
  29. data/lib/active_record/attribute_methods/write.rb +27 -5
  30. data/lib/active_record/autosave_association.rb +23 -8
  31. data/lib/active_record/base.rb +223 -1712
  32. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +98 -132
  33. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +82 -29
  34. data/lib/active_record/connection_adapters/abstract/database_statements.rb +13 -42
  35. data/lib/active_record/connection_adapters/abstract/query_cache.rb +1 -1
  36. data/lib/active_record/connection_adapters/abstract/quoting.rb +7 -4
  37. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +36 -25
  38. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +41 -13
  39. data/lib/active_record/connection_adapters/abstract_adapter.rb +78 -43
  40. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
  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 +1 -1
  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.rb +31 -76
  53. data/lib/active_record/fixtures/file.rb +65 -0
  54. data/lib/active_record/identity_map.rb +1 -7
  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 +19 -11
  58. data/lib/active_record/locking/pessimistic.rb +1 -1
  59. data/lib/active_record/log_subscriber.rb +3 -3
  60. data/lib/active_record/migration.rb +38 -29
  61. data/lib/active_record/migration/command_recorder.rb +7 -7
  62. data/lib/active_record/model_schema.rb +362 -0
  63. data/lib/active_record/nested_attributes.rb +3 -2
  64. data/lib/active_record/persistence.rb +51 -1
  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 +133 -77
  69. data/lib/active_record/readonly_attributes.rb +26 -0
  70. data/lib/active_record/reflection.rb +7 -15
  71. data/lib/active_record/relation.rb +78 -35
  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 +5 -4
  76. data/lib/active_record/relation/predicate_builder.rb +13 -16
  77. data/lib/active_record/relation/query_methods.rb +59 -4
  78. data/lib/active_record/result.rb +1 -1
  79. data/lib/active_record/sanitization.rb +194 -0
  80. data/lib/active_record/schema_dumper.rb +5 -2
  81. data/lib/active_record/scoping.rb +152 -0
  82. data/lib/active_record/scoping/default.rb +140 -0
  83. data/lib/active_record/scoping/named.rb +202 -0
  84. data/lib/active_record/serialization.rb +1 -43
  85. data/lib/active_record/serializers/xml_serializer.rb +2 -44
  86. data/lib/active_record/session_store.rb +11 -11
  87. data/lib/active_record/store.rb +50 -0
  88. data/lib/active_record/test_case.rb +11 -7
  89. data/lib/active_record/timestamp.rb +16 -3
  90. data/lib/active_record/transactions.rb +5 -5
  91. data/lib/active_record/translation.rb +22 -0
  92. data/lib/active_record/validations.rb +1 -1
  93. data/lib/active_record/validations/associated.rb +5 -4
  94. data/lib/active_record/validations/uniqueness.rb +4 -4
  95. data/lib/active_record/version.rb +3 -3
  96. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
  97. metadata +48 -38
  98. checksums.yaml +0 -7
  99. data/lib/active_record/named_scope.rb +0 -200
@@ -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
@@ -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+.
@@ -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
@@ -0,0 +1,49 @@
1
+ module ActiveRecord
2
+ module Integration
3
+ # Returns a String, which Action Pack uses for constructing an URL to this
4
+ # object. The default implementation returns this record's id as a String,
5
+ # or nil if this record's unsaved.
6
+ #
7
+ # For example, suppose that you have a User model, and that you have a
8
+ # <tt>resources :users</tt> route. Normally, +user_path+ will
9
+ # construct a path with the user object's 'id' in it:
10
+ #
11
+ # user = User.find_by_name('Phusion')
12
+ # user_path(user) # => "/users/1"
13
+ #
14
+ # You can override +to_param+ in your model to make +user_path+ construct
15
+ # a path using the user's name instead of the user's id:
16
+ #
17
+ # class User < ActiveRecord::Base
18
+ # def to_param # overridden
19
+ # name
20
+ # end
21
+ # end
22
+ #
23
+ # user = User.find_by_name('Phusion')
24
+ # user_path(user) # => "/users/Phusion"
25
+ def to_param
26
+ # We can't use alias_method here, because method 'id' optimizes itself on the fly.
27
+ id && id.to_s # Be sure to stringify the id for routes
28
+ end
29
+
30
+ # Returns a cache key that can be used to identify this record.
31
+ #
32
+ # ==== Examples
33
+ #
34
+ # Product.new.cache_key # => "products/new"
35
+ # Product.find(5).cache_key # => "products/5" (updated_at not available)
36
+ # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
37
+ def cache_key
38
+ case
39
+ when new_record?
40
+ "#{self.class.model_name.cache_key}/new"
41
+ when timestamp = self[:updated_at]
42
+ timestamp = timestamp.utc.to_s(:number)
43
+ "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
44
+ else
45
+ "#{self.class.model_name.cache_key}/#{id}"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -37,6 +37,9 @@ module ActiveRecord
37
37
  # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
38
38
  # or otherwise apply the business logic needed to resolve the conflict.
39
39
  #
40
+ # This locking mechanism will function inside a single Ruby process. To make it work across all
41
+ # web requests, the recommended approach is to add +lock_version+ as a hidden field to your form.
42
+ #
40
43
  # You must ensure that your database schema defaults the +lock_version+ column to 0.
41
44
  #
42
45
  # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
@@ -48,10 +51,6 @@ module ActiveRecord
48
51
  included do
49
52
  cattr_accessor :lock_optimistically, :instance_writer => false
50
53
  self.lock_optimistically = true
51
-
52
- class << self
53
- alias_method :locking_column=, :set_locking_column
54
- end
55
54
  end
56
55
 
57
56
  def locking_enabled? #:nodoc:
@@ -66,7 +65,7 @@ module ActiveRecord
66
65
  end
67
66
 
68
67
  def attributes_from_column_definition
69
- result = super
68
+ result = self.class.column_defaults.dup
70
69
 
71
70
  # If the locking column has no default value set,
72
71
  # start the lock version at zero. Note we can't use
@@ -103,7 +102,7 @@ module ActiveRecord
103
102
  affected_rows = connection.update stmt
104
103
 
105
104
  unless affected_rows == 1
106
- raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"
105
+ raise ActiveRecord::StaleObjectError.new(self, "update")
107
106
  end
108
107
 
109
108
  affected_rows
@@ -127,7 +126,7 @@ module ActiveRecord
127
126
  affected_rows = self.class.unscoped.where(predicate).delete_all
128
127
 
129
128
  unless affected_rows == 1
130
- raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}"
129
+ raise ActiveRecord::StaleObjectError.new(self, "destroy")
131
130
  end
132
131
  end
133
132
 
@@ -145,15 +144,24 @@ module ActiveRecord
145
144
  lock_optimistically && columns_hash[locking_column]
146
145
  end
147
146
 
147
+ def locking_column=(value)
148
+ @original_locking_column = @locking_column if defined?(@locking_column)
149
+ @locking_column = value.to_s
150
+ end
151
+
148
152
  # Set the column to use for optimistic locking. Defaults to +lock_version+.
149
153
  def set_locking_column(value = nil, &block)
150
- define_attr_method :locking_column, value, &block
151
- value
154
+ deprecated_property_setter :locking_column, value, block
152
155
  end
153
156
 
154
157
  # The version column used for optimistic locking. Defaults to +lock_version+.
155
158
  def locking_column
156
- reset_locking_column
159
+ reset_locking_column unless defined?(@locking_column)
160
+ @locking_column
161
+ end
162
+
163
+ def original_locking_column #:nodoc:
164
+ deprecated_original_property_getter :locking_column
157
165
  end
158
166
 
159
167
  # Quote the column name used for optimistic locking.
@@ -163,7 +171,7 @@ module ActiveRecord
163
171
 
164
172
  # Reset the column used for optimistic locking back to the +lock_version+ default.
165
173
  def reset_locking_column
166
- set_locking_column DEFAULT_LOCKING_COLUMN
174
+ self.locking_column = DEFAULT_LOCKING_COLUMN
167
175
  end
168
176
 
169
177
  # Make sure the lock version column gets updated when counters are
@@ -14,7 +14,7 @@ module ActiveRecord
14
14
  # Account.transaction do
15
15
  # # select * from accounts where name = 'shugo' limit 1 for update
16
16
  # shugo = Account.where("name = 'shugo'").lock(true).first
17
- # yuko = Account.where("name = 'shugo'").lock(true).first
17
+ # yuko = Account.where("name = 'yuko'").lock(true).first
18
18
  # shugo.balance -= 100
19
19
  # shugo.save!
20
20
  # yuko.balance += 100
@@ -26,9 +26,9 @@ module ActiveRecord
26
26
 
27
27
  return if 'SCHEMA' == payload[:name]
28
28
 
29
- name = '%s (%.1fms)' % [payload[:name], event.duration]
30
- sql = payload[:sql].squeeze(' ')
31
- binds = nil
29
+ name = '%s (%.1fms)' % [payload[:name], event.duration]
30
+ sql = payload[:sql].squeeze(' ')
31
+ binds = nil
32
32
 
33
33
  unless (payload[:binds] || []).empty?
34
34
  binds = " " + payload[:binds].map { |col,v|
@@ -68,9 +68,9 @@ module ActiveRecord
68
68
  # create_table :system_settings do |t|
69
69
  # t.string :name
70
70
  # t.string :label
71
- # t.text :value
71
+ # t.text :value
72
72
  # t.string :type
73
- # t.integer :position
73
+ # t.integer :position
74
74
  # end
75
75
  #
76
76
  # SystemSetting.create :name => "notice",
@@ -116,8 +116,9 @@ module ActiveRecord
116
116
  # +column_names+ from the table called +table_name+.
117
117
  # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
118
118
  # with the name of the column. Other options include
119
- # <tt>:name</tt> and <tt>:unique</tt> (e.g.
120
- # <tt>{ :name => "users_name_index", :unique => true }</tt>).
119
+ # <tt>:name</tt>, <tt>:unique</tt> (e.g.
120
+ # <tt>{ :name => "users_name_index", :unique => true }</tt>) and <tt>:order</tt>
121
+ # (e.g. { :order => {:name => :desc} }</tt>).
121
122
  # * <tt>remove_index(table_name, :column => column_name)</tt>: Removes the index
122
123
  # specified by +column_name+.
123
124
  # * <tt>remove_index(table_name, :name => index_name)</tt>: Removes the index
@@ -183,7 +184,7 @@ module ActiveRecord
183
184
  #
184
185
  # class RemoveEmptyTags < ActiveRecord::Migration
185
186
  # def up
186
- # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
187
+ # Tag.all.each { |tag| tag.destroy if tag.pages.empty? }
187
188
  # end
188
189
  #
189
190
  # def down
@@ -229,7 +230,7 @@ module ActiveRecord
229
230
  # def up
230
231
  # add_column :people, :salary, :integer
231
232
  # Person.reset_column_information
232
- # Person.find(:all).each do |p|
233
+ # Person.all.each do |p|
233
234
  # p.update_attribute :salary, SalaryCalculator.compute(p)
234
235
  # end
235
236
  # end
@@ -249,7 +250,7 @@ module ActiveRecord
249
250
  # def up
250
251
  # ...
251
252
  # say_with_time "Updating salaries..." do
252
- # Person.find(:all).each do |p|
253
+ # Person.all.each do |p|
253
254
  # p.update_attribute :salary, SalaryCalculator.compute(p)
254
255
  # end
255
256
  # end
@@ -301,7 +302,7 @@ module ActiveRecord
301
302
  #
302
303
  # class TenderloveMigration < ActiveRecord::Migration
303
304
  # def change
304
- # create_table(:horses) do
305
+ # create_table(:horses) do |t|
305
306
  # t.column :content, :text
306
307
  # t.column :remind_at, :datetime
307
308
  # end
@@ -442,6 +443,7 @@ module ActiveRecord
442
443
  say_with_time "#{method}(#{arg_list})" do
443
444
  unless arguments.empty? || method == :execute
444
445
  arguments[0] = Migrator.proper_table_name(arguments.first)
446
+ arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table
445
447
  end
446
448
  return super unless connection.respond_to?(method)
447
449
  connection.send(method, *arguments, &block)
@@ -455,26 +457,28 @@ module ActiveRecord
455
457
 
456
458
  destination_migrations = ActiveRecord::Migrator.migrations(destination)
457
459
  last = destination_migrations.last
458
- sources.each do |name, path|
460
+ sources.each do |scope, path|
459
461
  source_migrations = ActiveRecord::Migrator.migrations(path)
460
462
 
461
463
  source_migrations.each do |migration|
462
464
  source = File.read(migration.filename)
463
- source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}"
465
+ source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}"
464
466
 
465
467
  if duplicate = destination_migrations.detect { |m| m.name == migration.name }
466
- options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip]
468
+ if options[:on_skip] && duplicate.scope != scope.to_s
469
+ options[:on_skip].call(scope, migration)
470
+ end
467
471
  next
468
472
  end
469
473
 
470
474
  migration.version = next_migration_number(last ? last.version + 1 : 0).to_i
471
- new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb")
475
+ new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.#{scope}.rb")
472
476
  old_path, migration.filename = migration.filename, new_path
473
477
  last = migration
474
478
 
475
- FileUtils.cp(old_path, migration.filename)
479
+ File.open(migration.filename, "w") { |f| f.write source }
476
480
  copied << migration
477
- options[:on_copy].call(name, migration, old_path) if options[:on_copy]
481
+ options[:on_copy].call(scope, migration, old_path) if options[:on_copy]
478
482
  destination_migrations << migration
479
483
  end
480
484
  end
@@ -493,9 +497,9 @@ module ActiveRecord
493
497
 
494
498
  # MigrationProxy is used to defer loading of the actual migration classes
495
499
  # until they are needed
496
- class MigrationProxy < Struct.new(:name, :version, :filename)
500
+ class MigrationProxy < Struct.new(:name, :version, :filename, :scope)
497
501
 
498
- def initialize(name, version, filename)
502
+ def initialize(name, version, filename, scope)
499
503
  super
500
504
  @migration = nil
501
505
  end
@@ -524,16 +528,16 @@ module ActiveRecord
524
528
  attr_writer :migrations_paths
525
529
  alias :migrations_path= :migrations_paths=
526
530
 
527
- def migrate(migrations_paths, target_version = nil)
531
+ def migrate(migrations_paths, target_version = nil, &block)
528
532
  case
529
533
  when target_version.nil?
530
- up(migrations_paths, target_version)
534
+ up(migrations_paths, target_version, &block)
531
535
  when current_version == 0 && target_version == 0
532
536
  []
533
537
  when current_version > target_version
534
- down(migrations_paths, target_version)
538
+ down(migrations_paths, target_version, &block)
535
539
  else
536
- up(migrations_paths, target_version)
540
+ up(migrations_paths, target_version, &block)
537
541
  end
538
542
  end
539
543
 
@@ -545,12 +549,12 @@ module ActiveRecord
545
549
  move(:up, migrations_paths, steps)
546
550
  end
547
551
 
548
- def up(migrations_paths, target_version = nil)
549
- self.new(:up, migrations_paths, target_version).migrate
552
+ def up(migrations_paths, target_version = nil, &block)
553
+ self.new(:up, migrations_paths, target_version).migrate(&block)
550
554
  end
551
555
 
552
- def down(migrations_paths, target_version = nil)
553
- self.new(:down, migrations_paths, target_version).migrate
556
+ def down(migrations_paths, target_version = nil, &block)
557
+ self.new(:down, migrations_paths, target_version).migrate(&block)
554
558
  end
555
559
 
556
560
  def run(direction, migrations_paths, target_version)
@@ -590,15 +594,16 @@ module ActiveRecord
590
594
  migrations_paths.first
591
595
  end
592
596
 
593
- def migrations(paths)
597
+ def migrations(paths, subdirectories = true)
594
598
  paths = Array.wrap(paths)
595
599
 
596
- files = Dir[*paths.map { |p| "#{p}/[0-9]*_*.rb" }]
600
+ glob = subdirectories ? "**/" : ""
601
+ files = Dir[*paths.map { |p| "#{p}/#{glob}[0-9]*_*.rb" }]
597
602
 
598
603
  seen = Hash.new false
599
604
 
600
605
  migrations = files.map do |file|
601
- version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
606
+ version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?.rb/).first
602
607
 
603
608
  raise IllegalMigrationNameError.new(file) unless version
604
609
  version = version.to_i
@@ -609,7 +614,7 @@ module ActiveRecord
609
614
 
610
615
  seen[version] = seen[name] = true
611
616
 
612
- MigrationProxy.new(name, version, file)
617
+ MigrationProxy.new(name, version, file, scope)
613
618
  end
614
619
 
615
620
  migrations.sort_by(&:version)
@@ -652,7 +657,7 @@ module ActiveRecord
652
657
  end
653
658
  end
654
659
 
655
- def migrate
660
+ def migrate(&block)
656
661
  current = migrations.detect { |m| m.version == current_version }
657
662
  target = migrations.detect { |m| m.version == @target_version }
658
663
 
@@ -669,6 +674,10 @@ module ActiveRecord
669
674
 
670
675
  ran = []
671
676
  runnable.each do |migration|
677
+ if block && !block.call(migration)
678
+ next
679
+ end
680
+
672
681
  Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
673
682
 
674
683
  seen = migrated.include?(migration.version.to_i)