PageRenderTime 25ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/activerecord/lib/active_record/relation/calculations.rb

https://github.com/aviflombaum/rails
Ruby | 311 lines | 181 code | 46 blank | 84 comment | 45 complexity | 84df62038426d847280898b7bb6b1822 MD5 | raw file
  1. require 'active_support/core_ext/object/blank'
  2. require 'active_support/core_ext/object/try'
  3. module ActiveRecord
  4. module Calculations
  5. # Count the records.
  6. #
  7. # Person.count
  8. # # => the total count of all people
  9. #
  10. # Person.count(:age)
  11. # # => returns the total count of all people whose age is present in database
  12. #
  13. # Person.count(:all)
  14. # # => performs a COUNT(*) (:all is an alias for '*')
  15. #
  16. # Person.count(:age, distinct: true)
  17. # # => counts the number of different age values
  18. def count(column_name = nil, options = {})
  19. column_name, options = nil, column_name if column_name.is_a?(Hash)
  20. calculate(:count, column_name, options)
  21. end
  22. # Calculates the average value on a given column. Returns +nil+ if there's
  23. # no row. See +calculate+ for examples with options.
  24. #
  25. # Person.average('age') # => 35.8
  26. def average(column_name, options = {})
  27. calculate(:average, column_name, options)
  28. end
  29. # Calculates the minimum value on a given column. The value is returned
  30. # with the same data type of the column, or +nil+ if there's no row. See
  31. # +calculate+ for examples with options.
  32. #
  33. # Person.minimum('age') # => 7
  34. def minimum(column_name, options = {})
  35. calculate(:minimum, column_name, options)
  36. end
  37. # Calculates the maximum value on a given column. The value is returned
  38. # with the same data type of the column, or +nil+ if there's no row. See
  39. # +calculate+ for examples with options.
  40. #
  41. # Person.maximum('age') # => 93
  42. def maximum(column_name, options = {})
  43. calculate(:maximum, column_name, options)
  44. end
  45. # Calculates the sum of values on a given column. The value is returned
  46. # with the same data type of the column, 0 if there's no row. See
  47. # +calculate+ for examples with options.
  48. #
  49. # Person.sum('age') # => 4562
  50. def sum(*args)
  51. if block_given?
  52. self.to_a.sum(*args) {|*block_args| yield(*block_args)}
  53. else
  54. calculate(:sum, *args)
  55. end
  56. end
  57. # This calculates aggregate values in the given column. Methods for count, sum, average,
  58. # minimum, and maximum have been added as shortcuts.
  59. #
  60. # There are two basic forms of output:
  61. #
  62. # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
  63. # for AVG, and the given column's type for everything else.
  64. #
  65. # * Grouped values: This returns an ordered hash of the values and groups them. It
  66. # takes either a column name, or the name of a belongs_to association.
  67. #
  68. # values = Person.group('last_name').maximum(:age)
  69. # puts values["Drake"]
  70. # => 43
  71. #
  72. # drake = Family.find_by_last_name('Drake')
  73. # values = Person.group(:family).maximum(:age) # Person belongs_to :family
  74. # puts values[drake]
  75. # => 43
  76. #
  77. # values.each do |family, max_age|
  78. # ...
  79. # end
  80. #
  81. # Examples:
  82. # Person.calculate(:count, :all) # The same as Person.count
  83. # Person.average(:age) # SELECT AVG(age) FROM people...
  84. #
  85. # # Selects the minimum age for any family without any minors
  86. # Person.group(:last_name).having("min(age) > 17").minimum(:age)
  87. #
  88. # Person.sum("2 * age")
  89. def calculate(operation, column_name, options = {})
  90. relation = with_default_scope
  91. if relation.equal?(self)
  92. if eager_loading? || (includes_values.present? && references_eager_loaded_tables?)
  93. construct_relation_for_association_calculations.calculate(operation, column_name, options)
  94. else
  95. perform_calculation(operation, column_name, options)
  96. end
  97. else
  98. relation.calculate(operation, column_name, options)
  99. end
  100. rescue ThrowResult
  101. 0
  102. end
  103. # This method is designed to perform select by a single column as direct SQL query
  104. # Returns <tt>Array</tt> with values of the specified column name
  105. # The values has same data type as column.
  106. #
  107. # Examples:
  108. #
  109. # Person.pluck(:id) # SELECT people.id FROM people
  110. # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people
  111. # Person.where(:age => 21).limit(5).pluck(:id) # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
  112. #
  113. def pluck(column_name)
  114. key = column_name.to_s.split('.', 2).last
  115. if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s)
  116. column_name = "#{table_name}.#{column_name}"
  117. end
  118. result = klass.connection.select_all(select(column_name).arel, nil, bind_values)
  119. types = result.column_types.merge klass.column_types
  120. column = types[key]
  121. result.map do |attributes|
  122. value = klass.initialize_attributes(attributes)[key]
  123. if column
  124. column.type_cast value
  125. else
  126. value
  127. end
  128. end
  129. end
  130. private
  131. def perform_calculation(operation, column_name, options = {})
  132. operation = operation.to_s.downcase
  133. distinct = options[:distinct]
  134. if operation == "count"
  135. column_name ||= (select_for_count || :all)
  136. unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
  137. distinct = true
  138. end
  139. column_name = primary_key if column_name == :all && distinct
  140. distinct = nil if column_name =~ /\s*DISTINCT\s+/i
  141. end
  142. if group_values.any?
  143. execute_grouped_calculation(operation, column_name, distinct)
  144. else
  145. execute_simple_calculation(operation, column_name, distinct)
  146. end
  147. end
  148. def aggregate_column(column_name)
  149. if @klass.column_names.include?(column_name.to_s)
  150. Arel::Attribute.new(@klass.unscoped.table, column_name)
  151. else
  152. Arel.sql(column_name == :all ? "*" : column_name.to_s)
  153. end
  154. end
  155. def operation_over_aggregate_column(column, operation, distinct)
  156. operation == 'count' ? column.count(distinct) : column.send(operation)
  157. end
  158. def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
  159. # Postgresql doesn't like ORDER BY when there are no GROUP BY
  160. relation = reorder(nil)
  161. if operation == "count" && (relation.limit_value || relation.offset_value)
  162. # Shortcut when limit is zero.
  163. return 0 if relation.limit_value == 0
  164. query_builder = build_count_subquery(relation, column_name, distinct)
  165. else
  166. column = aggregate_column(column_name)
  167. select_value = operation_over_aggregate_column(column, operation, distinct)
  168. relation.select_values = [select_value]
  169. query_builder = relation.arel
  170. end
  171. result = @klass.connection.select_value(query_builder, nil, relation.bind_values)
  172. type_cast_calculated_value(result, column_for(column_name), operation)
  173. end
  174. def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
  175. group_attr = group_values
  176. association = @klass.reflect_on_association(group_attr.first.to_sym)
  177. associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
  178. group_fields = Array(associated ? association.foreign_key : group_attr)
  179. group_aliases = group_fields.map { |field| column_alias_for(field) }
  180. group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
  181. [aliaz, column_for(field)]
  182. }
  183. group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields
  184. if operation == 'count' && column_name == :all
  185. aggregate_alias = 'count_all'
  186. else
  187. aggregate_alias = column_alias_for(operation, column_name)
  188. end
  189. select_values = [
  190. operation_over_aggregate_column(
  191. aggregate_column(column_name),
  192. operation,
  193. distinct).as(aggregate_alias)
  194. ]
  195. select_values += select_values unless having_values.empty?
  196. select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
  197. "#{field} AS #{aliaz}"
  198. }
  199. relation = except(:group).group(group.join(','))
  200. relation.select_values = select_values
  201. calculated_data = @klass.connection.select_all(relation, nil, bind_values)
  202. if association
  203. key_ids = calculated_data.collect { |row| row[group_aliases.first] }
  204. key_records = association.klass.base_class.find(key_ids)
  205. key_records = Hash[key_records.map { |r| [r.id, r] }]
  206. end
  207. Hash[calculated_data.map do |row|
  208. key = group_columns.map { |aliaz, column|
  209. type_cast_calculated_value(row[aliaz], column)
  210. }
  211. key = key.first if key.size == 1
  212. key = key_records[key] if associated
  213. [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
  214. end]
  215. end
  216. # Converts the given keys to the value that the database adapter returns as
  217. # a usable column name:
  218. #
  219. # column_alias_for("users.id") # => "users_id"
  220. # column_alias_for("sum(id)") # => "sum_id"
  221. # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
  222. # column_alias_for("count(*)") # => "count_all"
  223. # column_alias_for("count", "id") # => "count_id"
  224. def column_alias_for(*keys)
  225. table_name = keys.join(' ')
  226. table_name.downcase!
  227. table_name.gsub!(/\*/, 'all')
  228. table_name.gsub!(/\W+/, ' ')
  229. table_name.strip!
  230. table_name.gsub!(/ +/, '_')
  231. @klass.connection.table_alias_for(table_name)
  232. end
  233. def column_for(field)
  234. field_name = field.to_s.split('.').last
  235. @klass.columns.detect { |c| c.name.to_s == field_name }
  236. end
  237. def type_cast_calculated_value(value, column, operation = nil)
  238. case operation
  239. when 'count' then value.to_i
  240. when 'sum' then type_cast_using_column(value || '0', column)
  241. when 'average' then value.respond_to?(:to_d) ? value.to_d : value
  242. else type_cast_using_column(value, column)
  243. end
  244. end
  245. def type_cast_using_column(value, column)
  246. column ? column.type_cast(value) : value
  247. end
  248. def select_for_count
  249. if select_values.present?
  250. select = select_values.join(", ")
  251. select if select !~ /[,*]/
  252. end
  253. end
  254. def build_count_subquery(relation, column_name, distinct)
  255. column_alias = Arel.sql('count_column')
  256. subquery_alias = Arel.sql('subquery_for_count')
  257. aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
  258. relation.select_values = [aliased_column]
  259. subquery = relation.arel.as(subquery_alias)
  260. sm = Arel::SelectManager.new relation.engine
  261. select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
  262. sm.project(select_value).from(subquery)
  263. end
  264. end
  265. end