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.

Files changed (181) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2102 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +35 -44
  5. data/examples/performance.rb +110 -100
  6. data/lib/active_record/aggregations.rb +59 -75
  7. data/lib/active_record/associations/alias_tracker.rb +76 -0
  8. data/lib/active_record/associations/association.rb +248 -0
  9. data/lib/active_record/associations/association_scope.rb +135 -0
  10. data/lib/active_record/associations/belongs_to_association.rb +60 -59
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +16 -59
  12. data/lib/active_record/associations/builder/association.rb +108 -0
  13. data/lib/active_record/associations/builder/belongs_to.rb +98 -0
  14. data/lib/active_record/associations/builder/collection_association.rb +89 -0
  15. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +39 -0
  16. data/lib/active_record/associations/builder/has_many.rb +15 -0
  17. data/lib/active_record/associations/builder/has_one.rb +25 -0
  18. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  19. data/lib/active_record/associations/collection_association.rb +608 -0
  20. data/lib/active_record/associations/collection_proxy.rb +986 -0
  21. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +40 -112
  22. data/lib/active_record/associations/has_many_association.rb +83 -76
  23. data/lib/active_record/associations/has_many_through_association.rb +147 -66
  24. data/lib/active_record/associations/has_one_association.rb +67 -108
  25. data/lib/active_record/associations/has_one_through_association.rb +21 -25
  26. data/lib/active_record/associations/join_dependency/join_association.rb +174 -0
  27. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  28. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  29. data/lib/active_record/associations/join_dependency.rb +235 -0
  30. data/lib/active_record/associations/join_helper.rb +45 -0
  31. data/lib/active_record/associations/preloader/association.rb +121 -0
  32. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  33. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  34. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  35. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  36. data/lib/active_record/associations/preloader/has_many_through.rb +19 -0
  37. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  38. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  39. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  40. data/lib/active_record/associations/preloader/through_association.rb +63 -0
  41. data/lib/active_record/associations/preloader.rb +178 -0
  42. data/lib/active_record/associations/singular_association.rb +64 -0
  43. data/lib/active_record/associations/through_association.rb +87 -0
  44. data/lib/active_record/associations.rb +512 -1224
  45. data/lib/active_record/attribute_assignment.rb +201 -0
  46. data/lib/active_record/attribute_methods/before_type_cast.rb +49 -12
  47. data/lib/active_record/attribute_methods/dirty.rb +51 -28
  48. data/lib/active_record/attribute_methods/primary_key.rb +94 -22
  49. data/lib/active_record/attribute_methods/query.rb +5 -4
  50. data/lib/active_record/attribute_methods/read.rb +63 -72
  51. data/lib/active_record/attribute_methods/serialization.rb +162 -0
  52. data/lib/active_record/attribute_methods/time_zone_conversion.rb +39 -41
  53. data/lib/active_record/attribute_methods/write.rb +39 -13
  54. data/lib/active_record/attribute_methods.rb +362 -29
  55. data/lib/active_record/autosave_association.rb +132 -75
  56. data/lib/active_record/base.rb +83 -1627
  57. data/lib/active_record/callbacks.rb +69 -47
  58. data/lib/active_record/coders/yaml_column.rb +38 -0
  59. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +411 -138
  60. data/lib/active_record/connection_adapters/abstract/database_limits.rb +21 -11
  61. data/lib/active_record/connection_adapters/abstract/database_statements.rb +234 -173
  62. data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -22
  63. data/lib/active_record/connection_adapters/abstract/quoting.rb +82 -25
  64. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +176 -414
  65. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +70 -0
  66. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +562 -232
  67. data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
  68. data/lib/active_record/connection_adapters/abstract_adapter.rb +281 -53
  69. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +782 -0
  70. data/lib/active_record/connection_adapters/column.rb +318 -0
  71. data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
  72. data/lib/active_record/connection_adapters/mysql2_adapter.rb +273 -0
  73. data/lib/active_record/connection_adapters/mysql_adapter.rb +365 -450
  74. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
  75. data/lib/active_record/connection_adapters/postgresql/cast.rb +152 -0
  76. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
  77. data/lib/active_record/connection_adapters/postgresql/oid.rb +366 -0
  78. data/lib/active_record/connection_adapters/postgresql/quoting.rb +171 -0
  79. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  80. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +489 -0
  81. data/lib/active_record/connection_adapters/postgresql_adapter.rb +672 -752
  82. data/lib/active_record/connection_adapters/schema_cache.rb +129 -0
  83. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +588 -17
  84. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  85. data/lib/active_record/connection_handling.rb +98 -0
  86. data/lib/active_record/core.rb +463 -0
  87. data/lib/active_record/counter_cache.rb +108 -101
  88. data/lib/active_record/dynamic_matchers.rb +131 -0
  89. data/lib/active_record/errors.rb +54 -13
  90. data/lib/active_record/explain.rb +38 -0
  91. data/lib/active_record/explain_registry.rb +30 -0
  92. data/lib/active_record/explain_subscriber.rb +29 -0
  93. data/lib/active_record/fixture_set/file.rb +55 -0
  94. data/lib/active_record/fixtures.rb +703 -785
  95. data/lib/active_record/inheritance.rb +200 -0
  96. data/lib/active_record/integration.rb +60 -0
  97. data/lib/active_record/locale/en.yml +8 -1
  98. data/lib/active_record/locking/optimistic.rb +69 -60
  99. data/lib/active_record/locking/pessimistic.rb +34 -12
  100. data/lib/active_record/log_subscriber.rb +40 -6
  101. data/lib/active_record/migration/command_recorder.rb +164 -0
  102. data/lib/active_record/migration/join_table.rb +15 -0
  103. data/lib/active_record/migration.rb +614 -216
  104. data/lib/active_record/model_schema.rb +345 -0
  105. data/lib/active_record/nested_attributes.rb +248 -119
  106. data/lib/active_record/null_relation.rb +65 -0
  107. data/lib/active_record/persistence.rb +275 -57
  108. data/lib/active_record/query_cache.rb +29 -9
  109. data/lib/active_record/querying.rb +62 -0
  110. data/lib/active_record/railtie.rb +135 -21
  111. data/lib/active_record/railties/console_sandbox.rb +5 -0
  112. data/lib/active_record/railties/controller_runtime.rb +17 -5
  113. data/lib/active_record/railties/databases.rake +249 -359
  114. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  115. data/lib/active_record/readonly_attributes.rb +30 -0
  116. data/lib/active_record/reflection.rb +283 -103
  117. data/lib/active_record/relation/batches.rb +38 -34
  118. data/lib/active_record/relation/calculations.rb +252 -139
  119. data/lib/active_record/relation/delegation.rb +125 -0
  120. data/lib/active_record/relation/finder_methods.rb +182 -188
  121. data/lib/active_record/relation/merger.rb +161 -0
  122. data/lib/active_record/relation/predicate_builder.rb +86 -21
  123. data/lib/active_record/relation/query_methods.rb +917 -134
  124. data/lib/active_record/relation/spawn_methods.rb +53 -92
  125. data/lib/active_record/relation.rb +405 -143
  126. data/lib/active_record/result.rb +67 -0
  127. data/lib/active_record/runtime_registry.rb +17 -0
  128. data/lib/active_record/sanitization.rb +168 -0
  129. data/lib/active_record/schema.rb +20 -14
  130. data/lib/active_record/schema_dumper.rb +55 -46
  131. data/lib/active_record/schema_migration.rb +39 -0
  132. data/lib/active_record/scoping/default.rb +146 -0
  133. data/lib/active_record/scoping/named.rb +175 -0
  134. data/lib/active_record/scoping.rb +82 -0
  135. data/lib/active_record/serialization.rb +8 -46
  136. data/lib/active_record/serializers/xml_serializer.rb +21 -68
  137. data/lib/active_record/statement_cache.rb +26 -0
  138. data/lib/active_record/store.rb +156 -0
  139. data/lib/active_record/tasks/database_tasks.rb +203 -0
  140. data/lib/active_record/tasks/firebird_database_tasks.rb +56 -0
  141. data/lib/active_record/tasks/mysql_database_tasks.rb +143 -0
  142. data/lib/active_record/tasks/oracle_database_tasks.rb +45 -0
  143. data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
  144. data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
  145. data/lib/active_record/tasks/sqlserver_database_tasks.rb +48 -0
  146. data/lib/active_record/test_case.rb +57 -28
  147. data/lib/active_record/timestamp.rb +49 -18
  148. data/lib/active_record/transactions.rb +106 -63
  149. data/lib/active_record/translation.rb +22 -0
  150. data/lib/active_record/validations/associated.rb +25 -24
  151. data/lib/active_record/validations/presence.rb +65 -0
  152. data/lib/active_record/validations/uniqueness.rb +123 -83
  153. data/lib/active_record/validations.rb +29 -29
  154. data/lib/active_record/version.rb +7 -5
  155. data/lib/active_record.rb +83 -34
  156. data/lib/rails/generators/active_record/migration/migration_generator.rb +46 -9
  157. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
  158. data/lib/rails/generators/active_record/migration/templates/migration.rb +30 -8
  159. data/lib/rails/generators/active_record/model/model_generator.rb +15 -5
  160. data/lib/rails/generators/active_record/model/templates/model.rb +7 -2
  161. data/lib/rails/generators/active_record/model/templates/module.rb +3 -1
  162. data/lib/rails/generators/active_record.rb +4 -8
  163. metadata +163 -121
  164. data/CHANGELOG +0 -6023
  165. data/examples/associations.png +0 -0
  166. data/lib/active_record/association_preload.rb +0 -403
  167. data/lib/active_record/associations/association_collection.rb +0 -562
  168. data/lib/active_record/associations/association_proxy.rb +0 -295
  169. data/lib/active_record/associations/through_association_scope.rb +0 -154
  170. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -113
  171. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -401
  172. data/lib/active_record/dynamic_finder_match.rb +0 -53
  173. data/lib/active_record/dynamic_scope_match.rb +0 -32
  174. data/lib/active_record/named_scope.rb +0 -138
  175. data/lib/active_record/observer.rb +0 -140
  176. data/lib/active_record/session_store.rb +0 -340
  177. data/lib/rails/generators/active_record/model/templates/migration.rb +0 -16
  178. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  179. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -2
  180. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -24
  181. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -16
@@ -1,79 +1,83 @@
1
- require 'active_support/core_ext/object/blank'
2
1
 
3
2
  module ActiveRecord
4
- module Batches # :nodoc:
5
- # Yields each record that was found by the find +options+. The find is
6
- # performed by find_in_batches with a batch size of 1000 (or as
7
- # specified by the <tt>:batch_size</tt> option).
3
+ module Batches
4
+ # Looping through a collection of records from the database
5
+ # (using the +all+ method, for example) is very inefficient
6
+ # since it will try to instantiate all the objects at once.
8
7
  #
9
- # Example:
8
+ # In that case, batch processing methods allow you to work
9
+ # with the records in batches, thereby greatly reducing memory consumption.
10
+ #
11
+ # The #find_each method uses #find_in_batches with a batch size of 1000 (or as
12
+ # specified by the +:batch_size+ option).
13
+ #
14
+ # Person.all.find_each do |person|
15
+ # person.do_awesome_stuff
16
+ # end
10
17
  #
11
18
  # Person.where("age > 21").find_each do |person|
12
19
  # person.party_all_night!
13
20
  # end
14
21
  #
15
- # Note: This method is only intended to use for batch processing of
16
- # large amounts of records that wouldn't fit in memory all at once. If
17
- # you just need to loop over less than 1000 records, it's probably
18
- # better just to use the regular find methods.
22
+ # You can also pass the +:start+ option to specify
23
+ # an offset to control the starting point.
19
24
  def find_each(options = {})
20
25
  find_in_batches(options) do |records|
21
26
  records.each { |record| yield record }
22
27
  end
23
-
24
- self
25
28
  end
26
29
 
27
30
  # Yields each batch of records that was found by the find +options+ as
28
- # an array. The size of each batch is set by the <tt>:batch_size</tt>
31
+ # an array. The size of each batch is set by the +:batch_size+
29
32
  # option; the default is 1000.
30
33
  #
31
34
  # You can control the starting point for the batch processing by
32
- # supplying the <tt>:start</tt> option. This is especially useful if you
35
+ # supplying the +:start+ option. This is especially useful if you
33
36
  # want multiple workers dealing with the same processing queue. You can
34
37
  # make worker 1 handle all the records between id 0 and 10,000 and
35
- # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt>
38
+ # worker 2 handle from 10,000 and beyond (by setting the +:start+
36
39
  # option on that worker).
37
40
  #
38
41
  # It's not possible to set the order. That is automatically set to
39
42
  # ascending on the primary key ("id ASC") to make the batch ordering
40
- # work. This also mean that this method only works with integer-based
43
+ # work. This also means that this method only works with integer-based
41
44
  # primary keys. You can't set the limit either, that's used to control
42
- # the the batch sizes.
43
- #
44
- # Example:
45
+ # the batch sizes.
45
46
  #
46
47
  # Person.where("age > 21").find_in_batches do |group|
47
48
  # sleep(50) # Make sure it doesn't get too crowded in there!
48
49
  # group.each { |person| person.party_all_night! }
49
50
  # end
51
+ #
52
+ # # Let's process the next 2000 records
53
+ # Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group|
54
+ # group.each { |person| person.party_all_night! }
55
+ # end
50
56
  def find_in_batches(options = {})
57
+ options.assert_valid_keys(:start, :batch_size)
58
+
51
59
  relation = self
52
60
 
53
- if orders.present? || taken.present?
61
+ unless arel.orders.blank? && arel.taken.blank?
54
62
  ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
55
63
  end
56
64
 
57
- if (finder_options = options.except(:start, :batch_size)).present?
58
- raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order].present?
59
- raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit].present?
60
-
61
- relation = apply_finder_options(finder_options)
62
- end
63
-
64
- start = options.delete(:start).to_i
65
+ start = options.delete(:start)
65
66
  batch_size = options.delete(:batch_size) || 1000
66
67
 
67
- relation = relation.except(:order).order(batch_order).limit(batch_size)
68
- records = relation.where(primary_key.gteq(start)).all
68
+ relation = relation.reorder(batch_order).limit(batch_size)
69
+ records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a
69
70
 
70
71
  while records.any?
72
+ records_size = records.size
73
+ primary_key_offset = records.last.id
74
+
71
75
  yield records
72
76
 
73
- break if records.size < batch_size
77
+ break if records_size < batch_size
74
78
 
75
- if primary_key_offset = records.last.id
76
- records = relation.where(primary_key.gt(primary_key_offset)).to_a
79
+ if primary_key_offset
80
+ records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
77
81
  else
78
82
  raise "Primary key not included in the custom select clause"
79
83
  end
@@ -83,7 +87,7 @@ module ActiveRecord
83
87
  private
84
88
 
85
89
  def batch_order
86
- "#{@klass.table_name}.#{@klass.primary_key} ASC"
90
+ "#{quoted_table_name}.#{quoted_primary_key} ASC"
87
91
  end
88
92
  end
89
93
  end
@@ -1,58 +1,24 @@
1
- require 'active_support/core_ext/object/blank'
2
- require 'active_support/core_ext/object/try'
3
-
4
1
  module ActiveRecord
5
2
  module Calculations
6
- # Count operates using three different approaches.
7
- #
8
- # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
9
- # * Count using column: By passing a column name to count, it will return a count of all the
10
- # rows for the model with supplied column present.
11
- # * Count using options will find the row count matched by the options used.
12
- #
13
- # The third approach, count using options, accepts an option hash as the only parameter. The options are:
14
- #
15
- # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
16
- # See conditions in the intro to ActiveRecord::Base.
17
- # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id"
18
- # (rarely needed) or named associations in the same form used for the <tt>:include</tt> option, which will
19
- # perform an INNER JOIN on the associated table(s). If the value is a string, then the records
20
- # will be returned read-only since they will have attributes that do not correspond to the table's columns.
21
- # Pass <tt>:readonly => false</tt> to override.
22
- # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs.
23
- # The symbols named refer to already defined associations. When using named associations, count
24
- # returns the number of DISTINCT items for the model you're counting.
25
- # See eager loading under Associations.
26
- # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
27
- # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
28
- # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example,
29
- # want to do a join but not include the joined columns.
30
- # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as
31
- # SELECT COUNT(DISTINCT posts.id) ...
32
- # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an
33
- # alternate table name (or even the name of a database view).
34
- #
35
- # Examples for counting all:
36
- # Person.count # returns the total count of all people
3
+ # Count the records.
37
4
  #
38
- # Examples for counting by column:
39
- # Person.count(:age) # returns the total count of all people whose age is present in database
5
+ # Person.count
6
+ # # => the total count of all people
40
7
  #
41
- # Examples for count with options:
42
- # Person.count(:conditions => "age > 26")
8
+ # Person.count(:age)
9
+ # # => returns the total count of all people whose age is present in database
43
10
  #
44
- # # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN.
45
- # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job)
11
+ # Person.count(:all)
12
+ # # => performs a COUNT(*) (:all is an alias for '*')
46
13
  #
47
- # # finds the number of rows matching the conditions and joins.
48
- # Person.count(:conditions => "age > 26 AND job.salary > 60000",
49
- # :joins => "LEFT JOIN jobs on jobs.person_id = person.id")
14
+ # Person.distinct.count(:age)
15
+ # # => counts the number of different age values
50
16
  #
51
- # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id)
52
- # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*')
17
+ # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column,
18
+ # and the values are the respective amounts:
53
19
  #
54
- # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition.
55
- # Use Person.count instead.
20
+ # Person.group(:city).count
21
+ # # => { 'Rome' => 5, 'Paris' => 3 }
56
22
  def count(column_name = nil, options = {})
57
23
  column_name, options = nil, column_name if column_name.is_a?(Hash)
58
24
  calculate(:count, column_name, options)
@@ -66,7 +32,7 @@ module ActiveRecord
66
32
  calculate(:average, column_name, options)
67
33
  end
68
34
 
69
- # Calculates the minimum value on a given column. The value is returned
35
+ # Calculates the minimum value on a given column. The value is returned
70
36
  # with the same data type of the column, or +nil+ if there's no row. See
71
37
  # +calculate+ for examples with options.
72
38
  #
@@ -89,150 +55,285 @@ module ActiveRecord
89
55
  # +calculate+ for examples with options.
90
56
  #
91
57
  # Person.sum('age') # => 4562
92
- def sum(column_name, options = {})
93
- calculate(:sum, column_name, options)
58
+ def sum(*args)
59
+ if block_given?
60
+ ActiveSupport::Deprecation.warn(
61
+ "Calling #sum with a block is deprecated and will be removed in Rails 4.1. " \
62
+ "If you want to perform sum calculation over the array of elements, use `to_a.sum(&block)`."
63
+ )
64
+ self.to_a.sum(*args) {|*block_args| yield(*block_args)}
65
+ else
66
+ calculate(:sum, *args)
67
+ end
94
68
  end
95
69
 
96
- # This calculates aggregate values in the given column. Methods for count, sum, average,
97
- # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>,
98
- # <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query.
70
+ # This calculates aggregate values in the given column. Methods for count, sum, average,
71
+ # minimum, and maximum have been added as shortcuts.
99
72
  #
100
73
  # There are two basic forms of output:
74
+ #
101
75
  # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
102
76
  # for AVG, and the given column's type for everything else.
103
- # * Grouped values: This returns an ordered hash of the values and groups them by the
104
- # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association.
105
77
  #
106
- # values = Person.maximum(:age, :group => 'last_name')
78
+ # * Grouped values: This returns an ordered hash of the values and groups them. It
79
+ # takes either a column name, or the name of a belongs_to association.
80
+ #
81
+ # values = Person.group('last_name').maximum(:age)
107
82
  # puts values["Drake"]
108
- # => 43
83
+ # # => 43
109
84
  #
110
- # drake = Family.find_by_last_name('Drake')
111
- # values = Person.maximum(:age, :group => :family) # Person belongs_to :family
85
+ # drake = Family.find_by(last_name: 'Drake')
86
+ # values = Person.group(:family).maximum(:age) # Person belongs_to :family
112
87
  # puts values[drake]
113
- # => 43
88
+ # # => 43
114
89
  #
115
90
  # values.each do |family, max_age|
116
91
  # ...
117
92
  # end
118
93
  #
119
- # Options:
120
- # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
121
- # See conditions in the intro to ActiveRecord::Base.
122
- # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything,
123
- # the purpose of this is to access fields on joined tables in your conditions, order, or group clauses.
124
- # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id".
125
- # (Rarely needed).
126
- # The records will be returned read-only since they will have attributes that do not correspond to the
127
- # table's columns.
128
- # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
129
- # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
130
- # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example
131
- # want to do a join, but not include the joined columns.
132
- # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as
133
- # SELECT COUNT(DISTINCT posts.id) ...
134
- #
135
- # Examples:
136
94
  # Person.calculate(:count, :all) # The same as Person.count
137
95
  # Person.average(:age) # SELECT AVG(age) FROM people...
138
- # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for
139
- # # everyone with a last name other than 'Drake'
140
96
  #
141
97
  # # Selects the minimum age for any family without any minors
142
- # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name)
98
+ # Person.group(:last_name).having("min(age) > 17").minimum(:age)
143
99
  #
144
100
  # Person.sum("2 * age")
145
101
  def calculate(operation, column_name, options = {})
146
- if options.except(:distinct).present?
147
- apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct])
148
- else
149
- if eager_loading? || includes_values.present?
102
+ relation = with_default_scope
103
+
104
+ if relation.equal?(self)
105
+ if has_include?(column_name)
150
106
  construct_relation_for_association_calculations.calculate(operation, column_name, options)
151
107
  else
152
108
  perform_calculation(operation, column_name, options)
153
109
  end
110
+ else
111
+ relation.calculate(operation, column_name, options)
154
112
  end
155
113
  rescue ThrowResult
156
114
  0
157
115
  end
158
116
 
117
+ # Use <tt>pluck</tt> as a shortcut to select one or more attributes without
118
+ # loading a bunch of records just to grab the attributes you want.
119
+ #
120
+ # Person.pluck(:name)
121
+ #
122
+ # instead of
123
+ #
124
+ # Person.all.map(&:name)
125
+ #
126
+ # Pluck returns an <tt>Array</tt> of attribute values type-casted to match
127
+ # the plucked column names, if they can be deduced. Plucking an SQL fragment
128
+ # returns String values by default.
129
+ #
130
+ # Person.pluck(:id)
131
+ # # SELECT people.id FROM people
132
+ # # => [1, 2, 3]
133
+ #
134
+ # Person.pluck(:id, :name)
135
+ # # SELECT people.id, people.name FROM people
136
+ # # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
137
+ #
138
+ # Person.pluck('DISTINCT role')
139
+ # # SELECT DISTINCT role FROM people
140
+ # # => ['admin', 'member', 'guest']
141
+ #
142
+ # Person.where(age: 21).limit(5).pluck(:id)
143
+ # # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
144
+ # # => [2, 3]
145
+ #
146
+ # Person.pluck('DATEDIFF(updated_at, created_at)')
147
+ # # SELECT DATEDIFF(updated_at, created_at) FROM people
148
+ # # => ['0', '27761', '173']
149
+ #
150
+ def pluck(*column_names)
151
+ column_names.map! do |column_name|
152
+ if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s)
153
+ "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
154
+ else
155
+ column_name
156
+ end
157
+ end
158
+
159
+ if has_include?(column_names.first)
160
+ construct_relation_for_association_calculations.pluck(*column_names)
161
+ else
162
+ relation = spawn
163
+ relation.select_values = column_names
164
+ result = klass.connection.select_all(relation.arel, nil, bind_values)
165
+ columns = result.columns.map do |key|
166
+ klass.column_types.fetch(key) {
167
+ result.column_types.fetch(key) {
168
+ Class.new { def type_cast(v); v; end }.new
169
+ }
170
+ }
171
+ end
172
+
173
+ result = result.map do |attributes|
174
+ values = klass.initialize_attributes(attributes).values
175
+
176
+ columns.zip(values).map do |column, value|
177
+ column.type_cast(value)
178
+ end
179
+ end
180
+ columns.one? ? result.map!(&:first) : result
181
+ end
182
+ end
183
+
184
+ # Pluck all the ID's for the relation using the table's primary key
185
+ #
186
+ # Person.ids # SELECT people.id FROM people
187
+ # Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id
188
+ def ids
189
+ pluck primary_key
190
+ end
191
+
159
192
  private
160
193
 
194
+ def has_include?(column_name)
195
+ eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?))
196
+ end
197
+
161
198
  def perform_calculation(operation, column_name, options = {})
162
199
  operation = operation.to_s.downcase
163
200
 
164
- distinct = nil
201
+ # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
202
+ distinct = self.distinct_value
203
+ if options.has_key?(:distinct)
204
+ ActiveSupport::Deprecation.warn "The :distinct option for `Relation#count` is deprecated. " \
205
+ "Please use `Relation#distinct` instead. (eg. `relation.distinct.count`)"
206
+ distinct = options[:distinct]
207
+ end
165
208
 
166
209
  if operation == "count"
167
210
  column_name ||= (select_for_count || :all)
168
211
 
169
- if arel.joins(arel) =~ /LEFT OUTER/i
212
+ unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
170
213
  distinct = true
171
- column_name = @klass.primary_key if column_name == :all
172
214
  end
173
215
 
216
+ column_name = primary_key if column_name == :all && distinct
217
+
174
218
  distinct = nil if column_name =~ /\s*DISTINCT\s+/i
175
219
  end
176
220
 
177
- distinct = options[:distinct] || distinct
178
- column_name = :all if column_name.blank? && operation == "count"
221
+ if group_values.any?
222
+ execute_grouped_calculation(operation, column_name, distinct)
223
+ else
224
+ execute_simple_calculation(operation, column_name, distinct)
225
+ end
226
+ end
179
227
 
180
- if @group_values.any?
181
- return execute_grouped_calculation(operation, column_name)
228
+ def aggregate_column(column_name)
229
+ if @klass.column_names.include?(column_name.to_s)
230
+ Arel::Attribute.new(@klass.unscoped.table, column_name)
182
231
  else
183
- return execute_simple_calculation(operation, column_name, distinct)
232
+ Arel.sql(column_name == :all ? "*" : column_name.to_s)
184
233
  end
185
234
  end
186
235
 
236
+ def operation_over_aggregate_column(column, operation, distinct)
237
+ operation == 'count' ? column.count(distinct) : column.send(operation)
238
+ end
239
+
187
240
  def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
188
- column = if @klass.column_names.include?(column_name.to_s)
189
- Arel::Attribute.new(@klass.unscoped, column_name)
241
+ # Postgresql doesn't like ORDER BY when there are no GROUP BY
242
+ relation = reorder(nil)
243
+
244
+ column_alias = column_name
245
+
246
+ if operation == "count" && (relation.limit_value || relation.offset_value)
247
+ # Shortcut when limit is zero.
248
+ return 0 if relation.limit_value == 0
249
+
250
+ query_builder = build_count_subquery(relation, column_name, distinct)
190
251
  else
191
- Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s)
252
+ column = aggregate_column(column_name)
253
+
254
+ select_value = operation_over_aggregate_column(column, operation, distinct)
255
+
256
+ column_alias = select_value.alias
257
+ relation.select_values = [select_value]
258
+
259
+ query_builder = relation.arel
192
260
  end
193
261
 
194
- # Postgresql doesn't like ORDER BY when there are no GROUP BY
195
- relation = except(:order).select(operation == 'count' ? column.count(distinct) : column.send(operation))
196
- type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation)
262
+ result = @klass.connection.select_all(query_builder, nil, relation.bind_values)
263
+ row = result.first
264
+ value = row && row.values.first
265
+ column = result.column_types.fetch(column_alias) do
266
+ column_for(column_name)
267
+ end
268
+
269
+ type_cast_calculated_value(value, column, operation)
197
270
  end
198
271
 
199
- def execute_grouped_calculation(operation, column_name) #:nodoc:
200
- group_attr = @group_values.first
201
- association = @klass.reflect_on_association(group_attr.to_sym)
202
- associated = association && association.macro == :belongs_to # only count belongs_to associations
203
- group_field = associated ? association.primary_key_name : group_attr
204
- group_alias = column_alias_for(group_field)
205
- group_column = column_for(group_field)
272
+ def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
273
+ group_attrs = group_values
206
274
 
207
- group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field
275
+ if group_attrs.first.respond_to?(:to_sym)
276
+ association = @klass.reflect_on_association(group_attrs.first.to_sym)
277
+ associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
278
+ group_fields = Array(associated ? association.foreign_key : group_attrs)
279
+ else
280
+ group_fields = group_attrs
281
+ end
208
282
 
209
- aggregate_alias = column_alias_for(operation, column_name)
283
+ group_aliases = group_fields.map { |field|
284
+ column_alias_for(field)
285
+ }
286
+ group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
287
+ [aliaz, field]
288
+ }
210
289
 
211
- select_statement = if operation == 'count' && column_name == :all
212
- "COUNT(*) AS count_all"
290
+ group = group_fields
291
+
292
+ if operation == 'count' && column_name == :all
293
+ aggregate_alias = 'count_all'
213
294
  else
214
- Arel::Attribute.new(@klass.unscoped, column_name).send(operation).as(aggregate_alias).to_sql
295
+ aggregate_alias = column_alias_for([operation, column_name].join(' '))
215
296
  end
216
297
 
217
- select_statement << ", #{group_field} AS #{group_alias}"
298
+ select_values = [
299
+ operation_over_aggregate_column(
300
+ aggregate_column(column_name),
301
+ operation,
302
+ distinct).as(aggregate_alias)
303
+ ]
304
+ select_values += select_values unless having_values.empty?
305
+
306
+ select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
307
+ if field.respond_to?(:as)
308
+ field.as(aliaz)
309
+ else
310
+ "#{field} AS #{aliaz}"
311
+ end
312
+ }
218
313
 
219
- relation = except(:group).select(select_statement).group(group)
314
+ relation = except(:group)
315
+ relation.group_values = group
316
+ relation.select_values = select_values
220
317
 
221
- calculated_data = @klass.connection.select_all(relation.to_sql)
318
+ calculated_data = @klass.connection.select_all(relation, nil, bind_values)
222
319
 
223
320
  if association
224
- key_ids = calculated_data.collect { |row| row[group_alias] }
321
+ key_ids = calculated_data.collect { |row| row[group_aliases.first] }
225
322
  key_records = association.klass.base_class.find(key_ids)
226
- key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
323
+ key_records = Hash[key_records.map { |r| [r.id, r] }]
227
324
  end
228
325
 
229
- calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
230
- key = type_cast_calculated_value(row[group_alias], group_column)
231
- key = key_records[key] if associated
232
- value = row[aggregate_alias]
233
- all[key] = type_cast_calculated_value(value, column_for(column_name), operation)
234
- all
235
- end
326
+ Hash[calculated_data.map do |row|
327
+ key = group_columns.map { |aliaz, col_name|
328
+ column = calculated_data.column_types.fetch(aliaz) do
329
+ column_for(col_name)
330
+ end
331
+ type_cast_calculated_value(row[aliaz], column)
332
+ }
333
+ key = key.first if key.size == 1
334
+ key = key_records[key] if associated
335
+ [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
336
+ end]
236
337
  end
237
338
 
238
339
  # Converts the given keys to the value that the database adapter returns as
@@ -243,9 +344,12 @@ module ActiveRecord
243
344
  # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
244
345
  # column_alias_for("count(*)") # => "count_all"
245
346
  # column_alias_for("count", "id") # => "count_id"
246
- def column_alias_for(*keys)
247
- table_name = keys.join(' ')
248
- table_name.downcase!
347
+ def column_alias_for(keys)
348
+ if keys.respond_to? :name
349
+ keys = "#{keys.relation.name}.#{keys.name}"
350
+ end
351
+
352
+ table_name = keys.to_s.downcase
249
353
  table_name.gsub!(/\*/, 'all')
250
354
  table_name.gsub!(/\W+/, ' ')
251
355
  table_name.strip!
@@ -255,20 +359,16 @@ module ActiveRecord
255
359
  end
256
360
 
257
361
  def column_for(field)
258
- field_name = field.to_s.split('.').last
259
- @klass.columns.detect { |c| c.name.to_s == field_name }
362
+ field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
363
+ @klass.columns_hash[field_name]
260
364
  end
261
365
 
262
366
  def type_cast_calculated_value(value, column, operation = nil)
263
- if value.is_a?(String) || value.nil?
264
- case operation
265
- when 'count' then value.to_i
266
- when 'sum' then type_cast_using_column(value || '0', column)
267
- when 'average' then value.try(:to_d)
268
- else type_cast_using_column(value, column)
269
- end
270
- else
271
- value
367
+ case operation
368
+ when 'count' then value.to_i
369
+ when 'sum' then type_cast_using_column(value || 0, column)
370
+ when 'average' then value.respond_to?(:to_d) ? value.to_d : value
371
+ else type_cast_using_column(value, column)
272
372
  end
273
373
  end
274
374
 
@@ -277,10 +377,23 @@ module ActiveRecord
277
377
  end
278
378
 
279
379
  def select_for_count
280
- if @select_values.present?
281
- select = @select_values.join(", ")
282
- select if select !~ /(,|\*)/
380
+ if select_values.present?
381
+ select = select_values.join(", ")
382
+ select if select !~ /[,*]/
283
383
  end
284
384
  end
385
+
386
+ def build_count_subquery(relation, column_name, distinct)
387
+ column_alias = Arel.sql('count_column')
388
+ subquery_alias = Arel.sql('subquery_for_count')
389
+
390
+ aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
391
+ relation.select_values = [aliased_column]
392
+ subquery = relation.arel.as(subquery_alias)
393
+
394
+ sm = Arel::SelectManager.new relation.engine
395
+ select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
396
+ sm.project(select_value).from(subquery)
397
+ end
285
398
  end
286
399
  end