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