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.
- data/CHANGELOG +220 -91
- data/README.rdoc +3 -3
- data/examples/performance.rb +88 -109
- data/lib/active_record.rb +6 -2
- data/lib/active_record/aggregations.rb +22 -45
- data/lib/active_record/associations.rb +264 -991
- data/lib/active_record/associations/alias_tracker.rb +85 -0
- data/lib/active_record/associations/association.rb +231 -0
- data/lib/active_record/associations/association_scope.rb +120 -0
- data/lib/active_record/associations/belongs_to_association.rb +40 -60
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +15 -63
- data/lib/active_record/associations/builder/association.rb +53 -0
- data/lib/active_record/associations/builder/belongs_to.rb +85 -0
- data/lib/active_record/associations/builder/collection_association.rb +75 -0
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +63 -0
- data/lib/active_record/associations/builder/has_many.rb +65 -0
- data/lib/active_record/associations/builder/has_one.rb +63 -0
- data/lib/active_record/associations/builder/singular_association.rb +32 -0
- data/lib/active_record/associations/collection_association.rb +524 -0
- data/lib/active_record/associations/collection_proxy.rb +125 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +27 -118
- data/lib/active_record/associations/has_many_association.rb +50 -79
- data/lib/active_record/associations/has_many_through_association.rb +98 -67
- data/lib/active_record/associations/has_one_association.rb +45 -115
- data/lib/active_record/associations/has_one_through_association.rb +21 -25
- data/lib/active_record/associations/join_dependency.rb +215 -0
- data/lib/active_record/associations/join_dependency/join_association.rb +150 -0
- data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
- data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
- data/lib/active_record/associations/join_helper.rb +56 -0
- data/lib/active_record/associations/preloader.rb +177 -0
- data/lib/active_record/associations/preloader/association.rb +126 -0
- data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
- data/lib/active_record/associations/preloader/collection_association.rb +24 -0
- data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
- data/lib/active_record/associations/preloader/has_many.rb +17 -0
- data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
- data/lib/active_record/associations/preloader/has_one.rb +23 -0
- data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
- data/lib/active_record/associations/preloader/singular_association.rb +21 -0
- data/lib/active_record/associations/preloader/through_association.rb +67 -0
- data/lib/active_record/associations/singular_association.rb +55 -0
- data/lib/active_record/associations/through_association.rb +80 -0
- data/lib/active_record/attribute_methods.rb +19 -5
- data/lib/active_record/attribute_methods/before_type_cast.rb +9 -8
- data/lib/active_record/attribute_methods/dirty.rb +8 -2
- data/lib/active_record/attribute_methods/primary_key.rb +33 -13
- data/lib/active_record/attribute_methods/read.rb +17 -17
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -4
- data/lib/active_record/attribute_methods/write.rb +2 -1
- data/lib/active_record/autosave_association.rb +66 -45
- data/lib/active_record/base.rb +445 -273
- data/lib/active_record/callbacks.rb +24 -33
- data/lib/active_record/coders/yaml_column.rb +41 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +106 -13
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +16 -2
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +12 -11
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +83 -12
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +16 -16
- data/lib/active_record/connection_adapters/abstract/quoting.rb +61 -22
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +16 -273
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +80 -42
- data/lib/active_record/connection_adapters/abstract_adapter.rb +44 -25
- data/lib/active_record/connection_adapters/column.rb +268 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +686 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +331 -88
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +295 -267
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +3 -7
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +108 -26
- data/lib/active_record/counter_cache.rb +7 -4
- data/lib/active_record/fixtures.rb +174 -192
- data/lib/active_record/identity_map.rb +131 -0
- data/lib/active_record/locking/optimistic.rb +20 -14
- data/lib/active_record/locking/pessimistic.rb +4 -4
- data/lib/active_record/log_subscriber.rb +24 -4
- data/lib/active_record/migration.rb +265 -144
- data/lib/active_record/migration/command_recorder.rb +103 -0
- data/lib/active_record/named_scope.rb +68 -25
- data/lib/active_record/nested_attributes.rb +58 -15
- data/lib/active_record/observer.rb +3 -7
- data/lib/active_record/persistence.rb +58 -38
- data/lib/active_record/query_cache.rb +25 -3
- data/lib/active_record/railtie.rb +21 -12
- data/lib/active_record/railties/console_sandbox.rb +6 -0
- data/lib/active_record/railties/databases.rake +147 -116
- data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
- data/lib/active_record/reflection.rb +176 -44
- data/lib/active_record/relation.rb +125 -49
- data/lib/active_record/relation/batches.rb +7 -5
- data/lib/active_record/relation/calculations.rb +50 -18
- data/lib/active_record/relation/finder_methods.rb +47 -26
- data/lib/active_record/relation/predicate_builder.rb +24 -21
- data/lib/active_record/relation/query_methods.rb +117 -101
- data/lib/active_record/relation/spawn_methods.rb +27 -20
- data/lib/active_record/result.rb +34 -0
- data/lib/active_record/schema.rb +5 -6
- data/lib/active_record/schema_dumper.rb +11 -13
- data/lib/active_record/serialization.rb +2 -2
- data/lib/active_record/serializers/xml_serializer.rb +10 -10
- data/lib/active_record/session_store.rb +8 -2
- data/lib/active_record/test_case.rb +9 -20
- data/lib/active_record/timestamp.rb +21 -9
- data/lib/active_record/transactions.rb +16 -15
- data/lib/active_record/validations.rb +21 -22
- data/lib/active_record/validations/associated.rb +3 -1
- data/lib/active_record/validations/uniqueness.rb +48 -58
- data/lib/active_record/version.rb +3 -3
- data/lib/rails/generators/active_record.rb +6 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +10 -2
- data/lib/rails/generators/active_record/model/model_generator.rb +2 -1
- data/lib/rails/generators/active_record/model/templates/migration.rb +6 -5
- data/lib/rails/generators/active_record/model/templates/model.rb +2 -0
- data/lib/rails/generators/active_record/model/templates/module.rb +2 -0
- data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +2 -1
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +2 -2
- metadata +106 -77
- checksums.yaml +0 -7
- data/lib/active_record/association_preload.rb +0 -431
- data/lib/active_record/associations/association_collection.rb +0 -572
- data/lib/active_record/associations/association_proxy.rb +0 -304
- 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.
|
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
|
-
|
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
|
-
|
82
|
-
|
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
|
-
|
96
|
+
stmt = relation.where(
|
91
97
|
relation.table[self.class.primary_key].eq(quoted_id).and(
|
92
|
-
relation.table[
|
98
|
+
relation.table[lock_col].eq(quote_value(previous_lock_value))
|
93
99
|
)
|
94
|
-
).arel.
|
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 + '=',
|
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
|
-
|
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
|
-
|
118
|
-
predicate =
|
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).
|
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)
|
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
|
-
|
26
|
-
|
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
|
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
|
45
|
+
# def up
|
49
46
|
# add_column :accounts, :ssl_enabled, :boolean, :default => 1
|
50
47
|
# end
|
51
48
|
#
|
52
|
-
# def
|
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
|
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
|
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>
|
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
|
149
|
+
# def up
|
153
150
|
# add_column :tablenames, :fieldname, :string
|
154
151
|
# end
|
155
152
|
#
|
156
|
-
# def
|
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
|
181
|
+
# def up
|
185
182
|
# Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
|
186
183
|
# end
|
187
184
|
#
|
188
|
-
# def
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
293
|
-
cattr_accessor :verbose
|
321
|
+
autoload :CommandRecorder, 'active_record/migration/command_recorder'
|
294
322
|
|
295
323
|
class << self
|
296
|
-
|
297
|
-
|
298
|
-
end
|
299
|
-
|
300
|
-
def down_with_benchmarks #:nodoc:
|
301
|
-
migrate(:down)
|
302
|
-
end
|
324
|
+
attr_accessor :delegate # :nodoc:
|
325
|
+
end
|
303
326
|
|
304
|
-
|
305
|
-
|
306
|
-
|
327
|
+
def self.method_missing(name, *args, &block) # :nodoc:
|
328
|
+
(delegate || superclass.delegate).send(name, *args, &block)
|
329
|
+
end
|
307
330
|
|
308
|
-
|
309
|
-
when :up then announce "migrating"
|
310
|
-
when :down then announce "reverting"
|
311
|
-
end
|
331
|
+
cattr_accessor :verbose
|
312
332
|
|
313
|
-
|
314
|
-
time = Benchmark.measure { result = send("#{direction}_without_benchmarks") }
|
333
|
+
attr_accessor :name, :version
|
315
334
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
335
|
+
def initialize
|
336
|
+
@name = self.class.name
|
337
|
+
@version = nil
|
338
|
+
@connection = nil
|
339
|
+
end
|
320
340
|
|
321
|
-
|
322
|
-
|
341
|
+
# instantiate the delegate object after initialize is defined
|
342
|
+
self.verbose = true
|
343
|
+
self.delegate = new
|
323
344
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
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
|
-
|
331
|
-
|
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
|
-
|
334
|
-
|
335
|
-
|
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
|
-
|
338
|
-
|
385
|
+
else
|
386
|
+
time = Benchmark.measure { send(direction) }
|
339
387
|
end
|
388
|
+
@connection = nil
|
340
389
|
end
|
341
390
|
|
342
|
-
|
343
|
-
|
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
|
-
|
347
|
-
|
397
|
+
def write(text="")
|
398
|
+
puts(text) if verbose
|
399
|
+
end
|
348
400
|
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
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
|
-
|
355
|
-
|
356
|
-
|
407
|
+
def say(message, subitem=false)
|
408
|
+
write "#{subitem ? " ->" : "--"} #{message}"
|
409
|
+
end
|
357
410
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
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
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
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
|
-
|
375
|
-
|
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
|
-
|
379
|
-
|
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
|
-
|
382
|
-
|
383
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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(
|
526
|
+
down(migrations_paths, target_version)
|
421
527
|
else
|
422
|
-
up(
|
528
|
+
up(migrations_paths, target_version)
|
423
529
|
end
|
424
530
|
end
|
425
531
|
|
426
|
-
def rollback(
|
427
|
-
move(:down,
|
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
|
435
|
-
|
536
|
+
def forward(migrations_paths, steps=1)
|
537
|
+
move(:up, migrations_paths, steps)
|
436
538
|
end
|
437
539
|
|
438
|
-
def
|
439
|
-
self.new(:
|
540
|
+
def up(migrations_paths, target_version = nil)
|
541
|
+
self.new(:up, migrations_paths, target_version).migrate
|
440
542
|
end
|
441
543
|
|
442
|
-
def
|
443
|
-
self.new(
|
544
|
+
def down(migrations_paths, target_version = nil)
|
545
|
+
self.new(:down, migrations_paths, target_version).migrate
|
444
546
|
end
|
445
547
|
|
446
|
-
def
|
447
|
-
|
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,
|
476
|
-
migrator = self.new(direction,
|
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,
|
619
|
+
send(direction, migrations_paths, version)
|
483
620
|
end
|
484
621
|
end
|
485
622
|
end
|
486
623
|
|
487
|
-
def initialize(direction,
|
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, @
|
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? &&
|
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? &&
|
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? &&
|
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? && !
|
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
|
-
|
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)).
|
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.
|
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
|
|