activerecord 3.1.12 → 3.2.22.1

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

Potentially problematic release.


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

Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +804 -338
  3. data/README.rdoc +3 -3
  4. data/examples/performance.rb +20 -1
  5. data/lib/active_record/aggregations.rb +1 -1
  6. data/lib/active_record/associations/alias_tracker.rb +3 -6
  7. data/lib/active_record/associations/association.rb +13 -45
  8. data/lib/active_record/associations/association_scope.rb +3 -15
  9. data/lib/active_record/associations/belongs_to_association.rb +1 -1
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +2 -1
  11. data/lib/active_record/associations/builder/association.rb +6 -4
  12. data/lib/active_record/associations/builder/belongs_to.rb +7 -4
  13. data/lib/active_record/associations/builder/collection_association.rb +2 -2
  14. data/lib/active_record/associations/builder/has_many.rb +4 -4
  15. data/lib/active_record/associations/builder/has_one.rb +5 -6
  16. data/lib/active_record/associations/builder/singular_association.rb +3 -16
  17. data/lib/active_record/associations/collection_association.rb +65 -32
  18. data/lib/active_record/associations/collection_proxy.rb +8 -41
  19. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -0
  20. data/lib/active_record/associations/has_many_association.rb +11 -7
  21. data/lib/active_record/associations/has_many_through_association.rb +19 -9
  22. data/lib/active_record/associations/has_one_association.rb +23 -13
  23. data/lib/active_record/associations/join_dependency/join_association.rb +6 -1
  24. data/lib/active_record/associations/join_dependency.rb +3 -3
  25. data/lib/active_record/associations/preloader/through_association.rb +3 -3
  26. data/lib/active_record/associations/preloader.rb +14 -10
  27. data/lib/active_record/associations/through_association.rb +8 -4
  28. data/lib/active_record/associations.rb +92 -76
  29. data/lib/active_record/attribute_assignment.rb +221 -0
  30. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  31. data/lib/active_record/attribute_methods/dirty.rb +21 -11
  32. data/lib/active_record/attribute_methods/primary_key.rb +62 -25
  33. data/lib/active_record/attribute_methods/read.rb +73 -83
  34. data/lib/active_record/attribute_methods/serialization.rb +120 -0
  35. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
  36. data/lib/active_record/attribute_methods/write.rb +32 -6
  37. data/lib/active_record/attribute_methods.rb +231 -30
  38. data/lib/active_record/autosave_association.rb +44 -26
  39. data/lib/active_record/base.rb +227 -1708
  40. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +150 -148
  41. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +85 -29
  42. data/lib/active_record/connection_adapters/abstract/database_statements.rb +7 -34
  43. data/lib/active_record/connection_adapters/abstract/query_cache.rb +10 -2
  44. data/lib/active_record/connection_adapters/abstract/quoting.rb +7 -4
  45. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +39 -28
  46. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +48 -19
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +77 -42
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +676 -0
  49. data/lib/active_record/connection_adapters/column.rb +37 -11
  50. data/lib/active_record/connection_adapters/mysql2_adapter.rb +133 -581
  51. data/lib/active_record/connection_adapters/mysql_adapter.rb +136 -693
  52. data/lib/active_record/connection_adapters/postgresql_adapter.rb +209 -97
  53. data/lib/active_record/connection_adapters/schema_cache.rb +69 -0
  54. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -6
  55. data/lib/active_record/connection_adapters/sqlite_adapter.rb +62 -35
  56. data/lib/active_record/counter_cache.rb +9 -4
  57. data/lib/active_record/dynamic_finder_match.rb +12 -0
  58. data/lib/active_record/dynamic_matchers.rb +84 -0
  59. data/lib/active_record/errors.rb +11 -1
  60. data/lib/active_record/explain.rb +86 -0
  61. data/lib/active_record/explain_subscriber.rb +25 -0
  62. data/lib/active_record/fixtures/file.rb +65 -0
  63. data/lib/active_record/fixtures.rb +57 -86
  64. data/lib/active_record/identity_map.rb +3 -4
  65. data/lib/active_record/inheritance.rb +174 -0
  66. data/lib/active_record/integration.rb +60 -0
  67. data/lib/active_record/locking/optimistic.rb +33 -26
  68. data/lib/active_record/locking/pessimistic.rb +23 -1
  69. data/lib/active_record/log_subscriber.rb +8 -4
  70. data/lib/active_record/migration/command_recorder.rb +8 -8
  71. data/lib/active_record/migration.rb +68 -35
  72. data/lib/active_record/model_schema.rb +368 -0
  73. data/lib/active_record/nested_attributes.rb +60 -24
  74. data/lib/active_record/persistence.rb +57 -11
  75. data/lib/active_record/query_cache.rb +6 -6
  76. data/lib/active_record/querying.rb +58 -0
  77. data/lib/active_record/railtie.rb +37 -29
  78. data/lib/active_record/railties/controller_runtime.rb +3 -1
  79. data/lib/active_record/railties/databases.rake +213 -117
  80. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  81. data/lib/active_record/readonly_attributes.rb +26 -0
  82. data/lib/active_record/reflection.rb +7 -15
  83. data/lib/active_record/relation/batches.rb +7 -4
  84. data/lib/active_record/relation/calculations.rb +55 -16
  85. data/lib/active_record/relation/delegation.rb +49 -0
  86. data/lib/active_record/relation/finder_methods.rb +16 -11
  87. data/lib/active_record/relation/predicate_builder.rb +8 -6
  88. data/lib/active_record/relation/query_methods.rb +75 -9
  89. data/lib/active_record/relation/spawn_methods.rb +48 -7
  90. data/lib/active_record/relation.rb +78 -32
  91. data/lib/active_record/result.rb +10 -4
  92. data/lib/active_record/sanitization.rb +194 -0
  93. data/lib/active_record/schema_dumper.rb +12 -5
  94. data/lib/active_record/scoping/default.rb +142 -0
  95. data/lib/active_record/scoping/named.rb +200 -0
  96. data/lib/active_record/scoping.rb +152 -0
  97. data/lib/active_record/serialization.rb +1 -43
  98. data/lib/active_record/serializers/xml_serializer.rb +4 -45
  99. data/lib/active_record/session_store.rb +18 -16
  100. data/lib/active_record/store.rb +52 -0
  101. data/lib/active_record/test_case.rb +11 -7
  102. data/lib/active_record/timestamp.rb +17 -3
  103. data/lib/active_record/transactions.rb +27 -6
  104. data/lib/active_record/translation.rb +22 -0
  105. data/lib/active_record/validations/associated.rb +5 -4
  106. data/lib/active_record/validations/uniqueness.rb +8 -8
  107. data/lib/active_record/validations.rb +1 -1
  108. data/lib/active_record/version.rb +3 -3
  109. data/lib/active_record.rb +38 -3
  110. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -1
  111. data/lib/rails/generators/active_record/migration/templates/migration.rb +12 -3
  112. data/lib/rails/generators/active_record/model/model_generator.rb +9 -1
  113. data/lib/rails/generators/active_record/model/templates/migration.rb +3 -5
  114. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  115. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
  116. metadata +49 -28
  117. data/lib/active_record/named_scope.rb +0 -200
@@ -1,5 +1,4 @@
1
1
  require 'active_record/connection_adapters/abstract_adapter'
2
- require 'active_support/core_ext/kernel/requires'
3
2
  require 'active_record/connection_adapters/statement_pool'
4
3
  require 'active_support/core_ext/string/encoding'
5
4
  require 'arel/visitors/bind_visitor'
@@ -8,26 +7,11 @@ module ActiveRecord
8
7
  module ConnectionAdapters #:nodoc:
9
8
  class SQLiteColumn < Column #:nodoc:
10
9
  class << self
11
- def string_to_binary(value)
12
- value.gsub(/\0|\%/n) do |b|
13
- case b
14
- when "\0" then "%00"
15
- when "%" then "%25"
16
- end
17
- end
18
- end
19
-
20
10
  def binary_to_string(value)
21
11
  if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT
22
12
  value = value.force_encoding(Encoding::ASCII_8BIT)
23
13
  end
24
-
25
- value.gsub(/%00|%25/n) do |b|
26
- case b
27
- when "%00" then "\0"
28
- when "%25" then "%"
29
- end
30
- end
14
+ value
31
15
  end
32
16
  end
33
17
  end
@@ -95,15 +79,11 @@ module ActiveRecord
95
79
  @statements = StatementPool.new(@connection,
96
80
  config.fetch(:statement_limit) { 1000 })
97
81
  @config = config
98
- end
99
-
100
- def self.visitor_for(pool) # :nodoc:
101
- config = pool.spec.config
102
82
 
103
83
  if config.fetch(:prepared_statements) { true }
104
- Arel::Visitors::SQLite.new pool
84
+ @visitor = Arel::Visitors::SQLite.new self
105
85
  else
106
- BindSubstitution.new pool
86
+ @visitor = BindSubstitution.new self
107
87
  end
108
88
  end
109
89
 
@@ -137,6 +117,11 @@ module ActiveRecord
137
117
  true
138
118
  end
139
119
 
120
+ # Returns true.
121
+ def supports_explain?
122
+ true
123
+ end
124
+
140
125
  def requires_reloading?
141
126
  true
142
127
  end
@@ -169,6 +154,10 @@ module ActiveRecord
169
154
  sqlite_version >= '3.1.0'
170
155
  end
171
156
 
157
+ def supports_index_sort_order?
158
+ sqlite_version >= '3.3.0'
159
+ end
160
+
172
161
  def native_database_types #:nodoc:
173
162
  {
174
163
  :primary_key => default_primary_key_type,
@@ -216,7 +205,7 @@ module ActiveRecord
216
205
  value = super
217
206
  if column.type == :string && value.encoding == Encoding::ASCII_8BIT
218
207
  logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger
219
- value.encode! 'utf-8'
208
+ value = value.encode Encoding::UTF_8
220
209
  end
221
210
  value
222
211
  end
@@ -230,6 +219,25 @@ module ActiveRecord
230
219
 
231
220
  # DATABASE STATEMENTS ======================================
232
221
 
222
+ def explain(arel, binds = [])
223
+ sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
224
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
225
+ end
226
+
227
+ class ExplainPrettyPrinter
228
+ # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
229
+ # the output of the SQLite shell:
230
+ #
231
+ # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
232
+ # 0|1|1|SCAN TABLE posts (~100000 rows)
233
+ #
234
+ def pp(result) # :nodoc:
235
+ result.rows.map do |row|
236
+ row.join('|')
237
+ end.join("\n") + "\n"
238
+ end
239
+ end
240
+
233
241
  def exec_query(sql, name = nil, binds = [])
234
242
  log(sql, name, binds) do
235
243
 
@@ -303,31 +311,36 @@ module ActiveRecord
303
311
  end
304
312
 
305
313
  def begin_db_transaction #:nodoc:
306
- @connection.transaction
314
+ log('begin transaction',nil) { @connection.transaction }
307
315
  end
308
316
 
309
317
  def commit_db_transaction #:nodoc:
310
- @connection.commit
318
+ log('commit transaction',nil) { @connection.commit }
311
319
  end
312
320
 
313
321
  def rollback_db_transaction #:nodoc:
314
- @connection.rollback
322
+ log('rollback transaction',nil) { @connection.rollback }
315
323
  end
316
324
 
317
325
  # SCHEMA STATEMENTS ========================================
318
326
 
319
- def tables(name = 'SCHEMA') #:nodoc:
327
+ def tables(name = 'SCHEMA', table_name = nil) #:nodoc:
320
328
  sql = <<-SQL
321
329
  SELECT name
322
330
  FROM sqlite_master
323
331
  WHERE type = 'table' AND NOT name = 'sqlite_sequence'
324
332
  SQL
333
+ sql << " AND name = #{quote_table_name(table_name)}" if table_name
325
334
 
326
335
  exec_query(sql, name).map do |row|
327
336
  row['name']
328
337
  end
329
338
  end
330
339
 
340
+ def table_exists?(name)
341
+ name && tables('SCHEMA', name).any?
342
+ end
343
+
331
344
  # Returns an array of +SQLiteColumn+ objects for the table specified by +table_name+.
332
345
  def columns(table_name, name = nil) #:nodoc:
333
346
  table_structure(table_name).map do |field|
@@ -346,12 +359,12 @@ module ActiveRecord
346
359
 
347
360
  # Returns an array of indexes for the given table.
348
361
  def indexes(table_name, name = nil) #:nodoc:
349
- exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
362
+ exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row|
350
363
  IndexDefinition.new(
351
364
  table_name,
352
365
  row['name'],
353
366
  row['unique'] != 0,
354
- exec_query("PRAGMA index_info('#{row['name']}')").map { |col|
367
+ exec_query("PRAGMA index_info('#{row['name']}')", 'SCHEMA').map { |col|
355
368
  col['name']
356
369
  })
357
370
  end
@@ -393,8 +406,15 @@ module ActiveRecord
393
406
  end
394
407
 
395
408
  def remove_column(table_name, *column_names) #:nodoc:
396
- raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
397
- column_names.flatten.each do |column_name|
409
+ raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
410
+
411
+ if column_names.flatten!
412
+ message = 'Passing array to remove_columns is deprecated, please use ' +
413
+ 'multiple arguments, like: `remove_columns(:posts, :foo, :bar)`'
414
+ ActiveSupport::Deprecation.warn message, caller
415
+ end
416
+
417
+ column_names.each do |column_name|
398
418
  alter_table(table_name) do |definition|
399
419
  definition.columns.delete(definition[column_name])
400
420
  end
@@ -425,6 +445,8 @@ module ActiveRecord
425
445
  self.limit = options[:limit] if options.include?(:limit)
426
446
  self.default = options[:default] if include_default
427
447
  self.null = options[:null] if options.include?(:null)
448
+ self.precision = options[:precision] if options.include?(:precision)
449
+ self.scale = options[:scale] if options.include?(:scale)
428
450
  end
429
451
  end
430
452
  end
@@ -468,7 +490,11 @@ module ActiveRecord
468
490
  end
469
491
 
470
492
  def copy_table(from, to, options = {}) #:nodoc:
471
- options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s))
493
+ from_primary_key = primary_key(from)
494
+ options[:primary_key] = from_primary_key if from_primary_key != 'id'
495
+ unless options[:primary_key]
496
+ options[:id] = !columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == from_primary_key
497
+ end
472
498
  create_table(to, options) do |definition|
473
499
  @definition = definition
474
500
  columns(from).each do |column|
@@ -479,9 +505,10 @@ module ActiveRecord
479
505
 
480
506
  @definition.column(column_name, column.type,
481
507
  :limit => column.limit, :default => column.default,
508
+ :precision => column.precision, :scale => column.scale,
482
509
  :null => column.null)
483
510
  end
484
- @definition.primary_key(primary_key(from)) if primary_key(from)
511
+ @definition.primary_key(from_primary_key) if from_primary_key
485
512
  yield @definition if block_given?
486
513
  end
487
514
 
@@ -507,7 +534,7 @@ module ActiveRecord
507
534
 
508
535
  unless columns.empty?
509
536
  # index name can't be the same
510
- opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") }
537
+ opts = { :name => name.gsub(/(^|_)(#{from})_/, "\\1#{to}_") }
511
538
  opts[:unique] = true if index.unique
512
539
  add_index(to, columns, opts)
513
540
  end
@@ -2,7 +2,7 @@ module ActiveRecord
2
2
  # = Active Record Counter Cache
3
3
  module CounterCache
4
4
  # Resets one or more counter caches to their correct value using an SQL
5
- # count query. This is useful when adding new counter caches, or if the
5
+ # count query. This is useful when adding new counter caches, or if the
6
6
  # counter has been corrupted or modified directly by SQL.
7
7
  #
8
8
  # ==== Parameters
@@ -19,15 +19,20 @@ module ActiveRecord
19
19
  counters.each do |association|
20
20
  has_many_association = reflect_on_association(association.to_sym)
21
21
 
22
- expected_name = if has_many_association.options[:as]
22
+ if has_many_association.options[:as]
23
23
  has_many_association.options[:as].to_s.classify
24
24
  else
25
25
  self.name
26
26
  end
27
27
 
28
+ if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
29
+ has_many_association = has_many_association.through_reflection
30
+ end
31
+
32
+ foreign_key = has_many_association.foreign_key.to_s
28
33
  child_class = has_many_association.klass
29
34
  belongs_to = child_class.reflect_on_all_associations(:belongs_to)
30
- reflection = belongs_to.find { |e| e.class_name == expected_name }
35
+ reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
31
36
  counter_name = reflection.counter_cache_column
32
37
 
33
38
  stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
@@ -65,7 +70,7 @@ module ActiveRecord
65
70
  # Post.update_counters [10, 15], :comment_count => 1
66
71
  # # Executes the following SQL:
67
72
  # # UPDATE posts
68
- # # SET comment_count = COALESCE(comment_count, 0) + 1,
73
+ # # SET comment_count = COALESCE(comment_count, 0) + 1
69
74
  # # WHERE id IN (10, 15)
70
75
  def update_counters(id, counters)
71
76
  updates = counters.map do |counter_name, value|
@@ -18,6 +18,10 @@ module ActiveRecord
18
18
  when /^find_by_([_a-zA-Z]\w*)\!$/
19
19
  bang = true
20
20
  names = $1
21
+ when /^find_or_create_by_([_a-zA-Z]\w*)\!$/
22
+ bang = true
23
+ instantiator = :create
24
+ names = $1
21
25
  when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
22
26
  instantiator = $1 == 'initialize' ? :new : :create
23
27
  names = $2
@@ -52,5 +56,13 @@ module ActiveRecord
52
56
  def bang?
53
57
  @bang
54
58
  end
59
+
60
+ def save_record?
61
+ @instantiator == :create
62
+ end
63
+
64
+ def save_method
65
+ bang? ? :save! : :save
66
+ end
55
67
  end
56
68
  end
@@ -0,0 +1,84 @@
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 !(match.is_a?(DynamicFinderMatch) && match.instantiator? && arguments.first.is_a?(Hash)) && 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 = if arguments.length > attribute_names.size
44
+ arguments.extract_options!
45
+ else
46
+ {}
47
+ end
48
+
49
+ relation = options.any? ? scoped(options) : scoped
50
+ relation.send :find_by_attributes, match, attribute_names, *arguments, &block
51
+ elsif match.instantiator?
52
+ scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
53
+ end
54
+ else
55
+ super
56
+ end
57
+ end
58
+
59
+ # Similar in purpose to +expand_hash_conditions_for_aggregates+.
60
+ def expand_attribute_names_for_aggregates(attribute_names)
61
+ attribute_names.map { |attribute_name|
62
+ unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
63
+ aggregate_mapping(aggregation).map do |field_attr, _|
64
+ field_attr.to_sym
65
+ end
66
+ else
67
+ attribute_name.to_sym
68
+ end
69
+ }.flatten
70
+ end
71
+
72
+ def all_attributes_exists?(attribute_names)
73
+ (expand_attribute_names_for_aggregates(attribute_names) -
74
+ column_methods_hash.keys).empty?
75
+ end
76
+
77
+ def aggregate_mapping(reflection)
78
+ mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
79
+ mapping.first.is_a?(Array) ? mapping : [mapping]
80
+ end
81
+
82
+
83
+ end
84
+ end
@@ -87,7 +87,7 @@ module ActiveRecord
87
87
  #
88
88
  # For example, in
89
89
  #
90
- # Location.find :all, :conditions => ["lat = ? AND lng = ?", 53.7362]
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,86 @@
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 the database adapter supports explain and auto explain is enabled,
15
+ # this method triggers EXPLAIN logging for the queries triggered by the
16
+ # block if it takes more than the threshold as a whole. That is, the
17
+ # threshold is not checked against each individual query, but against the
18
+ # duration of the entire block. This approach is convenient for relations.
19
+
20
+ #
21
+ # The available_queries_for_explain thread variable collects the queries
22
+ # to be explained. If the value is nil, it means queries are not being
23
+ # currently collected. A false value indicates collecting is turned
24
+ # off. Otherwise it is an array of queries.
25
+ def logging_query_plan # :nodoc:
26
+ return yield unless logger
27
+
28
+ threshold = auto_explain_threshold_in_seconds
29
+ current = Thread.current
30
+ if connection.supports_explain? && threshold && current[:available_queries_for_explain].nil?
31
+ begin
32
+ queries = current[:available_queries_for_explain] = []
33
+ start = Time.now
34
+ result = yield
35
+ logger.warn(exec_explain(queries)) if Time.now - start > threshold
36
+ result
37
+ ensure
38
+ current[:available_queries_for_explain] = nil
39
+ end
40
+ else
41
+ yield
42
+ end
43
+ end
44
+
45
+ # Relation#explain needs to be able to collect the queries regardless of
46
+ # whether auto explain is enabled. This method serves that purpose.
47
+ def collecting_queries_for_explain # :nodoc:
48
+ current = Thread.current
49
+ original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
50
+ return yield, current[:available_queries_for_explain]
51
+ ensure
52
+ # Note that the return value above does not depend on this assigment.
53
+ current[:available_queries_for_explain] = original
54
+ end
55
+
56
+ # Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
57
+ # Returns a formatted string ready to be logged.
58
+ def exec_explain(queries) # :nodoc:
59
+ queries && queries.map do |sql, bind|
60
+ [].tap do |msg|
61
+ msg << "EXPLAIN for: #{sql}"
62
+ unless bind.empty?
63
+ bind_msg = bind.map {|col, val| [col.name, val]}.inspect
64
+ msg.last << " #{bind_msg}"
65
+ end
66
+ msg << connection.explain(sql, bind)
67
+ end.join("\n")
68
+ end.join("\n")
69
+ end
70
+
71
+ # Silences automatic EXPLAIN logging for the duration of the block.
72
+ #
73
+ # This has high priority, no EXPLAINs will be run even if downwards
74
+ # the threshold is set to 0.
75
+ #
76
+ # As the name of the method suggests this only applies to automatic
77
+ # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run.
78
+ def silence_auto_explain
79
+ current = Thread.current
80
+ original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false
81
+ yield
82
+ ensure
83
+ current[:available_queries_for_explain] = original
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,25 @@
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
+ #
15
+ # On the other hand, we want to monitor the performance of our real database
16
+ # queries, not the performance of the access to the query cache.
17
+ IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
18
+ EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i
19
+ def ignore_payload?(payload)
20
+ payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
21
+ end
22
+
23
+ ActiveSupport::Notifications.subscribe("sql.active_record", new)
24
+ end
25
+ 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