activerecord 3.0.0.beta3 → 3.0.0.beta4

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 (51) hide show
  1. data/CHANGELOG +27 -0
  2. data/lib/active_record.rb +5 -1
  3. data/lib/active_record/aggregations.rb +1 -0
  4. data/lib/active_record/association_preload.rb +1 -1
  5. data/lib/active_record/associations.rb +88 -33
  6. data/lib/active_record/associations/association_collection.rb +12 -11
  7. data/lib/active_record/associations/association_proxy.rb +1 -1
  8. data/lib/active_record/attribute_methods.rb +10 -1
  9. data/lib/active_record/attribute_methods/dirty.rb +50 -50
  10. data/lib/active_record/attribute_methods/primary_key.rb +1 -1
  11. data/lib/active_record/autosave_association.rb +20 -5
  12. data/lib/active_record/base.rb +28 -343
  13. data/lib/active_record/callbacks.rb +23 -34
  14. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +1 -1
  15. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +3 -3
  16. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  17. data/lib/active_record/connection_adapters/abstract/database_statements.rb +56 -0
  18. data/lib/active_record/connection_adapters/abstract/query_cache.rb +11 -18
  19. data/lib/active_record/connection_adapters/abstract/quoting.rb +3 -7
  20. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +3 -3
  21. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +61 -7
  22. data/lib/active_record/connection_adapters/abstract_adapter.rb +3 -1
  23. data/lib/active_record/connection_adapters/mysql_adapter.rb +22 -50
  24. data/lib/active_record/connection_adapters/postgresql_adapter.rb +31 -143
  25. data/lib/active_record/connection_adapters/sqlite_adapter.rb +6 -2
  26. data/lib/active_record/counter_cache.rb +105 -0
  27. data/lib/active_record/fixtures.rb +16 -15
  28. data/lib/active_record/locale/en.yml +2 -2
  29. data/lib/active_record/locking/optimistic.rb +37 -16
  30. data/lib/active_record/migration.rb +7 -3
  31. data/lib/active_record/named_scope.rb +1 -5
  32. data/lib/active_record/nested_attributes.rb +13 -2
  33. data/lib/active_record/observer.rb +19 -7
  34. data/lib/active_record/persistence.rb +230 -0
  35. data/lib/active_record/railtie.rb +17 -23
  36. data/lib/active_record/railties/databases.rake +3 -2
  37. data/lib/active_record/relation.rb +5 -2
  38. data/lib/active_record/relation/batches.rb +6 -1
  39. data/lib/active_record/relation/calculations.rb +12 -9
  40. data/lib/active_record/relation/finder_methods.rb +14 -10
  41. data/lib/active_record/relation/query_methods.rb +62 -44
  42. data/lib/active_record/relation/spawn_methods.rb +6 -1
  43. data/lib/active_record/schema_dumper.rb +3 -0
  44. data/lib/active_record/serializers/xml_serializer.rb +30 -56
  45. data/lib/active_record/session_store.rb +1 -1
  46. data/lib/active_record/timestamp.rb +22 -26
  47. data/lib/active_record/transactions.rb +168 -50
  48. data/lib/active_record/validations.rb +33 -49
  49. data/lib/active_record/version.rb +1 -1
  50. data/lib/rails/generators/active_record.rb +6 -9
  51. metadata +27 -10
@@ -11,6 +11,7 @@ require 'active_record/connection_adapters/abstract/quoting'
11
11
  require 'active_record/connection_adapters/abstract/connection_pool'
12
12
  require 'active_record/connection_adapters/abstract/connection_specification'
13
13
  require 'active_record/connection_adapters/abstract/query_cache'
14
+ require 'active_record/connection_adapters/abstract/database_limits'
14
15
 
15
16
  module ActiveRecord
16
17
  module ConnectionAdapters # :nodoc:
@@ -29,6 +30,7 @@ module ActiveRecord
29
30
  # notably, the instance methods provided by SchemaStatement are very useful.
30
31
  class AbstractAdapter
31
32
  include Quoting, DatabaseStatements, SchemaStatements
33
+ include DatabaseLimits
32
34
  include QueryCache
33
35
  include ActiveSupport::Callbacks
34
36
 
@@ -197,7 +199,7 @@ module ActiveRecord
197
199
  def log(sql, name)
198
200
  name ||= "SQL"
199
201
  result = nil
200
- ActiveSupport::Notifications.instrument("active_record.sql",
202
+ ActiveSupport::Notifications.instrument("sql.active_record",
201
203
  :sql => sql, :name => name, :connection_id => self.object_id) do
202
204
  @runtime += Benchmark.ms { result = yield }
203
205
  end
@@ -3,48 +3,6 @@ require 'active_support/core_ext/kernel/requires'
3
3
  require 'active_support/core_ext/object/blank'
4
4
  require 'set'
5
5
 
6
- module MysqlCompat #:nodoc:
7
- # add all_hashes method to standard mysql-c bindings or pure ruby version
8
- def self.define_all_hashes_method!
9
- raise 'Mysql not loaded' unless defined?(::Mysql)
10
-
11
- target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
12
- return if target.instance_methods.include?('all_hashes') ||
13
- target.instance_methods.include?(:all_hashes)
14
-
15
- # Ruby driver has a version string and returns null values in each_hash
16
- # C driver >= 2.7 returns null values in each_hash
17
- if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
18
- target.class_eval <<-'end_eval'
19
- def all_hashes # def all_hashes
20
- rows = [] # rows = []
21
- each_hash { |row| rows << row } # each_hash { |row| rows << row }
22
- rows # rows
23
- end # end
24
- end_eval
25
-
26
- # adapters before 2.7 don't have a version constant
27
- # and don't return null values in each_hash
28
- else
29
- target.class_eval <<-'end_eval'
30
- def all_hashes # def all_hashes
31
- rows = [] # rows = []
32
- all_fields = fetch_fields.inject({}) { |fields, f| # all_fields = fetch_fields.inject({}) { |fields, f|
33
- fields[f.name] = nil; fields # fields[f.name] = nil; fields
34
- } # }
35
- each_hash { |row| rows << all_fields.dup.update(row) } # each_hash { |row| rows << all_fields.dup.update(row) }
36
- rows # rows
37
- end # end
38
- end_eval
39
- end
40
-
41
- unless target.instance_methods.include?('all_hashes') ||
42
- target.instance_methods.include?(:all_hashes)
43
- raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
44
- end
45
- end
46
- end
47
-
48
6
  module ActiveRecord
49
7
  class Base
50
8
  # Establishes a connection to the database that's used by all Active Record objects.
@@ -57,17 +15,17 @@ module ActiveRecord
57
15
  password = config[:password].to_s
58
16
  database = config[:database]
59
17
 
60
- # Require the MySQL driver and define Mysql::Result.all_hashes
61
18
  unless defined? Mysql
62
19
  begin
63
- require_library_or_gem('mysql')
20
+ require 'mysql'
64
21
  rescue LoadError
65
- $stderr.puts '!!! Please install the mysql gem and try again: gem install mysql.'
66
- raise
22
+ raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql', '2.8.1'"
67
23
  end
68
- end
69
24
 
70
- MysqlCompat.define_all_hashes_method!
25
+ unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash)
26
+ raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'"
27
+ end
28
+ end
71
29
 
72
30
  mysql = Mysql.init
73
31
  mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
@@ -461,10 +419,11 @@ module ActiveRecord
461
419
  if current_index != row[2]
462
420
  next if row[2] == "PRIMARY" # skip the primary key
463
421
  current_index = row[2]
464
- indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
422
+ indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [], [])
465
423
  end
466
424
 
467
425
  indexes.last.columns << row[4]
426
+ indexes.last.lengths << row[7]
468
427
  end
469
428
  result.free
470
429
  indexes
@@ -594,6 +553,18 @@ module ActiveRecord
594
553
  end
595
554
 
596
555
  protected
556
+ def quoted_columns_for_index(column_names, options = {})
557
+ length = options[:length] if options.is_a?(Hash)
558
+
559
+ quoted_column_names = case length
560
+ when Hash
561
+ column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
562
+ when Fixnum
563
+ column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
564
+ else
565
+ column_names.map {|name| quote_column_name(name) }
566
+ end
567
+ end
597
568
 
598
569
  def translate_exception(exception, message)
599
570
  return super unless exception.respond_to?(:errno)
@@ -643,7 +614,8 @@ module ActiveRecord
643
614
  def select(sql, name = nil)
644
615
  @connection.query_with_result = true
645
616
  result = execute(sql, name)
646
- rows = result.all_hashes
617
+ rows = []
618
+ result.each_hash { |row| rows << row }
647
619
  result.free
648
620
  rows
649
621
  end
@@ -2,26 +2,12 @@ require 'active_record/connection_adapters/abstract_adapter'
2
2
  require 'active_support/core_ext/kernel/requires'
3
3
  require 'active_support/core_ext/object/blank'
4
4
 
5
- begin
6
- require_library_or_gem 'pg'
7
- rescue LoadError => e
8
- begin
9
- require_library_or_gem 'postgres'
10
- class PGresult
11
- alias_method :nfields, :num_fields unless self.method_defined?(:nfields)
12
- alias_method :ntuples, :num_tuples unless self.method_defined?(:ntuples)
13
- alias_method :ftype, :type unless self.method_defined?(:ftype)
14
- alias_method :cmd_tuples, :cmdtuples unless self.method_defined?(:cmd_tuples)
15
- end
16
- rescue LoadError
17
- raise e
18
- end
19
- end
20
-
21
5
  module ActiveRecord
22
6
  class Base
23
7
  # Establishes a connection to the database that's used by all Active Record objects
24
8
  def self.postgresql_connection(config) # :nodoc:
9
+ require 'pg'
10
+
25
11
  config = config.symbolize_keys
26
12
  host = config[:host]
27
13
  port = config[:port] || 5432
@@ -277,20 +263,12 @@ module ActiveRecord
277
263
  true
278
264
  end
279
265
 
280
- # Does PostgreSQL support standard conforming strings?
281
- def supports_standard_conforming_strings?
282
- # Temporarily set the client message level above error to prevent unintentional
283
- # error messages in the logs when working on a PostgreSQL database server that
284
- # does not support standard conforming strings.
285
- client_min_messages_old = client_min_messages
286
- self.client_min_messages = 'panic'
287
-
288
- # postgres-pr does not raise an exception when client_min_messages is set higher
289
- # than error and "SHOW standard_conforming_strings" fails, but returns an empty
290
- # PGresult instead.
291
- has_support = query('SHOW standard_conforming_strings')[0][0] rescue false
292
- self.client_min_messages = client_min_messages_old
293
- has_support
266
+ # Enable standard-conforming strings if available.
267
+ def set_standard_conforming_strings
268
+ old, self.client_min_messages = client_min_messages, 'panic'
269
+ execute('SET standard_conforming_strings = on') rescue nil
270
+ ensure
271
+ self.client_min_messages = old
294
272
  end
295
273
 
296
274
  def supports_insert_with_returning?
@@ -314,85 +292,23 @@ module ActiveRecord
314
292
  # QUOTING ==================================================
315
293
 
316
294
  # Escapes binary strings for bytea input to the database.
317
- def escape_bytea(original_value)
318
- if @connection.respond_to?(:escape_bytea)
319
- self.class.instance_eval do
320
- define_method(:escape_bytea) do |value|
321
- @connection.escape_bytea(value) if value
322
- end
323
- end
324
- elsif PGconn.respond_to?(:escape_bytea)
325
- self.class.instance_eval do
326
- define_method(:escape_bytea) do |value|
327
- PGconn.escape_bytea(value) if value
328
- end
329
- end
330
- else
331
- self.class.instance_eval do
332
- define_method(:escape_bytea) do |value|
333
- if value
334
- result = ''
335
- value.each_byte { |c| result << sprintf('\\\\%03o', c) }
336
- result
337
- end
338
- end
339
- end
340
- end
341
- escape_bytea(original_value)
295
+ def escape_bytea(value)
296
+ @connection.escape_bytea(value) if value
342
297
  end
343
298
 
344
299
  # Unescapes bytea output from a database to the binary string it represents.
345
300
  # NOTE: This is NOT an inverse of escape_bytea! This is only to be used
346
301
  # on escaped binary output from database drive.
347
- def unescape_bytea(original_value)
348
- # In each case, check if the value actually is escaped PostgreSQL bytea output
349
- # or an unescaped Active Record attribute that was just written.
350
- if PGconn.respond_to?(:unescape_bytea)
351
- self.class.instance_eval do
352
- define_method(:unescape_bytea) do |value|
353
- if value =~ /\\\d{3}/
354
- PGconn.unescape_bytea(value)
355
- else
356
- value
357
- end
358
- end
359
- end
360
- else
361
- self.class.instance_eval do
362
- define_method(:unescape_bytea) do |value|
363
- if value =~ /\\\d{3}/
364
- result = ''
365
- i, max = 0, value.size
366
- while i < max
367
- char = value[i]
368
- if char == ?\\
369
- if value[i+1] == ?\\
370
- char = ?\\
371
- i += 1
372
- else
373
- char = value[i+1..i+3].oct
374
- i += 3
375
- end
376
- end
377
- result << char
378
- i += 1
379
- end
380
- result
381
- else
382
- value
383
- end
384
- end
385
- end
386
- end
387
- unescape_bytea(original_value)
302
+ def unescape_bytea(value)
303
+ @connection.unescape_bytea(value) if value
388
304
  end
389
305
 
390
306
  # Quotes PostgreSQL-specific data types for SQL input.
391
307
  def quote(value, column = nil) #:nodoc:
392
308
  if value.kind_of?(String) && column && column.type == :binary
393
- "#{quoted_string_prefix}'#{escape_bytea(value)}'"
309
+ "'#{escape_bytea(value)}'"
394
310
  elsif value.kind_of?(String) && column && column.sql_type == 'xml'
395
- "xml E'#{quote_string(value)}'"
311
+ "xml '#{quote_string(value)}'"
396
312
  elsif value.kind_of?(Numeric) && column && column.sql_type == 'money'
397
313
  # Not truly string input, so doesn't require (or allow) escape string syntax.
398
314
  "'#{value.to_s}'"
@@ -408,28 +324,9 @@ module ActiveRecord
408
324
  end
409
325
  end
410
326
 
411
- # Quotes strings for use in SQL input in the postgres driver for better performance.
412
- def quote_string(original_value) #:nodoc:
413
- if @connection.respond_to?(:escape)
414
- self.class.instance_eval do
415
- define_method(:quote_string) do |s|
416
- @connection.escape(s)
417
- end
418
- end
419
- elsif PGconn.respond_to?(:escape)
420
- self.class.instance_eval do
421
- define_method(:quote_string) do |s|
422
- PGconn.escape(s)
423
- end
424
- end
425
- else
426
- # There are some incorrectly compiled postgres drivers out there
427
- # that don't define PGconn.escape.
428
- self.class.instance_eval do
429
- remove_method(:quote_string)
430
- end
431
- end
432
- quote_string(original_value)
327
+ # Quotes strings for use in SQL input.
328
+ def quote_string(s) #:nodoc:
329
+ @connection.escape(s)
433
330
  end
434
331
 
435
332
  # Checks the following cases:
@@ -593,12 +490,8 @@ module ActiveRecord
593
490
  execute "ROLLBACK"
594
491
  end
595
492
 
596
- if defined?(PGconn::PQTRANS_IDLE)
597
- # The ruby-pg driver supports inspecting the transaction status,
598
- # while the ruby-postgres driver does not.
599
- def outside_transaction?
600
- @connection.transaction_status == PGconn::PQTRANS_IDLE
601
- end
493
+ def outside_transaction?
494
+ @connection.transaction_status == PGconn::PQTRANS_IDLE
602
495
  end
603
496
 
604
497
  def create_savepoint
@@ -926,9 +819,12 @@ module ActiveRecord
926
819
  execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
927
820
  end
928
821
 
929
- # Drops an index from a table.
930
- def remove_index(table_name, options = {})
931
- execute "DROP INDEX #{quote_table_name(index_name(table_name, options))}"
822
+ def remove_index!(table_name, index_name) #:nodoc:
823
+ execute "DROP INDEX #{quote_table_name(index_name)}"
824
+ end
825
+
826
+ def index_name_length
827
+ 63
932
828
  end
933
829
 
934
830
  # Maps logical Rails types to PostgreSQL-specific data types.
@@ -1005,22 +901,11 @@ module ActiveRecord
1005
901
  # Ignore async_exec and async_query when using postgres-pr.
1006
902
  @async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec)
1007
903
 
1008
- # Use escape string syntax if available. We cannot do this lazily when encountering
1009
- # the first string, because that could then break any transactions in progress.
1010
- # See: http://www.postgresql.org/docs/current/static/runtime-config-compatible.html
1011
- # If PostgreSQL doesn't know the standard_conforming_strings parameter then it doesn't
1012
- # support escape string syntax. Don't override the inherited quoted_string_prefix.
1013
- if supports_standard_conforming_strings?
1014
- self.class.instance_eval do
1015
- define_method(:quoted_string_prefix) { 'E' }
1016
- end
1017
- end
1018
-
1019
904
  # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
1020
905
  # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
1021
906
  # should know about this but can't detect it there, so deal with it here.
1022
- PostgreSQLColumn.money_precision =
1023
- (postgresql_version >= 80300) ? 19 : 10
907
+ PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
908
+
1024
909
  configure_connection
1025
910
  end
1026
911
 
@@ -1036,7 +921,10 @@ module ActiveRecord
1036
921
  end
1037
922
  self.client_min_messages = @config[:min_messages] if @config[:min_messages]
1038
923
  self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
1039
-
924
+
925
+ # Use standard-conforming strings if available so we don't have to do the E'...' dance.
926
+ set_standard_conforming_strings
927
+
1040
928
  # If using ActiveRecord's time zone support configure the connection to return
1041
929
  # TIMESTAMP WITH ZONE types in UTC.
1042
930
  execute("SET time zone 'UTC'") if ActiveRecord::Base.default_timezone == :utc
@@ -34,6 +34,10 @@ module ActiveRecord
34
34
  end
35
35
 
36
36
  def binary_to_string(value)
37
+ if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT
38
+ value = value.force_encoding(Encoding::ASCII_8BIT)
39
+ end
40
+
37
41
  value.gsub(/%00|%25/n) do |b|
38
42
  case b
39
43
  when "%00" then "\0"
@@ -217,8 +221,8 @@ module ActiveRecord
217
221
  column ? column['name'] : nil
218
222
  end
219
223
 
220
- def remove_index(table_name, options={}) #:nodoc:
221
- execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
224
+ def remove_index!(table_name, index_name) #:nodoc:
225
+ execute "DROP INDEX #{quote_column_name(index_name)}"
222
226
  end
223
227
 
224
228
  def rename_table(name, new_name)
@@ -0,0 +1,105 @@
1
+ module ActiveRecord
2
+ module CounterCache
3
+ # Resets one or more counter caches to their correct value using an SQL
4
+ # count query. This is useful when adding new counter caches, or if the
5
+ # counter has been corrupted or modified directly by SQL.
6
+ #
7
+ # ==== Parameters
8
+ #
9
+ # * +id+ - The id of the object you wish to reset a counter on.
10
+ # * +counters+ - One or more counter names to reset
11
+ #
12
+ # ==== Examples
13
+ #
14
+ # # For Post with id #1 records reset the comments_count
15
+ # Post.reset_counters(1, :comments)
16
+ def reset_counters(id, *counters)
17
+ object = find(id)
18
+ counters.each do |association|
19
+ child_class = reflect_on_association(association.to_sym).klass
20
+ belongs_name = self.name.demodulize.underscore.to_sym
21
+ counter_name = child_class.reflect_on_association(belongs_name).counter_cache_column
22
+
23
+ self.unscoped.where(arel_table[self.primary_key].eq(object.id)).arel.update({
24
+ arel_table[counter_name] => object.send(association).count
25
+ })
26
+ end
27
+ return true
28
+ end
29
+
30
+ # A generic "counter updater" implementation, intended primarily to be
31
+ # used by increment_counter and decrement_counter, but which may also
32
+ # be useful on its own. It simply does a direct SQL update for the record
33
+ # with the given ID, altering the given hash of counters by the amount
34
+ # given by the corresponding value:
35
+ #
36
+ # ==== Parameters
37
+ #
38
+ # * +id+ - The id of the object you wish to update a counter on or an Array of ids.
39
+ # * +counters+ - An Array of Hashes containing the names of the fields
40
+ # to update as keys and the amount to update the field by as values.
41
+ #
42
+ # ==== Examples
43
+ #
44
+ # # For the Post with id of 5, decrement the comment_count by 1, and
45
+ # # increment the action_count by 1
46
+ # Post.update_counters 5, :comment_count => -1, :action_count => 1
47
+ # # Executes the following SQL:
48
+ # # UPDATE posts
49
+ # # SET comment_count = comment_count - 1,
50
+ # # action_count = action_count + 1
51
+ # # WHERE id = 5
52
+ #
53
+ # # For the Posts with id of 10 and 15, increment the comment_count by 1
54
+ # Post.update_counters [10, 15], :comment_count => 1
55
+ # # Executes the following SQL:
56
+ # # UPDATE posts
57
+ # # SET comment_count = comment_count + 1,
58
+ # # WHERE id IN (10, 15)
59
+ def update_counters(id, counters)
60
+ updates = counters.map do |counter_name, value|
61
+ operator = value < 0 ? '-' : '+'
62
+ quoted_column = connection.quote_column_name(counter_name)
63
+ "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
64
+ end
65
+
66
+ update_all(updates.join(', '), primary_key => id )
67
+ end
68
+
69
+ # Increment a number field by one, usually representing a count.
70
+ #
71
+ # This is used for caching aggregate values, so that they don't need to be computed every time.
72
+ # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is
73
+ # shown it would have to run an SQL query to find how many posts and comments there are.
74
+ #
75
+ # ==== Parameters
76
+ #
77
+ # * +counter_name+ - The name of the field that should be incremented.
78
+ # * +id+ - The id of the object that should be incremented.
79
+ #
80
+ # ==== Examples
81
+ #
82
+ # # Increment the post_count column for the record with an id of 5
83
+ # DiscussionBoard.increment_counter(:post_count, 5)
84
+ def increment_counter(counter_name, id)
85
+ update_counters(id, counter_name => 1)
86
+ end
87
+
88
+ # Decrement a number field by one, usually representing a count.
89
+ #
90
+ # This works the same as increment_counter but reduces the column value by 1 instead of increasing it.
91
+ #
92
+ # ==== Parameters
93
+ #
94
+ # * +counter_name+ - The name of the field that should be decremented.
95
+ # * +id+ - The id of the object that should be decremented.
96
+ #
97
+ # ==== Examples
98
+ #
99
+ # # Decrement the post_count column for the record with an id of 5
100
+ # DiscussionBoard.decrement_counter(:post_count, 5)
101
+ def decrement_counter(counter_name, id)
102
+ update_counters(id, counter_name => -1)
103
+ end
104
+ end
105
+ end