activerecord 3.0.0 → 4.0.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2102 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +35 -44
- data/examples/performance.rb +110 -100
- data/lib/active_record/aggregations.rb +59 -75
- data/lib/active_record/associations/alias_tracker.rb +76 -0
- data/lib/active_record/associations/association.rb +248 -0
- data/lib/active_record/associations/association_scope.rb +135 -0
- data/lib/active_record/associations/belongs_to_association.rb +60 -59
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +16 -59
- data/lib/active_record/associations/builder/association.rb +108 -0
- data/lib/active_record/associations/builder/belongs_to.rb +98 -0
- data/lib/active_record/associations/builder/collection_association.rb +89 -0
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +39 -0
- data/lib/active_record/associations/builder/has_many.rb +15 -0
- data/lib/active_record/associations/builder/has_one.rb +25 -0
- data/lib/active_record/associations/builder/singular_association.rb +32 -0
- data/lib/active_record/associations/collection_association.rb +608 -0
- data/lib/active_record/associations/collection_proxy.rb +986 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +40 -112
- data/lib/active_record/associations/has_many_association.rb +83 -76
- data/lib/active_record/associations/has_many_through_association.rb +147 -66
- data/lib/active_record/associations/has_one_association.rb +67 -108
- data/lib/active_record/associations/has_one_through_association.rb +21 -25
- data/lib/active_record/associations/join_dependency/join_association.rb +174 -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_dependency.rb +235 -0
- data/lib/active_record/associations/join_helper.rb +45 -0
- data/lib/active_record/associations/preloader/association.rb +121 -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 +19 -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 +63 -0
- data/lib/active_record/associations/preloader.rb +178 -0
- data/lib/active_record/associations/singular_association.rb +64 -0
- data/lib/active_record/associations/through_association.rb +87 -0
- data/lib/active_record/associations.rb +512 -1224
- data/lib/active_record/attribute_assignment.rb +201 -0
- data/lib/active_record/attribute_methods/before_type_cast.rb +49 -12
- data/lib/active_record/attribute_methods/dirty.rb +51 -28
- data/lib/active_record/attribute_methods/primary_key.rb +94 -22
- data/lib/active_record/attribute_methods/query.rb +5 -4
- data/lib/active_record/attribute_methods/read.rb +63 -72
- data/lib/active_record/attribute_methods/serialization.rb +162 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +39 -41
- data/lib/active_record/attribute_methods/write.rb +39 -13
- data/lib/active_record/attribute_methods.rb +362 -29
- data/lib/active_record/autosave_association.rb +132 -75
- data/lib/active_record/base.rb +83 -1627
- data/lib/active_record/callbacks.rb +69 -47
- data/lib/active_record/coders/yaml_column.rb +38 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +411 -138
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +21 -11
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +234 -173
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -22
- data/lib/active_record/connection_adapters/abstract/quoting.rb +82 -25
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +176 -414
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +70 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +562 -232
- data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +281 -53
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +782 -0
- data/lib/active_record/connection_adapters/column.rb +318 -0
- data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +273 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +365 -450
- data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
- data/lib/active_record/connection_adapters/postgresql/cast.rb +152 -0
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
- data/lib/active_record/connection_adapters/postgresql/oid.rb +366 -0
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +171 -0
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +489 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +672 -752
- data/lib/active_record/connection_adapters/schema_cache.rb +129 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +588 -17
- data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
- data/lib/active_record/connection_handling.rb +98 -0
- data/lib/active_record/core.rb +463 -0
- data/lib/active_record/counter_cache.rb +108 -101
- data/lib/active_record/dynamic_matchers.rb +131 -0
- data/lib/active_record/errors.rb +54 -13
- data/lib/active_record/explain.rb +38 -0
- data/lib/active_record/explain_registry.rb +30 -0
- data/lib/active_record/explain_subscriber.rb +29 -0
- data/lib/active_record/fixture_set/file.rb +55 -0
- data/lib/active_record/fixtures.rb +703 -785
- data/lib/active_record/inheritance.rb +200 -0
- data/lib/active_record/integration.rb +60 -0
- data/lib/active_record/locale/en.yml +8 -1
- data/lib/active_record/locking/optimistic.rb +69 -60
- data/lib/active_record/locking/pessimistic.rb +34 -12
- data/lib/active_record/log_subscriber.rb +40 -6
- data/lib/active_record/migration/command_recorder.rb +164 -0
- data/lib/active_record/migration/join_table.rb +15 -0
- data/lib/active_record/migration.rb +614 -216
- data/lib/active_record/model_schema.rb +345 -0
- data/lib/active_record/nested_attributes.rb +248 -119
- data/lib/active_record/null_relation.rb +65 -0
- data/lib/active_record/persistence.rb +275 -57
- data/lib/active_record/query_cache.rb +29 -9
- data/lib/active_record/querying.rb +62 -0
- data/lib/active_record/railtie.rb +135 -21
- data/lib/active_record/railties/console_sandbox.rb +5 -0
- data/lib/active_record/railties/controller_runtime.rb +17 -5
- data/lib/active_record/railties/databases.rake +249 -359
- data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
- data/lib/active_record/readonly_attributes.rb +30 -0
- data/lib/active_record/reflection.rb +283 -103
- data/lib/active_record/relation/batches.rb +38 -34
- data/lib/active_record/relation/calculations.rb +252 -139
- data/lib/active_record/relation/delegation.rb +125 -0
- data/lib/active_record/relation/finder_methods.rb +182 -188
- data/lib/active_record/relation/merger.rb +161 -0
- data/lib/active_record/relation/predicate_builder.rb +86 -21
- data/lib/active_record/relation/query_methods.rb +917 -134
- data/lib/active_record/relation/spawn_methods.rb +53 -92
- data/lib/active_record/relation.rb +405 -143
- data/lib/active_record/result.rb +67 -0
- data/lib/active_record/runtime_registry.rb +17 -0
- data/lib/active_record/sanitization.rb +168 -0
- data/lib/active_record/schema.rb +20 -14
- data/lib/active_record/schema_dumper.rb +55 -46
- data/lib/active_record/schema_migration.rb +39 -0
- data/lib/active_record/scoping/default.rb +146 -0
- data/lib/active_record/scoping/named.rb +175 -0
- data/lib/active_record/scoping.rb +82 -0
- data/lib/active_record/serialization.rb +8 -46
- data/lib/active_record/serializers/xml_serializer.rb +21 -68
- data/lib/active_record/statement_cache.rb +26 -0
- data/lib/active_record/store.rb +156 -0
- data/lib/active_record/tasks/database_tasks.rb +203 -0
- data/lib/active_record/tasks/firebird_database_tasks.rb +56 -0
- data/lib/active_record/tasks/mysql_database_tasks.rb +143 -0
- data/lib/active_record/tasks/oracle_database_tasks.rb +45 -0
- data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
- data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
- data/lib/active_record/tasks/sqlserver_database_tasks.rb +48 -0
- data/lib/active_record/test_case.rb +57 -28
- data/lib/active_record/timestamp.rb +49 -18
- data/lib/active_record/transactions.rb +106 -63
- data/lib/active_record/translation.rb +22 -0
- data/lib/active_record/validations/associated.rb +25 -24
- data/lib/active_record/validations/presence.rb +65 -0
- data/lib/active_record/validations/uniqueness.rb +123 -83
- data/lib/active_record/validations.rb +29 -29
- data/lib/active_record/version.rb +7 -5
- data/lib/active_record.rb +83 -34
- data/lib/rails/generators/active_record/migration/migration_generator.rb +46 -9
- data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +30 -8
- data/lib/rails/generators/active_record/model/model_generator.rb +15 -5
- data/lib/rails/generators/active_record/model/templates/model.rb +7 -2
- data/lib/rails/generators/active_record/model/templates/module.rb +3 -1
- data/lib/rails/generators/active_record.rb +4 -8
- metadata +163 -121
- data/CHANGELOG +0 -6023
- data/examples/associations.png +0 -0
- data/lib/active_record/association_preload.rb +0 -403
- data/lib/active_record/associations/association_collection.rb +0 -562
- data/lib/active_record/associations/association_proxy.rb +0 -295
- data/lib/active_record/associations/through_association_scope.rb +0 -154
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -113
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -401
- data/lib/active_record/dynamic_finder_match.rb +0 -53
- data/lib/active_record/dynamic_scope_match.rb +0 -32
- data/lib/active_record/named_scope.rb +0 -138
- data/lib/active_record/observer.rb +0 -140
- data/lib/active_record/session_store.rb +0 -340
- data/lib/rails/generators/active_record/model/templates/migration.rb +0 -16
- data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
- data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -2
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -24
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -16
@@ -1,8 +1,15 @@
|
|
1
|
+
require 'active_support/test_case'
|
2
|
+
|
3
|
+
ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase')
|
1
4
|
module ActiveRecord
|
2
5
|
# = Active Record Test Case
|
3
6
|
#
|
4
7
|
# Defines some test assertions to test against SQL queries.
|
5
8
|
class TestCase < ActiveSupport::TestCase #:nodoc:
|
9
|
+
def teardown
|
10
|
+
SQLCounter.clear_log
|
11
|
+
end
|
12
|
+
|
6
13
|
def assert_date_from_db(expected, actual, message = nil)
|
7
14
|
# SybaseAdapter doesn't have a separate column type just for dates,
|
8
15
|
# so the time is in the string and incorrectly formatted
|
@@ -14,54 +21,76 @@ module ActiveRecord
|
|
14
21
|
end
|
15
22
|
|
16
23
|
def assert_sql(*patterns_to_match)
|
17
|
-
|
24
|
+
SQLCounter.clear_log
|
18
25
|
yield
|
26
|
+
SQLCounter.log_all
|
19
27
|
ensure
|
20
28
|
failed_patterns = []
|
21
29
|
patterns_to_match.each do |pattern|
|
22
|
-
failed_patterns << pattern unless
|
30
|
+
failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
|
23
31
|
end
|
24
|
-
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{
|
32
|
+
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
|
25
33
|
end
|
26
34
|
|
27
|
-
def assert_queries(num = 1)
|
28
|
-
|
35
|
+
def assert_queries(num = 1, options = {})
|
36
|
+
ignore_none = options.fetch(:ignore_none) { num == :any }
|
37
|
+
SQLCounter.clear_log
|
29
38
|
yield
|
30
39
|
ensure
|
31
|
-
|
32
|
-
|
40
|
+
the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
|
41
|
+
if num == :any
|
42
|
+
assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
|
43
|
+
else
|
44
|
+
mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
|
45
|
+
assert_equal num, the_log.size, mesg
|
46
|
+
end
|
33
47
|
end
|
34
48
|
|
35
49
|
def assert_no_queries(&block)
|
36
|
-
assert_queries(0, &block)
|
50
|
+
assert_queries(0, :ignore_none => true, &block)
|
37
51
|
end
|
38
52
|
|
39
|
-
|
40
|
-
|
41
|
-
|
53
|
+
end
|
54
|
+
|
55
|
+
class SQLCounter
|
56
|
+
class << self
|
57
|
+
attr_accessor :ignored_sql, :log, :log_all
|
58
|
+
def clear_log; self.log = []; self.log_all = []; end
|
42
59
|
end
|
43
60
|
|
44
|
-
|
45
|
-
|
46
|
-
|
61
|
+
self.clear_log
|
62
|
+
|
63
|
+
self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
|
64
|
+
|
65
|
+
# FIXME: this needs to be refactored so specific database can add their own
|
66
|
+
# ignored SQL, or better yet, use a different notification for the queries
|
67
|
+
# instead examining the SQL content.
|
68
|
+
oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
|
69
|
+
mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/]
|
70
|
+
postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
|
71
|
+
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
|
72
|
+
|
73
|
+
[oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
|
74
|
+
ignored_sql.concat db_ignored_sql
|
47
75
|
end
|
48
76
|
|
49
|
-
|
50
|
-
|
51
|
-
|
77
|
+
attr_reader :ignore
|
78
|
+
|
79
|
+
def initialize(ignore = Regexp.union(self.class.ignored_sql))
|
80
|
+
@ignore = ignore
|
52
81
|
end
|
53
82
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
yield
|
64
|
-
end
|
83
|
+
def call(name, start, finish, message_id, values)
|
84
|
+
sql = values[:sql]
|
85
|
+
|
86
|
+
# FIXME: this seems bad. we should probably have a better way to indicate
|
87
|
+
# the query was cached
|
88
|
+
return if 'CACHE' == values[:name]
|
89
|
+
|
90
|
+
self.class.log_all << sql
|
91
|
+
self.class.log << sql unless ignore =~ sql
|
65
92
|
end
|
66
93
|
end
|
94
|
+
|
95
|
+
ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
|
67
96
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
|
1
2
|
module ActiveRecord
|
2
3
|
# = Active Record Timestamp
|
3
4
|
#
|
@@ -7,47 +8,56 @@ module ActiveRecord
|
|
7
8
|
#
|
8
9
|
# Timestamping can be turned off by setting:
|
9
10
|
#
|
10
|
-
#
|
11
|
+
# config.active_record.record_timestamps = false
|
11
12
|
#
|
12
13
|
# Timestamps are in the local timezone by default but you can use UTC by setting:
|
13
14
|
#
|
14
|
-
#
|
15
|
+
# config.active_record.default_timezone = :utc
|
15
16
|
#
|
16
17
|
# == Time Zone aware attributes
|
17
18
|
#
|
18
19
|
# By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code.
|
19
20
|
#
|
20
|
-
#
|
21
|
+
# config.active_record.time_zone_aware_attributes = true
|
21
22
|
#
|
22
23
|
# This feature can easily be turned off by assigning value <tt>false</tt> .
|
23
24
|
#
|
24
|
-
# If your attributes are time zone aware and you desire to skip time zone conversion
|
25
|
-
# attributes then you can do following:
|
25
|
+
# If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone
|
26
|
+
# when reading certain attributes then you can do following:
|
26
27
|
#
|
27
|
-
# Topic
|
28
|
+
# class Topic < ActiveRecord::Base
|
29
|
+
# self.skip_time_zone_conversion_for_attributes = [:written_on]
|
30
|
+
# end
|
28
31
|
module Timestamp
|
29
32
|
extend ActiveSupport::Concern
|
30
33
|
|
31
34
|
included do
|
32
|
-
|
35
|
+
class_attribute :record_timestamps
|
33
36
|
self.record_timestamps = true
|
34
37
|
end
|
35
38
|
|
39
|
+
def initialize_dup(other) # :nodoc:
|
40
|
+
clear_timestamp_attributes
|
41
|
+
super
|
42
|
+
end
|
43
|
+
|
36
44
|
private
|
37
45
|
|
38
|
-
def
|
39
|
-
if record_timestamps
|
46
|
+
def create_record
|
47
|
+
if self.record_timestamps
|
40
48
|
current_time = current_time_from_proper_timezone
|
41
49
|
|
42
50
|
all_timestamp_attributes.each do |column|
|
43
|
-
|
51
|
+
if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
|
52
|
+
write_attribute(column.to_s, current_time)
|
53
|
+
end
|
44
54
|
end
|
45
55
|
end
|
46
56
|
|
47
57
|
super
|
48
58
|
end
|
49
59
|
|
50
|
-
def
|
60
|
+
def update_record(*args)
|
51
61
|
if should_record_timestamps?
|
52
62
|
current_time = current_time_from_proper_timezone
|
53
63
|
|
@@ -61,28 +71,49 @@ module ActiveRecord
|
|
61
71
|
end
|
62
72
|
|
63
73
|
def should_record_timestamps?
|
64
|
-
record_timestamps && (!
|
74
|
+
self.record_timestamps && (!partial_writes? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
|
75
|
+
end
|
76
|
+
|
77
|
+
def timestamp_attributes_for_create_in_model
|
78
|
+
timestamp_attributes_for_create.select { |c| self.class.column_names.include?(c.to_s) }
|
65
79
|
end
|
66
80
|
|
67
81
|
def timestamp_attributes_for_update_in_model
|
68
|
-
timestamp_attributes_for_update.select { |c|
|
82
|
+
timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def all_timestamp_attributes_in_model
|
86
|
+
timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model
|
69
87
|
end
|
70
88
|
|
71
|
-
def timestamp_attributes_for_update
|
89
|
+
def timestamp_attributes_for_update
|
72
90
|
[:updated_at, :updated_on]
|
73
91
|
end
|
74
92
|
|
75
|
-
def timestamp_attributes_for_create
|
93
|
+
def timestamp_attributes_for_create
|
76
94
|
[:created_at, :created_on]
|
77
95
|
end
|
78
96
|
|
79
|
-
def all_timestamp_attributes
|
97
|
+
def all_timestamp_attributes
|
80
98
|
timestamp_attributes_for_create + timestamp_attributes_for_update
|
81
99
|
end
|
82
100
|
|
83
|
-
def
|
101
|
+
def max_updated_column_timestamp
|
102
|
+
if (timestamps = timestamp_attributes_for_update.map { |attr| self[attr] }.compact).present?
|
103
|
+
timestamps.map { |ts| ts.to_time }.max
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def current_time_from_proper_timezone
|
84
108
|
self.class.default_timezone == :utc ? Time.now.utc : Time.now
|
85
109
|
end
|
110
|
+
|
111
|
+
# Clear attributes and changed_attributes
|
112
|
+
def clear_timestamp_attributes
|
113
|
+
all_timestamp_attributes_in_model.each do |attribute_name|
|
114
|
+
self[attribute_name] = nil
|
115
|
+
changed_attributes.delete(attribute_name)
|
116
|
+
end
|
117
|
+
end
|
86
118
|
end
|
87
119
|
end
|
88
|
-
|
@@ -4,6 +4,7 @@ module ActiveRecord
|
|
4
4
|
# See ActiveRecord::Transactions::ClassMethods for documentation.
|
5
5
|
module Transactions
|
6
6
|
extend ActiveSupport::Concern
|
7
|
+
ACTIONS = [:create, :destroy, :update]
|
7
8
|
|
8
9
|
class TransactionError < ActiveRecordError # :nodoc:
|
9
10
|
end
|
@@ -11,6 +12,7 @@ module ActiveRecord
|
|
11
12
|
included do
|
12
13
|
define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
|
13
14
|
end
|
15
|
+
|
14
16
|
# = Active Record Transactions
|
15
17
|
#
|
16
18
|
# Transactions are protective blocks where SQL statements are only permanent
|
@@ -107,10 +109,10 @@ module ActiveRecord
|
|
107
109
|
#
|
108
110
|
# # Suppose that we have a Number model with a unique column called 'i'.
|
109
111
|
# Number.transaction do
|
110
|
-
# Number.create(:
|
112
|
+
# Number.create(i: 0)
|
111
113
|
# begin
|
112
114
|
# # This will raise a unique constraint error...
|
113
|
-
# Number.create(:
|
115
|
+
# Number.create(i: 0)
|
114
116
|
# rescue ActiveRecord::StatementInvalid
|
115
117
|
# # ...which we ignore.
|
116
118
|
# end
|
@@ -118,7 +120,7 @@ module ActiveRecord
|
|
118
120
|
# # On PostgreSQL, the transaction is now unusable. The following
|
119
121
|
# # statement will cause a PostgreSQL error, even though the unique
|
120
122
|
# # constraint is no longer violated:
|
121
|
-
# Number.create(:
|
123
|
+
# Number.create(i: 1)
|
122
124
|
# # => "PGError: ERROR: current transaction is aborted, commands
|
123
125
|
# # ignored until end of transaction block"
|
124
126
|
# end
|
@@ -130,38 +132,41 @@ module ActiveRecord
|
|
130
132
|
#
|
131
133
|
# +transaction+ calls can be nested. By default, this makes all database
|
132
134
|
# statements in the nested transaction block become part of the parent
|
133
|
-
# transaction. For example:
|
135
|
+
# transaction. For example, the following behavior may be surprising:
|
134
136
|
#
|
135
137
|
# User.transaction do
|
136
|
-
# User.create(:
|
138
|
+
# User.create(username: 'Kotori')
|
137
139
|
# User.transaction do
|
138
|
-
# User.create(:
|
140
|
+
# User.create(username: 'Nemu')
|
139
141
|
# raise ActiveRecord::Rollback
|
140
142
|
# end
|
141
143
|
# end
|
142
144
|
#
|
143
|
-
#
|
145
|
+
# creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt>
|
146
|
+
# exception in the nested block does not issue a ROLLBACK. Since these exceptions
|
147
|
+
# are captured in transaction blocks, the parent block does not see it and the
|
148
|
+
# real transaction is committed.
|
144
149
|
#
|
145
|
-
#
|
146
|
-
# <tt
|
147
|
-
# database rolls back to the beginning of the sub-transaction
|
148
|
-
#
|
150
|
+
# In order to get a ROLLBACK for the nested transaction you may ask for a real
|
151
|
+
# sub-transaction by passing <tt>requires_new: true</tt>. If anything goes wrong,
|
152
|
+
# the database rolls back to the beginning of the sub-transaction without rolling
|
153
|
+
# back the parent transaction. If we add it to the previous example:
|
149
154
|
#
|
150
155
|
# User.transaction do
|
151
|
-
# User.create(:
|
152
|
-
# User.transaction(:
|
153
|
-
# User.create(:
|
156
|
+
# User.create(username: 'Kotori')
|
157
|
+
# User.transaction(requires_new: true) do
|
158
|
+
# User.create(username: 'Nemu')
|
154
159
|
# raise ActiveRecord::Rollback
|
155
160
|
# end
|
156
161
|
# end
|
157
162
|
#
|
158
|
-
#
|
163
|
+
# only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it.
|
159
164
|
#
|
160
165
|
# Most databases don't support true nested transactions. At the time of
|
161
166
|
# writing, the only database that we're aware of that supports true nested
|
162
167
|
# transactions, is MS-SQL. Because of this, Active Record emulates nested
|
163
|
-
# transactions by using savepoints. See
|
164
|
-
# http://dev.mysql.com/doc/refman/5.
|
168
|
+
# transactions by using savepoints on MySQL and PostgreSQL. See
|
169
|
+
# http://dev.mysql.com/doc/refman/5.6/en/savepoint.html
|
165
170
|
# for more information about savepoints.
|
166
171
|
#
|
167
172
|
# === Callbacks
|
@@ -190,7 +195,7 @@ module ActiveRecord
|
|
190
195
|
# automatically released. The following example demonstrates the problem:
|
191
196
|
#
|
192
197
|
# Model.connection.transaction do # BEGIN
|
193
|
-
# Model.connection.transaction(:
|
198
|
+
# Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1
|
194
199
|
# Model.connection.create_table(...) # active_record_1 now automatically released
|
195
200
|
# end # RELEASE savepoint active_record_1
|
196
201
|
# # ^^^^ BOOM! database error!
|
@@ -204,28 +209,56 @@ module ActiveRecord
|
|
204
209
|
connection.transaction(options, &block)
|
205
210
|
end
|
206
211
|
|
212
|
+
# This callback is called after a record has been created, updated, or destroyed.
|
213
|
+
#
|
214
|
+
# You can specify that the callback should only be fired by a certain action with
|
215
|
+
# the +:on+ option:
|
216
|
+
#
|
217
|
+
# after_commit :do_foo, on: :create
|
218
|
+
# after_commit :do_bar, on: :update
|
219
|
+
# after_commit :do_baz, on: :destroy
|
220
|
+
#
|
221
|
+
# after_commit :do_foo_bar, :on [:create, :update]
|
222
|
+
# after_commit :do_bar_baz, :on [:update, :destroy]
|
223
|
+
#
|
224
|
+
# Note that transactional fixtures do not play well with this feature. Please
|
225
|
+
# use the +test_after_commit+ gem to have these hooks fired in tests.
|
207
226
|
def after_commit(*args, &block)
|
208
|
-
|
209
|
-
if options.is_a?(Hash) && options[:on]
|
210
|
-
options[:if] = Array.wrap(options[:if])
|
211
|
-
options[:if] << "transaction_include_action?(:#{options[:on]})"
|
212
|
-
end
|
227
|
+
set_options_for_callbacks!(args)
|
213
228
|
set_callback(:commit, :after, *args, &block)
|
214
229
|
end
|
215
230
|
|
231
|
+
# This callback is called after a create, update, or destroy are rolled back.
|
232
|
+
#
|
233
|
+
# Please check the documentation of +after_commit+ for options.
|
216
234
|
def after_rollback(*args, &block)
|
235
|
+
set_options_for_callbacks!(args)
|
236
|
+
set_callback(:rollback, :after, *args, &block)
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
def set_options_for_callbacks!(args)
|
217
242
|
options = args.last
|
218
243
|
if options.is_a?(Hash) && options[:on]
|
219
|
-
|
220
|
-
options[:if]
|
244
|
+
assert_valid_transaction_action(options[:on])
|
245
|
+
options[:if] = Array(options[:if])
|
246
|
+
fire_on = Array(options[:on]).map(&:to_sym)
|
247
|
+
options[:if] << "transaction_include_any_action?(#{fire_on})"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def assert_valid_transaction_action(actions)
|
252
|
+
actions = Array(actions)
|
253
|
+
if (actions - ACTIONS).any?
|
254
|
+
raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}"
|
221
255
|
end
|
222
|
-
set_callback(:rollback, :after, *args, &block)
|
223
256
|
end
|
224
257
|
end
|
225
258
|
|
226
259
|
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
|
227
|
-
def transaction(&block)
|
228
|
-
self.class.transaction(&block)
|
260
|
+
def transaction(options = {}, &block)
|
261
|
+
self.class.transaction(options, &block)
|
229
262
|
end
|
230
263
|
|
231
264
|
def destroy #:nodoc:
|
@@ -254,8 +287,11 @@ module ActiveRecord
|
|
254
287
|
end
|
255
288
|
|
256
289
|
# Call the after_commit callbacks
|
290
|
+
#
|
291
|
+
# Ensure that it is not called if the object was never persisted (failed create),
|
292
|
+
# but call it after the commit of a destroyed object
|
257
293
|
def committed! #:nodoc:
|
258
|
-
|
294
|
+
run_callbacks :commit if destroyed? || persisted?
|
259
295
|
ensure
|
260
296
|
clear_transaction_record_state
|
261
297
|
end
|
@@ -263,7 +299,7 @@ module ActiveRecord
|
|
263
299
|
# Call the after rollback callbacks. The restore_state argument indicates if the record
|
264
300
|
# state should be rolled back to the beginning or just to the last savepoint.
|
265
301
|
def rolledback!(force_restore_state = false) #:nodoc:
|
266
|
-
|
302
|
+
run_callbacks :rollback
|
267
303
|
ensure
|
268
304
|
restore_transaction_record_state(force_restore_state)
|
269
305
|
end
|
@@ -286,7 +322,13 @@ module ActiveRecord
|
|
286
322
|
status = nil
|
287
323
|
self.class.transaction do
|
288
324
|
add_to_transaction
|
289
|
-
|
325
|
+
begin
|
326
|
+
status = yield
|
327
|
+
rescue ActiveRecord::Rollback
|
328
|
+
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
|
329
|
+
status = nil
|
330
|
+
end
|
331
|
+
|
290
332
|
raise ActiveRecord::Rollback unless status
|
291
333
|
end
|
292
334
|
status
|
@@ -295,61 +337,62 @@ module ActiveRecord
|
|
295
337
|
protected
|
296
338
|
|
297
339
|
# Save the new record state and id of a record so it can be restored later if a transaction fails.
|
298
|
-
def remember_transaction_record_state #:nodoc
|
299
|
-
@_start_transaction_state
|
340
|
+
def remember_transaction_record_state #:nodoc:
|
341
|
+
@_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
|
300
342
|
unless @_start_transaction_state.include?(:new_record)
|
301
|
-
@_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
|
302
343
|
@_start_transaction_state[:new_record] = @new_record
|
303
344
|
end
|
304
345
|
unless @_start_transaction_state.include?(:destroyed)
|
305
346
|
@_start_transaction_state[:destroyed] = @destroyed
|
306
347
|
end
|
307
348
|
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
|
349
|
+
@_start_transaction_state[:frozen?] = @attributes.frozen?
|
308
350
|
end
|
309
351
|
|
310
352
|
# Clear the new record state and id of a record.
|
311
|
-
def clear_transaction_record_state #:nodoc
|
312
|
-
|
313
|
-
|
314
|
-
remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1
|
315
|
-
end
|
353
|
+
def clear_transaction_record_state #:nodoc:
|
354
|
+
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
|
355
|
+
@_start_transaction_state.clear if @_start_transaction_state[:level] < 1
|
316
356
|
end
|
317
357
|
|
318
358
|
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
|
319
|
-
def restore_transaction_record_state(force = false) #:nodoc
|
320
|
-
|
359
|
+
def restore_transaction_record_state(force = false) #:nodoc:
|
360
|
+
unless @_start_transaction_state.empty?
|
321
361
|
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
|
322
|
-
if @_start_transaction_state[:level] < 1
|
323
|
-
restore_state =
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
end
|
362
|
+
if @_start_transaction_state[:level] < 1 || force
|
363
|
+
restore_state = @_start_transaction_state
|
364
|
+
was_frozen = restore_state[:frozen?]
|
365
|
+
@attributes = @attributes.dup if @attributes.frozen?
|
366
|
+
@new_record = restore_state[:new_record]
|
367
|
+
@destroyed = restore_state[:destroyed]
|
368
|
+
if restore_state.has_key?(:id)
|
369
|
+
self.id = restore_state[:id]
|
370
|
+
else
|
371
|
+
@attributes.delete(self.class.primary_key)
|
372
|
+
@attributes_cache.delete(self.class.primary_key)
|
334
373
|
end
|
374
|
+
@attributes.freeze if was_frozen
|
375
|
+
@_start_transaction_state.clear
|
335
376
|
end
|
336
377
|
end
|
337
378
|
end
|
338
379
|
|
339
380
|
# Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
|
340
|
-
def transaction_record_state(state) #:nodoc
|
341
|
-
@_start_transaction_state[state]
|
381
|
+
def transaction_record_state(state) #:nodoc:
|
382
|
+
@_start_transaction_state[state]
|
342
383
|
end
|
343
384
|
|
344
385
|
# Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
|
345
|
-
def
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
386
|
+
def transaction_include_any_action?(actions) #:nodoc:
|
387
|
+
actions.any? do |action|
|
388
|
+
case action
|
389
|
+
when :create
|
390
|
+
transaction_record_state(:new_record)
|
391
|
+
when :destroy
|
392
|
+
destroyed?
|
393
|
+
when :update
|
394
|
+
!(transaction_record_state(:new_record) || destroyed?)
|
395
|
+
end
|
353
396
|
end
|
354
397
|
end
|
355
398
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Translation
|
3
|
+
include ActiveModel::Translation
|
4
|
+
|
5
|
+
# Set the lookup ancestors for ActiveModel.
|
6
|
+
def lookup_ancestors #:nodoc:
|
7
|
+
klass = self
|
8
|
+
classes = [klass]
|
9
|
+
return classes if klass == ActiveRecord::Base
|
10
|
+
|
11
|
+
while klass != klass.base_class
|
12
|
+
classes << klass = klass.superclass
|
13
|
+
end
|
14
|
+
classes
|
15
|
+
end
|
16
|
+
|
17
|
+
# Set the i18n scope to overwrite ActiveModel.
|
18
|
+
def i18n_scope #:nodoc:
|
19
|
+
:activerecord
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -1,14 +1,16 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module Validations
|
3
|
-
class AssociatedValidator < ActiveModel::EachValidator
|
3
|
+
class AssociatedValidator < ActiveModel::EachValidator #:nodoc:
|
4
4
|
def validate_each(record, attribute, value)
|
5
|
-
|
6
|
-
|
5
|
+
if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
|
6
|
+
record.errors.add(attribute, :invalid, options.merge(:value => value))
|
7
|
+
end
|
7
8
|
end
|
8
9
|
end
|
9
10
|
|
10
11
|
module ClassMethods
|
11
|
-
# Validates whether the associated object or objects are all valid
|
12
|
+
# Validates whether the associated object or objects are all valid.
|
13
|
+
# Works with any kind of association.
|
12
14
|
#
|
13
15
|
# class Book < ActiveRecord::Base
|
14
16
|
# has_many :pages
|
@@ -17,29 +19,28 @@ module ActiveRecord
|
|
17
19
|
# validates_associated :pages, :library
|
18
20
|
# end
|
19
21
|
#
|
20
|
-
#
|
22
|
+
# WARNING: This validation must not be used on both ends of an association.
|
23
|
+
# Doing so will lead to a circular dependency and cause infinite recursion.
|
21
24
|
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# validates_associated :book
|
26
|
-
# end
|
27
|
-
#
|
28
|
-
# this would specify a circular dependency and cause infinite recursion.
|
29
|
-
#
|
30
|
-
# NOTE: This validation will not fail if the association hasn't been assigned. If you want to
|
31
|
-
# ensure that the association is both present and guaranteed to be valid, you also need to
|
32
|
-
# use +validates_presence_of+.
|
25
|
+
# NOTE: This validation will not fail if the association hasn't been
|
26
|
+
# assigned. If you want to ensure that the association is both present and
|
27
|
+
# guaranteed to be valid, you also need to use +validates_presence_of+.
|
33
28
|
#
|
34
29
|
# Configuration options:
|
35
|
-
#
|
36
|
-
# * <tt>:
|
37
|
-
# * <tt>:
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# * <tt>:
|
41
|
-
#
|
42
|
-
#
|
30
|
+
#
|
31
|
+
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
|
32
|
+
# * <tt>:on</tt> - Specifies when this validation is active. Runs in all
|
33
|
+
# validation contexts by default (+nil+), other options are <tt>:create</tt>
|
34
|
+
# and <tt>:update</tt>.
|
35
|
+
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
|
36
|
+
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
|
37
|
+
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
|
38
|
+
# proc or string should return or evaluate to a +true+ or +false+ value.
|
39
|
+
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
|
40
|
+
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
|
41
|
+
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
42
|
+
# method, proc or string should return or evaluate to a +true+ or +false+
|
43
|
+
# value.
|
43
44
|
def validates_associated(*attr_names)
|
44
45
|
validates_with AssociatedValidator, _merge_attributes(attr_names)
|
45
46
|
end
|