activerecord 3.0.20 → 3.1.0.beta1

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 (122) hide show
  1. data/CHANGELOG +220 -91
  2. data/README.rdoc +3 -3
  3. data/examples/performance.rb +88 -109
  4. data/lib/active_record.rb +6 -2
  5. data/lib/active_record/aggregations.rb +22 -45
  6. data/lib/active_record/associations.rb +264 -991
  7. data/lib/active_record/associations/alias_tracker.rb +85 -0
  8. data/lib/active_record/associations/association.rb +231 -0
  9. data/lib/active_record/associations/association_scope.rb +120 -0
  10. data/lib/active_record/associations/belongs_to_association.rb +40 -60
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +15 -63
  12. data/lib/active_record/associations/builder/association.rb +53 -0
  13. data/lib/active_record/associations/builder/belongs_to.rb +85 -0
  14. data/lib/active_record/associations/builder/collection_association.rb +75 -0
  15. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +63 -0
  16. data/lib/active_record/associations/builder/has_many.rb +65 -0
  17. data/lib/active_record/associations/builder/has_one.rb +63 -0
  18. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  19. data/lib/active_record/associations/collection_association.rb +524 -0
  20. data/lib/active_record/associations/collection_proxy.rb +125 -0
  21. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +27 -118
  22. data/lib/active_record/associations/has_many_association.rb +50 -79
  23. data/lib/active_record/associations/has_many_through_association.rb +98 -67
  24. data/lib/active_record/associations/has_one_association.rb +45 -115
  25. data/lib/active_record/associations/has_one_through_association.rb +21 -25
  26. data/lib/active_record/associations/join_dependency.rb +215 -0
  27. data/lib/active_record/associations/join_dependency/join_association.rb +150 -0
  28. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  29. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  30. data/lib/active_record/associations/join_helper.rb +56 -0
  31. data/lib/active_record/associations/preloader.rb +177 -0
  32. data/lib/active_record/associations/preloader/association.rb +126 -0
  33. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  34. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  35. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  36. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  37. data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
  38. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  39. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  40. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  41. data/lib/active_record/associations/preloader/through_association.rb +67 -0
  42. data/lib/active_record/associations/singular_association.rb +55 -0
  43. data/lib/active_record/associations/through_association.rb +80 -0
  44. data/lib/active_record/attribute_methods.rb +19 -5
  45. data/lib/active_record/attribute_methods/before_type_cast.rb +9 -8
  46. data/lib/active_record/attribute_methods/dirty.rb +8 -2
  47. data/lib/active_record/attribute_methods/primary_key.rb +33 -13
  48. data/lib/active_record/attribute_methods/read.rb +17 -17
  49. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -4
  50. data/lib/active_record/attribute_methods/write.rb +2 -1
  51. data/lib/active_record/autosave_association.rb +66 -45
  52. data/lib/active_record/base.rb +445 -273
  53. data/lib/active_record/callbacks.rb +24 -33
  54. data/lib/active_record/coders/yaml_column.rb +41 -0
  55. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +106 -13
  56. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +16 -2
  57. data/lib/active_record/connection_adapters/abstract/database_limits.rb +12 -11
  58. data/lib/active_record/connection_adapters/abstract/database_statements.rb +83 -12
  59. data/lib/active_record/connection_adapters/abstract/query_cache.rb +16 -16
  60. data/lib/active_record/connection_adapters/abstract/quoting.rb +61 -22
  61. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +16 -273
  62. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +80 -42
  63. data/lib/active_record/connection_adapters/abstract_adapter.rb +44 -25
  64. data/lib/active_record/connection_adapters/column.rb +268 -0
  65. data/lib/active_record/connection_adapters/mysql2_adapter.rb +686 -0
  66. data/lib/active_record/connection_adapters/mysql_adapter.rb +331 -88
  67. data/lib/active_record/connection_adapters/postgresql_adapter.rb +295 -267
  68. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +3 -7
  69. data/lib/active_record/connection_adapters/sqlite_adapter.rb +108 -26
  70. data/lib/active_record/counter_cache.rb +7 -4
  71. data/lib/active_record/fixtures.rb +174 -192
  72. data/lib/active_record/identity_map.rb +131 -0
  73. data/lib/active_record/locking/optimistic.rb +20 -14
  74. data/lib/active_record/locking/pessimistic.rb +4 -4
  75. data/lib/active_record/log_subscriber.rb +24 -4
  76. data/lib/active_record/migration.rb +265 -144
  77. data/lib/active_record/migration/command_recorder.rb +103 -0
  78. data/lib/active_record/named_scope.rb +68 -25
  79. data/lib/active_record/nested_attributes.rb +58 -15
  80. data/lib/active_record/observer.rb +3 -7
  81. data/lib/active_record/persistence.rb +58 -38
  82. data/lib/active_record/query_cache.rb +25 -3
  83. data/lib/active_record/railtie.rb +21 -12
  84. data/lib/active_record/railties/console_sandbox.rb +6 -0
  85. data/lib/active_record/railties/databases.rake +147 -116
  86. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  87. data/lib/active_record/reflection.rb +176 -44
  88. data/lib/active_record/relation.rb +125 -49
  89. data/lib/active_record/relation/batches.rb +7 -5
  90. data/lib/active_record/relation/calculations.rb +50 -18
  91. data/lib/active_record/relation/finder_methods.rb +47 -26
  92. data/lib/active_record/relation/predicate_builder.rb +24 -21
  93. data/lib/active_record/relation/query_methods.rb +117 -101
  94. data/lib/active_record/relation/spawn_methods.rb +27 -20
  95. data/lib/active_record/result.rb +34 -0
  96. data/lib/active_record/schema.rb +5 -6
  97. data/lib/active_record/schema_dumper.rb +11 -13
  98. data/lib/active_record/serialization.rb +2 -2
  99. data/lib/active_record/serializers/xml_serializer.rb +10 -10
  100. data/lib/active_record/session_store.rb +8 -2
  101. data/lib/active_record/test_case.rb +9 -20
  102. data/lib/active_record/timestamp.rb +21 -9
  103. data/lib/active_record/transactions.rb +16 -15
  104. data/lib/active_record/validations.rb +21 -22
  105. data/lib/active_record/validations/associated.rb +3 -1
  106. data/lib/active_record/validations/uniqueness.rb +48 -58
  107. data/lib/active_record/version.rb +3 -3
  108. data/lib/rails/generators/active_record.rb +6 -0
  109. data/lib/rails/generators/active_record/migration/templates/migration.rb +10 -2
  110. data/lib/rails/generators/active_record/model/model_generator.rb +2 -1
  111. data/lib/rails/generators/active_record/model/templates/migration.rb +6 -5
  112. data/lib/rails/generators/active_record/model/templates/model.rb +2 -0
  113. data/lib/rails/generators/active_record/model/templates/module.rb +2 -0
  114. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  115. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +2 -1
  116. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +2 -2
  117. metadata +106 -77
  118. checksums.yaml +0 -7
  119. data/lib/active_record/association_preload.rb +0 -431
  120. data/lib/active_record/associations/association_collection.rb +0 -572
  121. data/lib/active_record/associations/association_proxy.rb +0 -304
  122. data/lib/active_record/associations/through_association_scope.rb +0 -160
@@ -0,0 +1,131 @@
1
+ module ActiveRecord
2
+ # = Active Record Identity Map
3
+ #
4
+ # Ensures that each object gets loaded only once by keeping every loaded
5
+ # object in a map. Looks up objects using the map when referring to them.
6
+ #
7
+ # More information on Identity Map pattern:
8
+ # http://www.martinfowler.com/eaaCatalog/identityMap.html
9
+ #
10
+ # == Configuration
11
+ #
12
+ # In order to enable IdentityMap, set <tt>config.active_record.identity_map = true</tt>
13
+ # in your <tt>config/application.rb</tt> file.
14
+ #
15
+ # IdentityMap is disabled by default.
16
+ #
17
+ module IdentityMap
18
+ extend ActiveSupport::Concern
19
+
20
+ class << self
21
+ def enabled=(flag)
22
+ Thread.current[:identity_map_enabled] = flag
23
+ end
24
+
25
+ def enabled
26
+ Thread.current[:identity_map_enabled]
27
+ end
28
+ alias enabled? enabled
29
+
30
+ def repository
31
+ Thread.current[:identity_map] ||= Hash.new { |h,k| h[k] = {} }
32
+ end
33
+
34
+ def use
35
+ old, self.enabled = enabled, true
36
+
37
+ yield if block_given?
38
+ ensure
39
+ self.enabled = old
40
+ clear
41
+ end
42
+
43
+ def without
44
+ old, self.enabled = enabled, false
45
+
46
+ yield if block_given?
47
+ ensure
48
+ self.enabled = old
49
+ end
50
+
51
+ def get(klass, primary_key)
52
+ record = repository[klass.symbolized_base_class][primary_key]
53
+
54
+ if record.is_a?(klass)
55
+ ActiveSupport::Notifications.instrument("identity.active_record",
56
+ :line => "From Identity Map (id: #{primary_key})",
57
+ :name => "#{klass} Loaded",
58
+ :connection_id => object_id)
59
+
60
+ record
61
+ else
62
+ nil
63
+ end
64
+ end
65
+
66
+ def add(record)
67
+ repository[record.class.symbolized_base_class][record.id] = record
68
+ end
69
+
70
+ def remove(record)
71
+ repository[record.class.symbolized_base_class].delete(record.id)
72
+ end
73
+
74
+ def remove_by_id(symbolized_base_class, id)
75
+ repository[symbolized_base_class].delete(id)
76
+ end
77
+
78
+ def clear
79
+ repository.clear
80
+ end
81
+ end
82
+
83
+ # Reinitialize an Identity Map model object from +coder+.
84
+ # +coder+ must contain the attributes necessary for initializing an empty
85
+ # model object.
86
+ def reinit_with(coder)
87
+ @attributes_cache = {}
88
+ dirty = @changed_attributes.keys
89
+ @attributes.update(coder['attributes'].except(*dirty))
90
+ @changed_attributes.update(coder['attributes'].slice(*dirty))
91
+ @changed_attributes.delete_if{|k,v| v.eql? @attributes[k]}
92
+
93
+ set_serialized_attributes
94
+
95
+ run_callbacks :find
96
+
97
+ self
98
+ end
99
+
100
+ class Middleware
101
+ class Body #:nodoc:
102
+ def initialize(target, original)
103
+ @target = target
104
+ @original = original
105
+ end
106
+
107
+ def each(&block)
108
+ @target.each(&block)
109
+ end
110
+
111
+ def close
112
+ @target.close if @target.respond_to?(:close)
113
+ ensure
114
+ IdentityMap.enabled = @original
115
+ IdentityMap.clear
116
+ end
117
+ end
118
+
119
+ def initialize(app)
120
+ @app = app
121
+ end
122
+
123
+ def call(env)
124
+ enabled = IdentityMap.enabled
125
+ IdentityMap.enabled = true
126
+ status, headers, body = @app.call(env)
127
+ [status, headers, Body.new(body, enabled)]
128
+ end
129
+ end
130
+ end
131
+ end
@@ -23,7 +23,7 @@ module ActiveRecord
23
23
  # p2.first_name = "should fail"
24
24
  # p2.save # Raises a ActiveRecord::StaleObjectError
25
25
  #
26
- # Optimistic locking will also check for stale data when objects are destroyed. Example:
26
+ # Optimistic locking will also check for stale data when objects are destroyed. Example:
27
27
  #
28
28
  # p1 = Person.find(1)
29
29
  # p2 = Person.find(1)
@@ -58,6 +58,12 @@ module ActiveRecord
58
58
  end
59
59
 
60
60
  private
61
+ def increment_lock
62
+ lock_col = self.class.locking_column
63
+ previous_lock_value = send(lock_col).to_i
64
+ send(lock_col + '=', previous_lock_value + 1)
65
+ end
66
+
61
67
  def attributes_from_column_definition
62
68
  result = super
63
69
 
@@ -70,7 +76,7 @@ module ActiveRecord
70
76
  result[self.class.locking_column] ||= 0
71
77
  end
72
78
 
73
- return result
79
+ result
74
80
  end
75
81
 
76
82
  def update(attribute_names = @attributes.keys) #:nodoc:
@@ -78,8 +84,8 @@ module ActiveRecord
78
84
  return 0 if attribute_names.empty?
79
85
 
80
86
  lock_col = self.class.locking_column
81
- previous_value = send(lock_col).to_i
82
- send(lock_col + '=', previous_value + 1)
87
+ previous_lock_value = send(lock_col).to_i
88
+ increment_lock
83
89
 
84
90
  attribute_names += [lock_col]
85
91
  attribute_names.uniq!
@@ -87,11 +93,13 @@ module ActiveRecord
87
93
  begin
88
94
  relation = self.class.unscoped
89
95
 
90
- affected_rows = relation.where(
96
+ stmt = relation.where(
91
97
  relation.table[self.class.primary_key].eq(quoted_id).and(
92
- relation.table[self.class.locking_column].eq(quote_value(previous_value))
98
+ relation.table[lock_col].eq(quote_value(previous_lock_value))
93
99
  )
94
- ).arel.update(arel_attributes_values(false, false, attribute_names))
100
+ ).arel.compile_update(arel_attributes_values(false, false, attribute_names))
101
+
102
+ affected_rows = connection.update stmt.to_sql
95
103
 
96
104
  unless affected_rows == 1
97
105
  raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"
@@ -101,7 +109,7 @@ module ActiveRecord
101
109
 
102
110
  # If something went wrong, revert the version.
103
111
  rescue Exception
104
- send(lock_col + '=', previous_value)
112
+ send(lock_col + '=', previous_lock_value)
105
113
  raise
106
114
  end
107
115
  end
@@ -109,13 +117,11 @@ module ActiveRecord
109
117
  def destroy #:nodoc:
110
118
  return super unless locking_enabled?
111
119
 
112
- unless new_record?
113
- lock_col = self.class.locking_column
114
- previous_value = send(lock_col).to_i
115
-
120
+ if persisted?
116
121
  table = self.class.arel_table
117
- predicate = table[self.class.primary_key].eq(id)
118
- predicate = predicate.and(table[self.class.locking_column].eq(previous_value))
122
+ lock_col = self.class.locking_column
123
+ predicate = table[self.class.primary_key].eq(id).
124
+ and(table[lock_col].eq(send(lock_col).to_i))
119
125
 
120
126
  affected_rows = self.class.unscoped.where(predicate).delete_all
121
127
 
@@ -9,9 +9,8 @@ module ActiveRecord
9
9
  # Account.find(1, :lock => true)
10
10
  #
11
11
  # Pass <tt>:lock => 'some locking clause'</tt> to give a database-specific locking clause
12
- # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'.
12
+ # of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
13
13
  #
14
- # Example:
15
14
  # Account.transaction do
16
15
  # # select * from accounts where name = 'shugo' limit 1 for update
17
16
  # shugo = Account.where("name = 'shugo'").lock(true).first
@@ -24,6 +23,7 @@ module ActiveRecord
24
23
  #
25
24
  # You can also use ActiveRecord::Base#lock! method to lock one record by id.
26
25
  # This may be better if you don't need to lock every row. Example:
26
+ #
27
27
  # Account.transaction do
28
28
  # # select * from accounts where ...
29
29
  # accounts = Account.where(...).all
@@ -44,10 +44,10 @@ module ActiveRecord
44
44
  module Pessimistic
45
45
  # Obtain a row lock on this record. Reloads the record to obtain the requested
46
46
  # lock. Pass an SQL locking clause to append the end of the SELECT statement
47
- # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
47
+ # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
48
48
  # the locked record.
49
49
  def lock!(lock = true)
50
- reload(:lock => lock) unless new_record?
50
+ reload(:lock => lock) if persisted?
51
51
  self
52
52
  end
53
53
  end
@@ -22,8 +22,19 @@ module ActiveRecord
22
22
  self.class.runtime += event.duration
23
23
  return unless logger.debug?
24
24
 
25
- name = '%s (%.1fms)' % [event.payload[:name], event.duration]
26
- sql = event.payload[:sql].squeeze(' ')
25
+ payload = event.payload
26
+
27
+ return if 'SCHEMA' == payload[:name]
28
+
29
+ name = '%s (%.1fms)' % [payload[:name], event.duration]
30
+ sql = payload[:sql].squeeze(' ')
31
+ binds = nil
32
+
33
+ unless (payload[:binds] || []).empty?
34
+ binds = " " + payload[:binds].map { |col,v|
35
+ [col.name, v]
36
+ }.inspect
37
+ end
27
38
 
28
39
  if odd?
29
40
  name = color(name, CYAN, true)
@@ -32,7 +43,16 @@ module ActiveRecord
32
43
  name = color(name, MAGENTA, true)
33
44
  end
34
45
 
35
- debug " #{name} #{sql}"
46
+ debug " #{name} #{sql}#{binds}"
47
+ end
48
+
49
+ def identity(event)
50
+ return unless logger.debug?
51
+
52
+ name = color(event.payload[:name], odd? ? CYAN : MAGENTA, true)
53
+ line = odd? ? color(event.payload[:line], nil, true) : event.payload[:line]
54
+
55
+ debug " #{name} #{line}"
36
56
  end
37
57
 
38
58
  def odd?
@@ -45,4 +65,4 @@ module ActiveRecord
45
65
  end
46
66
  end
47
67
 
48
- ActiveRecord::LogSubscriber.attach_to :active_record
68
+ ActiveRecord::LogSubscriber.attach_to :active_record
@@ -1,7 +1,4 @@
1
- require 'active_support/core_ext/kernel/singleton_class'
2
- require 'active_support/core_ext/module/aliasing'
3
- require 'active_support/core_ext/module/delegation'
4
- require 'active_support/core_ext/class/attribute_accessors'
1
+ require "active_support/core_ext/array/wrap"
5
2
 
6
3
  module ActiveRecord
7
4
  # Exception that can be raised to stop migrations from going backwards.
@@ -45,11 +42,11 @@ module ActiveRecord
45
42
  # Example of a simple migration:
46
43
  #
47
44
  # class AddSsl < ActiveRecord::Migration
48
- # def self.up
45
+ # def up
49
46
  # add_column :accounts, :ssl_enabled, :boolean, :default => 1
50
47
  # end
51
48
  #
52
- # def self.down
49
+ # def down
53
50
  # remove_column :accounts, :ssl_enabled
54
51
  # end
55
52
  # end
@@ -65,7 +62,7 @@ module ActiveRecord
65
62
  # Example of a more complex migration that also needs to initialize data:
66
63
  #
67
64
  # class AddSystemSettings < ActiveRecord::Migration
68
- # def self.up
65
+ # def up
69
66
  # create_table :system_settings do |t|
70
67
  # t.string :name
71
68
  # t.string :label
@@ -79,7 +76,7 @@ module ActiveRecord
79
76
  # :value => 1
80
77
  # end
81
78
  #
82
- # def self.down
79
+ # def down
83
80
  # drop_table :system_settings
84
81
  # end
85
82
  # end
@@ -140,7 +137,7 @@ module ActiveRecord
140
137
  # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the
141
138
  # UTC formatted date and time that the migration was generated.
142
139
  #
143
- # You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of
140
+ # You may then edit the <tt>up</tt> and <tt>down</tt> methods of
144
141
  # MyNewMigration.
145
142
  #
146
143
  # There is a special syntactic shortcut to generate migrations that add fields to a table.
@@ -149,11 +146,11 @@ module ActiveRecord
149
146
  #
150
147
  # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this:
151
148
  # class AddFieldnameToTablename < ActiveRecord::Migration
152
- # def self.up
149
+ # def up
153
150
  # add_column :tablenames, :fieldname, :string
154
151
  # end
155
152
  #
156
- # def self.down
153
+ # def down
157
154
  # remove_column :tablenames, :fieldname
158
155
  # end
159
156
  # end
@@ -181,11 +178,11 @@ module ActiveRecord
181
178
  # Not all migrations change the schema. Some just fix the data:
182
179
  #
183
180
  # class RemoveEmptyTags < ActiveRecord::Migration
184
- # def self.up
181
+ # def up
185
182
  # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
186
183
  # end
187
184
  #
188
- # def self.down
185
+ # def down
189
186
  # # not much we can do to restore deleted data
190
187
  # raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags"
191
188
  # end
@@ -194,12 +191,12 @@ module ActiveRecord
194
191
  # Others remove columns when they migrate up instead of down:
195
192
  #
196
193
  # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
197
- # def self.up
194
+ # def up
198
195
  # remove_column :items, :incomplete_items_count
199
196
  # remove_column :items, :completed_items_count
200
197
  # end
201
198
  #
202
- # def self.down
199
+ # def down
203
200
  # add_column :items, :incomplete_items_count
204
201
  # add_column :items, :completed_items_count
205
202
  # end
@@ -208,11 +205,11 @@ module ActiveRecord
208
205
  # And sometimes you need to do something in SQL not abstracted directly by migrations:
209
206
  #
210
207
  # class MakeJoinUnique < ActiveRecord::Migration
211
- # def self.up
208
+ # def up
212
209
  # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
213
210
  # end
214
211
  #
215
- # def self.down
212
+ # def down
216
213
  # execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`"
217
214
  # end
218
215
  # end
@@ -225,7 +222,7 @@ module ActiveRecord
225
222
  # latest column data from after the new column was added. Example:
226
223
  #
227
224
  # class AddPeopleSalary < ActiveRecord::Migration
228
- # def self.up
225
+ # def up
229
226
  # add_column :people, :salary, :integer
230
227
  # Person.reset_column_information
231
228
  # Person.find(:all).each do |p|
@@ -245,7 +242,7 @@ module ActiveRecord
245
242
  # You can also insert your own messages and benchmarks by using the +say_with_time+
246
243
  # method:
247
244
  #
248
- # def self.up
245
+ # def up
249
246
  # ...
250
247
  # say_with_time "Updating salaries..." do
251
248
  # Person.find(:all).each do |p|
@@ -288,111 +285,216 @@ module ActiveRecord
288
285
  #
289
286
  # In application.rb.
290
287
  #
288
+ # == Reversible Migrations
289
+ #
290
+ # Starting with Rails 3.1, you will be able to define reversible migrations.
291
+ # Reversible migrations are migrations that know how to go +down+ for you.
292
+ # You simply supply the +up+ logic, and the Migration system will figure out
293
+ # how to execute the down commands for you.
294
+ #
295
+ # To define a reversible migration, define the +change+ method in your
296
+ # migration like this:
297
+ #
298
+ # class TenderloveMigration < ActiveRecord::Migration
299
+ # def change
300
+ # create_table(:horses) do
301
+ # t.column :content, :text
302
+ # t.column :remind_at, :datetime
303
+ # end
304
+ # end
305
+ # end
306
+ #
307
+ # This migration will create the horses table for you on the way up, and
308
+ # automatically figure out how to drop the table on the way down.
309
+ #
310
+ # Some commands like +remove_column+ cannot be reversed. If you care to
311
+ # define how to move up and down in these cases, you should define the +up+
312
+ # and +down+ methods as before.
313
+ #
314
+ # If a command cannot be reversed, an
315
+ # <tt>ActiveRecord::IrreversibleMigration</tt> exception will be raised when
316
+ # the migration is moving down.
317
+ #
318
+ # For a list of commands that are reversible, please see
319
+ # <tt>ActiveRecord::Migration::CommandRecorder</tt>.
291
320
  class Migration
292
- @@verbose = true
293
- cattr_accessor :verbose
321
+ autoload :CommandRecorder, 'active_record/migration/command_recorder'
294
322
 
295
323
  class << self
296
- def up_with_benchmarks #:nodoc:
297
- migrate(:up)
298
- end
299
-
300
- def down_with_benchmarks #:nodoc:
301
- migrate(:down)
302
- end
324
+ attr_accessor :delegate # :nodoc:
325
+ end
303
326
 
304
- # Execute this migration in the named direction
305
- def migrate(direction)
306
- return unless respond_to?(direction)
327
+ def self.method_missing(name, *args, &block) # :nodoc:
328
+ (delegate || superclass.delegate).send(name, *args, &block)
329
+ end
307
330
 
308
- case direction
309
- when :up then announce "migrating"
310
- when :down then announce "reverting"
311
- end
331
+ cattr_accessor :verbose
312
332
 
313
- result = nil
314
- time = Benchmark.measure { result = send("#{direction}_without_benchmarks") }
333
+ attr_accessor :name, :version
315
334
 
316
- case direction
317
- when :up then announce "migrated (%.4fs)" % time.real; write
318
- when :down then announce "reverted (%.4fs)" % time.real; write
319
- end
335
+ def initialize
336
+ @name = self.class.name
337
+ @version = nil
338
+ @connection = nil
339
+ end
320
340
 
321
- result
322
- end
341
+ # instantiate the delegate object after initialize is defined
342
+ self.verbose = true
343
+ self.delegate = new
323
344
 
324
- # Because the method added may do an alias_method, it can be invoked
325
- # recursively. We use @ignore_new_methods as a guard to indicate whether
326
- # it is safe for the call to proceed.
327
- def singleton_method_added(sym) #:nodoc:
328
- return if defined?(@ignore_new_methods) && @ignore_new_methods
345
+ def up
346
+ self.class.delegate = self
347
+ return unless self.class.respond_to?(:up)
348
+ self.class.up
349
+ end
329
350
 
330
- begin
331
- @ignore_new_methods = true
351
+ def down
352
+ self.class.delegate = self
353
+ return unless self.class.respond_to?(:down)
354
+ self.class.down
355
+ end
332
356
 
333
- case sym
334
- when :up, :down
335
- singleton_class.send(:alias_method_chain, sym, "benchmarks")
357
+ # Execute this migration in the named direction
358
+ def migrate(direction)
359
+ return unless respond_to?(direction)
360
+
361
+ case direction
362
+ when :up then announce "migrating"
363
+ when :down then announce "reverting"
364
+ end
365
+
366
+ time = nil
367
+ ActiveRecord::Base.connection_pool.with_connection do |conn|
368
+ @connection = conn
369
+ if respond_to?(:change)
370
+ if direction == :down
371
+ recorder = CommandRecorder.new(@connection)
372
+ suppress_messages do
373
+ @connection = recorder
374
+ change
375
+ end
376
+ @connection = conn
377
+ time = Benchmark.measure {
378
+ recorder.inverse.each do |cmd, args|
379
+ send(cmd, *args)
380
+ end
381
+ }
382
+ else
383
+ time = Benchmark.measure { change }
336
384
  end
337
- ensure
338
- @ignore_new_methods = false
385
+ else
386
+ time = Benchmark.measure { send(direction) }
339
387
  end
388
+ @connection = nil
340
389
  end
341
390
 
342
- def write(text="")
343
- puts(text) if verbose
391
+ case direction
392
+ when :up then announce "migrated (%.4fs)" % time.real; write
393
+ when :down then announce "reverted (%.4fs)" % time.real; write
344
394
  end
395
+ end
345
396
 
346
- def announce(message)
347
- version = defined?(@version) ? @version : nil
397
+ def write(text="")
398
+ puts(text) if verbose
399
+ end
348
400
 
349
- text = "#{version} #{name}: #{message}"
350
- length = [0, 75 - text.length].max
351
- write "== %s %s" % [text, "=" * length]
352
- end
401
+ def announce(message)
402
+ text = "#{version} #{name}: #{message}"
403
+ length = [0, 75 - text.length].max
404
+ write "== %s %s" % [text, "=" * length]
405
+ end
353
406
 
354
- def say(message, subitem=false)
355
- write "#{subitem ? " ->" : "--"} #{message}"
356
- end
407
+ def say(message, subitem=false)
408
+ write "#{subitem ? " ->" : "--"} #{message}"
409
+ end
357
410
 
358
- def say_with_time(message)
359
- say(message)
360
- result = nil
361
- time = Benchmark.measure { result = yield }
362
- say "%.4fs" % time.real, :subitem
363
- say("#{result} rows", :subitem) if result.is_a?(Integer)
364
- result
365
- end
411
+ def say_with_time(message)
412
+ say(message)
413
+ result = nil
414
+ time = Benchmark.measure { result = yield }
415
+ say "%.4fs" % time.real, :subitem
416
+ say("#{result} rows", :subitem) if result.is_a?(Integer)
417
+ result
418
+ end
366
419
 
367
- def suppress_messages
368
- save, self.verbose = verbose, false
369
- yield
370
- ensure
371
- self.verbose = save
372
- end
420
+ def suppress_messages
421
+ save, self.verbose = verbose, false
422
+ yield
423
+ ensure
424
+ self.verbose = save
425
+ end
426
+
427
+ def connection
428
+ @connection || ActiveRecord::Base.connection
429
+ end
430
+
431
+ def method_missing(method, *arguments, &block)
432
+ arg_list = arguments.map{ |a| a.inspect } * ', '
373
433
 
374
- def connection
375
- ActiveRecord::Base.connection
434
+ say_with_time "#{method}(#{arg_list})" do
435
+ unless arguments.empty? || method == :execute
436
+ arguments[0] = Migrator.proper_table_name(arguments.first)
437
+ end
438
+ return super unless connection.respond_to?(method)
439
+ connection.send(method, *arguments, &block)
376
440
  end
441
+ end
442
+
443
+ def copy(destination, sources, options = {})
444
+ copied = []
445
+
446
+ FileUtils.mkdir_p(destination) unless File.exists?(destination)
377
447
 
378
- def method_missing(method, *arguments, &block)
379
- arg_list = arguments.map{ |a| a.inspect } * ', '
448
+ destination_migrations = ActiveRecord::Migrator.migrations(destination)
449
+ last = destination_migrations.last
450
+ sources.each do |name, path|
451
+ source_migrations = ActiveRecord::Migrator.migrations(path)
380
452
 
381
- say_with_time "#{method}(#{arg_list})" do
382
- unless arguments.empty? || method == :execute
383
- arguments[0] = Migrator.proper_table_name(arguments.first)
453
+ source_migrations.each do |migration|
454
+ source = File.read(migration.filename)
455
+ source = "# This migration comes from #{name} (originally #{migration.version})\n#{source}"
456
+
457
+ if duplicate = destination_migrations.detect { |m| m.name == migration.name }
458
+ options[:on_skip].call(name, migration) if File.read(duplicate.filename) != source && options[:on_skip]
459
+ next
384
460
  end
385
- connection.send(method, *arguments, &block)
461
+
462
+ migration.version = next_migration_number(last ? last.version + 1 : 0).to_i
463
+ new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.rb")
464
+ old_path, migration.filename = migration.filename, new_path
465
+ last = migration
466
+
467
+ FileUtils.cp(old_path, migration.filename)
468
+ copied << migration
469
+ options[:on_copy].call(name, migration, old_path) if options[:on_copy]
470
+ destination_migrations << migration
386
471
  end
387
472
  end
473
+
474
+ copied
475
+ end
476
+
477
+ def next_migration_number(number)
478
+ if ActiveRecord::Base.timestamped_migrations
479
+ [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max
480
+ else
481
+ "%.3d" % number
482
+ end
388
483
  end
389
484
  end
390
485
 
391
486
  # MigrationProxy is used to defer loading of the actual migration classes
392
487
  # until they are needed
393
- class MigrationProxy
488
+ class MigrationProxy < Struct.new(:name, :version, :filename)
394
489
 
395
- attr_accessor :name, :version, :filename
490
+ def initialize(name, version, filename)
491
+ super
492
+ @migration = nil
493
+ end
494
+
495
+ def basename
496
+ File.basename(filename)
497
+ end
396
498
 
397
499
  delegate :migrate, :announce, :write, :to=>:migration
398
500
 
@@ -404,47 +506,47 @@ module ActiveRecord
404
506
 
405
507
  def load_migration
406
508
  require(File.expand_path(filename))
407
- name.constantize
509
+ name.constantize.new
408
510
  end
409
511
 
410
512
  end
411
513
 
412
514
  class Migrator#:nodoc:
413
515
  class << self
414
- def migrate(migrations_path, target_version = nil)
516
+ attr_writer :migrations_paths
517
+ alias :migrations_path= :migrations_paths=
518
+
519
+ def migrate(migrations_paths, target_version = nil)
415
520
  case
416
521
  when target_version.nil?
417
- up(migrations_path, target_version)
522
+ up(migrations_paths, target_version)
418
523
  when current_version == 0 && target_version == 0
524
+ []
419
525
  when current_version > target_version
420
- down(migrations_path, target_version)
526
+ down(migrations_paths, target_version)
421
527
  else
422
- up(migrations_path, target_version)
528
+ up(migrations_paths, target_version)
423
529
  end
424
530
  end
425
531
 
426
- def rollback(migrations_path, steps=1)
427
- move(:down, migrations_path, steps)
428
- end
429
-
430
- def forward(migrations_path, steps=1)
431
- move(:up, migrations_path, steps)
532
+ def rollback(migrations_paths, steps=1)
533
+ move(:down, migrations_paths, steps)
432
534
  end
433
535
 
434
- def up(migrations_path, target_version = nil)
435
- self.new(:up, migrations_path, target_version).migrate
536
+ def forward(migrations_paths, steps=1)
537
+ move(:up, migrations_paths, steps)
436
538
  end
437
539
 
438
- def down(migrations_path, target_version = nil)
439
- self.new(:down, migrations_path, target_version).migrate
540
+ def up(migrations_paths, target_version = nil)
541
+ self.new(:up, migrations_paths, target_version).migrate
440
542
  end
441
543
 
442
- def run(direction, migrations_path, target_version)
443
- self.new(direction, migrations_path, target_version).run
544
+ def down(migrations_paths, target_version = nil)
545
+ self.new(:down, migrations_paths, target_version).migrate
444
546
  end
445
547
 
446
- def migrations_path
447
- 'db/migrate'
548
+ def run(direction, migrations_paths, target_version)
549
+ self.new(direction, migrations_paths, target_version).run
448
550
  end
449
551
 
450
552
  def schema_migrations_table_name
@@ -470,24 +572,59 @@ module ActiveRecord
470
572
  name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
471
573
  end
472
574
 
575
+ def migrations_paths
576
+ @migrations_paths ||= ['db/migrate']
577
+ # just to not break things if someone uses: migration_path = some_string
578
+ Array.wrap(@migrations_paths)
579
+ end
580
+
581
+ def migrations_path
582
+ migrations_paths.first
583
+ end
584
+
585
+ def migrations(paths)
586
+ paths = Array.wrap(paths)
587
+
588
+ files = Dir[*paths.map { |p| "#{p}/[0-9]*_*.rb" }]
589
+
590
+ seen = Hash.new false
591
+
592
+ migrations = files.map do |file|
593
+ version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
594
+
595
+ raise IllegalMigrationNameError.new(file) unless version
596
+ version = version.to_i
597
+ name = name.camelize
598
+
599
+ raise DuplicateMigrationVersionError.new(version) if seen[version]
600
+ raise DuplicateMigrationNameError.new(name) if seen[name]
601
+
602
+ seen[version] = seen[name] = true
603
+
604
+ MigrationProxy.new(name, version, file)
605
+ end
606
+
607
+ migrations.sort_by(&:version)
608
+ end
609
+
473
610
  private
474
611
 
475
- def move(direction, migrations_path, steps)
476
- migrator = self.new(direction, migrations_path)
612
+ def move(direction, migrations_paths, steps)
613
+ migrator = self.new(direction, migrations_paths)
477
614
  start_index = migrator.migrations.index(migrator.current_migration)
478
615
 
479
616
  if start_index
480
617
  finish = migrator.migrations[start_index + steps]
481
618
  version = finish ? finish.version : 0
482
- send(direction, migrations_path, version)
619
+ send(direction, migrations_paths, version)
483
620
  end
484
621
  end
485
622
  end
486
623
 
487
- def initialize(direction, migrations_path, target_version = nil)
624
+ def initialize(direction, migrations_paths, target_version = nil)
488
625
  raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
489
626
  Base.connection.initialize_schema_migrations_table
490
- @direction, @migrations_path, @target_version = direction, migrations_path, target_version
627
+ @direction, @migrations_paths, @target_version = direction, migrations_paths, target_version
491
628
  end
492
629
 
493
630
  def current_version
@@ -511,7 +648,7 @@ module ActiveRecord
511
648
  current = migrations.detect { |m| m.version == current_version }
512
649
  target = migrations.detect { |m| m.version == @target_version }
513
650
 
514
- if target.nil? && !@target_version.nil? && @target_version > 0
651
+ if target.nil? && @target_version && @target_version > 0
515
652
  raise UnknownMigrationVersionError.new(@target_version)
516
653
  end
517
654
 
@@ -520,16 +657,19 @@ module ActiveRecord
520
657
  runnable = migrations[start..finish]
521
658
 
522
659
  # skip the last migration if we're headed down, but not ALL the way down
523
- runnable.pop if down? && !target.nil?
660
+ runnable.pop if down? && target
524
661
 
662
+ ran = []
525
663
  runnable.each do |migration|
526
664
  Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
527
665
 
666
+ seen = migrated.include?(migration.version.to_i)
667
+
528
668
  # On our way up, we skip migrating the ones we've already migrated
529
- next if up? && migrated.include?(migration.version.to_i)
669
+ next if up? && seen
530
670
 
531
671
  # On our way down, we skip reverting the ones we've never migrated
532
- if down? && !migrated.include?(migration.version.to_i)
672
+ if down? && !seen
533
673
  migration.announce 'never migrated, skipping'; migration.write
534
674
  next
535
675
  end
@@ -539,39 +679,18 @@ module ActiveRecord
539
679
  migration.migrate(@direction)
540
680
  record_version_state_after_migrating(migration.version)
541
681
  end
682
+ ran << migration
542
683
  rescue => e
543
684
  canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
544
685
  raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
545
686
  end
546
687
  end
688
+ ran
547
689
  end
548
690
 
549
691
  def migrations
550
692
  @migrations ||= begin
551
- files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
552
-
553
- migrations = files.inject([]) do |klasses, file|
554
- version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
555
-
556
- raise IllegalMigrationNameError.new(file) unless version
557
- version = version.to_i
558
-
559
- if klasses.detect { |m| m.version == version }
560
- raise DuplicateMigrationVersionError.new(version)
561
- end
562
-
563
- if klasses.detect { |m| m.name == name.camelize }
564
- raise DuplicateMigrationNameError.new(name.camelize)
565
- end
566
-
567
- migration = MigrationProxy.new
568
- migration.name = name.camelize
569
- migration.version = version
570
- migration.filename = file
571
- klasses << migration
572
- end
573
-
574
- migrations = migrations.sort_by { |m| m.version }
693
+ migrations = self.class.migrations(@migrations_paths)
575
694
  down? ? migrations.reverse : migrations
576
695
  end
577
696
  end
@@ -592,10 +711,12 @@ module ActiveRecord
592
711
  @migrated_versions ||= []
593
712
  if down?
594
713
  @migrated_versions.delete(version)
595
- table.where(table["version"].eq(version.to_s)).delete
714
+ stmt = table.where(table["version"].eq(version.to_s)).compile_delete
715
+ Base.connection.delete stmt.to_sql
596
716
  else
597
717
  @migrated_versions.push(version).sort!
598
- table.insert table["version"] => version.to_s
718
+ stmt = table.compile_insert table["version"] => version.to_s
719
+ Base.connection.insert stmt.to_sql
599
720
  end
600
721
  end
601
722