sskirby-activerecord 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. data/CHANGELOG.md +6749 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +222 -0
  4. data/examples/associations.png +0 -0
  5. data/examples/performance.rb +177 -0
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record.rb +147 -0
  8. data/lib/active_record/aggregations.rb +255 -0
  9. data/lib/active_record/associations.rb +1604 -0
  10. data/lib/active_record/associations/alias_tracker.rb +79 -0
  11. data/lib/active_record/associations/association.rb +239 -0
  12. data/lib/active_record/associations/association_scope.rb +119 -0
  13. data/lib/active_record/associations/belongs_to_association.rb +79 -0
  14. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +34 -0
  15. data/lib/active_record/associations/builder/association.rb +55 -0
  16. data/lib/active_record/associations/builder/belongs_to.rb +85 -0
  17. data/lib/active_record/associations/builder/collection_association.rb +75 -0
  18. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +57 -0
  19. data/lib/active_record/associations/builder/has_many.rb +71 -0
  20. data/lib/active_record/associations/builder/has_one.rb +62 -0
  21. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  22. data/lib/active_record/associations/collection_association.rb +574 -0
  23. data/lib/active_record/associations/collection_proxy.rb +132 -0
  24. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +62 -0
  25. data/lib/active_record/associations/has_many_association.rb +108 -0
  26. data/lib/active_record/associations/has_many_through_association.rb +180 -0
  27. data/lib/active_record/associations/has_one_association.rb +73 -0
  28. data/lib/active_record/associations/has_one_through_association.rb +36 -0
  29. data/lib/active_record/associations/join_dependency.rb +214 -0
  30. data/lib/active_record/associations/join_dependency/join_association.rb +154 -0
  31. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  32. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  33. data/lib/active_record/associations/join_helper.rb +55 -0
  34. data/lib/active_record/associations/preloader.rb +177 -0
  35. data/lib/active_record/associations/preloader/association.rb +127 -0
  36. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  37. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  38. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  39. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  40. data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
  41. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  42. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  43. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  44. data/lib/active_record/associations/preloader/through_association.rb +67 -0
  45. data/lib/active_record/associations/singular_association.rb +64 -0
  46. data/lib/active_record/associations/through_association.rb +83 -0
  47. data/lib/active_record/attribute_assignment.rb +221 -0
  48. data/lib/active_record/attribute_methods.rb +272 -0
  49. data/lib/active_record/attribute_methods/before_type_cast.rb +31 -0
  50. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  51. data/lib/active_record/attribute_methods/dirty.rb +101 -0
  52. data/lib/active_record/attribute_methods/primary_key.rb +114 -0
  53. data/lib/active_record/attribute_methods/query.rb +39 -0
  54. data/lib/active_record/attribute_methods/read.rb +135 -0
  55. data/lib/active_record/attribute_methods/serialization.rb +93 -0
  56. data/lib/active_record/attribute_methods/time_zone_conversion.rb +62 -0
  57. data/lib/active_record/attribute_methods/write.rb +69 -0
  58. data/lib/active_record/autosave_association.rb +422 -0
  59. data/lib/active_record/base.rb +716 -0
  60. data/lib/active_record/callbacks.rb +275 -0
  61. data/lib/active_record/coders/yaml_column.rb +41 -0
  62. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +452 -0
  63. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +188 -0
  64. data/lib/active_record/connection_adapters/abstract/database_limits.rb +58 -0
  65. data/lib/active_record/connection_adapters/abstract/database_statements.rb +388 -0
  66. data/lib/active_record/connection_adapters/abstract/query_cache.rb +82 -0
  67. data/lib/active_record/connection_adapters/abstract/quoting.rb +115 -0
  68. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +492 -0
  69. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +598 -0
  70. data/lib/active_record/connection_adapters/abstract_adapter.rb +296 -0
  71. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
  72. data/lib/active_record/connection_adapters/column.rb +270 -0
  73. data/lib/active_record/connection_adapters/mysql2_adapter.rb +288 -0
  74. data/lib/active_record/connection_adapters/mysql_adapter.rb +426 -0
  75. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1261 -0
  76. data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
  77. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -0
  78. data/lib/active_record/connection_adapters/sqlite_adapter.rb +577 -0
  79. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  80. data/lib/active_record/counter_cache.rb +119 -0
  81. data/lib/active_record/dynamic_finder_match.rb +56 -0
  82. data/lib/active_record/dynamic_matchers.rb +79 -0
  83. data/lib/active_record/dynamic_scope_match.rb +23 -0
  84. data/lib/active_record/errors.rb +195 -0
  85. data/lib/active_record/explain.rb +85 -0
  86. data/lib/active_record/explain_subscriber.rb +21 -0
  87. data/lib/active_record/fixtures.rb +906 -0
  88. data/lib/active_record/fixtures/file.rb +65 -0
  89. data/lib/active_record/identity_map.rb +156 -0
  90. data/lib/active_record/inheritance.rb +167 -0
  91. data/lib/active_record/integration.rb +49 -0
  92. data/lib/active_record/locale/en.yml +40 -0
  93. data/lib/active_record/locking/optimistic.rb +183 -0
  94. data/lib/active_record/locking/pessimistic.rb +77 -0
  95. data/lib/active_record/log_subscriber.rb +68 -0
  96. data/lib/active_record/migration.rb +765 -0
  97. data/lib/active_record/migration/command_recorder.rb +105 -0
  98. data/lib/active_record/model_schema.rb +366 -0
  99. data/lib/active_record/nested_attributes.rb +469 -0
  100. data/lib/active_record/observer.rb +121 -0
  101. data/lib/active_record/persistence.rb +372 -0
  102. data/lib/active_record/query_cache.rb +74 -0
  103. data/lib/active_record/querying.rb +58 -0
  104. data/lib/active_record/railtie.rb +119 -0
  105. data/lib/active_record/railties/console_sandbox.rb +6 -0
  106. data/lib/active_record/railties/controller_runtime.rb +49 -0
  107. data/lib/active_record/railties/databases.rake +620 -0
  108. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  109. data/lib/active_record/readonly_attributes.rb +26 -0
  110. data/lib/active_record/reflection.rb +534 -0
  111. data/lib/active_record/relation.rb +534 -0
  112. data/lib/active_record/relation/batches.rb +90 -0
  113. data/lib/active_record/relation/calculations.rb +354 -0
  114. data/lib/active_record/relation/delegation.rb +49 -0
  115. data/lib/active_record/relation/finder_methods.rb +398 -0
  116. data/lib/active_record/relation/predicate_builder.rb +58 -0
  117. data/lib/active_record/relation/query_methods.rb +417 -0
  118. data/lib/active_record/relation/spawn_methods.rb +148 -0
  119. data/lib/active_record/result.rb +34 -0
  120. data/lib/active_record/sanitization.rb +194 -0
  121. data/lib/active_record/schema.rb +58 -0
  122. data/lib/active_record/schema_dumper.rb +204 -0
  123. data/lib/active_record/scoping.rb +152 -0
  124. data/lib/active_record/scoping/default.rb +142 -0
  125. data/lib/active_record/scoping/named.rb +202 -0
  126. data/lib/active_record/serialization.rb +18 -0
  127. data/lib/active_record/serializers/xml_serializer.rb +202 -0
  128. data/lib/active_record/session_store.rb +358 -0
  129. data/lib/active_record/store.rb +50 -0
  130. data/lib/active_record/test_case.rb +73 -0
  131. data/lib/active_record/timestamp.rb +113 -0
  132. data/lib/active_record/transactions.rb +360 -0
  133. data/lib/active_record/translation.rb +22 -0
  134. data/lib/active_record/validations.rb +83 -0
  135. data/lib/active_record/validations/associated.rb +43 -0
  136. data/lib/active_record/validations/uniqueness.rb +180 -0
  137. data/lib/active_record/version.rb +10 -0
  138. data/lib/rails/generators/active_record.rb +25 -0
  139. data/lib/rails/generators/active_record/migration.rb +15 -0
  140. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  141. data/lib/rails/generators/active_record/migration/templates/migration.rb +31 -0
  142. data/lib/rails/generators/active_record/model/model_generator.rb +43 -0
  143. data/lib/rails/generators/active_record/model/templates/migration.rb +15 -0
  144. data/lib/rails/generators/active_record/model/templates/model.rb +7 -0
  145. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  146. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  147. data/lib/rails/generators/active_record/observer/templates/observer.rb +4 -0
  148. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +25 -0
  149. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +12 -0
  150. metadata +242 -0
@@ -0,0 +1,90 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
3
+ module ActiveRecord
4
+ module Batches
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).
8
+ #
9
+ # Example:
10
+ #
11
+ # Person.where("age > 21").find_each do |person|
12
+ # person.party_all_night!
13
+ # end
14
+ #
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.
19
+ def find_each(options = {})
20
+ find_in_batches(options) do |records|
21
+ records.each { |record| yield record }
22
+ end
23
+ end
24
+
25
+ # Yields each batch of records that was found by the find +options+ as
26
+ # an array. The size of each batch is set by the <tt>:batch_size</tt>
27
+ # option; the default is 1000.
28
+ #
29
+ # You can control the starting point for the batch processing by
30
+ # supplying the <tt>:start</tt> option. This is especially useful if you
31
+ # want multiple workers dealing with the same processing queue. You can
32
+ # make worker 1 handle all the records between id 0 and 10,000 and
33
+ # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt>
34
+ # option on that worker).
35
+ #
36
+ # It's not possible to set the order. That is automatically set to
37
+ # ascending on the primary key ("id ASC") to make the batch ordering
38
+ # work. This also mean that this method only works with integer-based
39
+ # primary keys. You can't set the limit either, that's used to control
40
+ # the batch sizes.
41
+ #
42
+ # Example:
43
+ #
44
+ # Person.where("age > 21").find_in_batches do |group|
45
+ # sleep(50) # Make sure it doesn't get too crowded in there!
46
+ # group.each { |person| person.party_all_night! }
47
+ # end
48
+ def find_in_batches(options = {})
49
+ relation = self
50
+
51
+ unless arel.orders.blank? && arel.taken.blank?
52
+ ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
53
+ end
54
+
55
+ if (finder_options = options.except(:start, :batch_size)).present?
56
+ raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order].present?
57
+ raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit].present?
58
+
59
+ relation = apply_finder_options(finder_options)
60
+ end
61
+
62
+ start = options.delete(:start).to_i
63
+ batch_size = options.delete(:batch_size) || 1000
64
+
65
+ relation = relation.reorder(batch_order).limit(batch_size)
66
+ records = relation.where(table[primary_key].gteq(start)).all
67
+
68
+ while records.any?
69
+ records_size = records.size
70
+ primary_key_offset = records.last.id
71
+
72
+ yield records
73
+
74
+ break if records_size < batch_size
75
+
76
+ if primary_key_offset
77
+ records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
78
+ else
79
+ raise "Primary key not included in the custom select clause"
80
+ end
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def batch_order
87
+ "#{quoted_table_name}.#{quoted_primary_key} ASC"
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,354 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require 'active_support/core_ext/object/try'
3
+
4
+ module ActiveRecord
5
+ 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
37
+ #
38
+ # Examples for counting by column:
39
+ # Person.count(:age) # returns the total count of all people whose age is present in database
40
+ #
41
+ # Examples for count with options:
42
+ # Person.count(:conditions => "age > 26")
43
+ #
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)
46
+ #
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")
50
+ #
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 '*')
53
+ #
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.
56
+ def count(column_name = nil, options = {})
57
+ column_name, options = nil, column_name if column_name.is_a?(Hash)
58
+ calculate(:count, column_name, options)
59
+ end
60
+
61
+ # Calculates the average value on a given column. Returns +nil+ if there's
62
+ # no row. See +calculate+ for examples with options.
63
+ #
64
+ # Person.average('age') # => 35.8
65
+ def average(column_name, options = {})
66
+ calculate(:average, column_name, options)
67
+ end
68
+
69
+ # Calculates the minimum value on a given column. The value is returned
70
+ # with the same data type of the column, or +nil+ if there's no row. See
71
+ # +calculate+ for examples with options.
72
+ #
73
+ # Person.minimum('age') # => 7
74
+ def minimum(column_name, options = {})
75
+ calculate(:minimum, column_name, options)
76
+ end
77
+
78
+ # Calculates the maximum value on a given column. The value is returned
79
+ # with the same data type of the column, or +nil+ if there's no row. See
80
+ # +calculate+ for examples with options.
81
+ #
82
+ # Person.maximum('age') # => 93
83
+ def maximum(column_name, options = {})
84
+ calculate(:maximum, column_name, options)
85
+ end
86
+
87
+ # Calculates the sum of values on a given column. The value is returned
88
+ # with the same data type of the column, 0 if there's no row. See
89
+ # +calculate+ for examples with options.
90
+ #
91
+ # Person.sum('age') # => 4562
92
+ def sum(*args)
93
+ if block_given?
94
+ self.to_a.sum(*args) {|*block_args| yield(*block_args)}
95
+ else
96
+ calculate(:sum, *args)
97
+ end
98
+ end
99
+
100
+ # This calculates aggregate values in the given column. Methods for count, sum, average,
101
+ # minimum, and maximum have been added as shortcuts. Options such as <tt>:conditions</tt>,
102
+ # <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query.
103
+ #
104
+ # There are two basic forms of output:
105
+ # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
106
+ # for AVG, and the given column's type for everything else.
107
+ # * Grouped values: This returns an ordered hash of the values and groups them by the
108
+ # <tt>:group</tt> option. It takes either a column name, or the name of a belongs_to association.
109
+ #
110
+ # values = Person.maximum(:age, :group => 'last_name')
111
+ # puts values["Drake"]
112
+ # => 43
113
+ #
114
+ # drake = Family.find_by_last_name('Drake')
115
+ # values = Person.maximum(:age, :group => :family) # Person belongs_to :family
116
+ # puts values[drake]
117
+ # => 43
118
+ #
119
+ # values.each do |family, max_age|
120
+ # ...
121
+ # end
122
+ #
123
+ # Options:
124
+ # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
125
+ # See conditions in the intro to ActiveRecord::Base.
126
+ # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything,
127
+ # the purpose of this is to access fields on joined tables in your conditions, order, or group clauses.
128
+ # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id".
129
+ # (Rarely needed).
130
+ # The records will be returned read-only since they will have attributes that do not correspond to the
131
+ # table's columns.
132
+ # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
133
+ # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
134
+ # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example
135
+ # want to do a join, but not include the joined columns.
136
+ # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as
137
+ # SELECT COUNT(DISTINCT posts.id) ...
138
+ #
139
+ # Examples:
140
+ # Person.calculate(:count, :all) # The same as Person.count
141
+ # Person.average(:age) # SELECT AVG(age) FROM people...
142
+ # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for
143
+ # # everyone with a last name other than 'Drake'
144
+ #
145
+ # # Selects the minimum age for any family without any minors
146
+ # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name)
147
+ #
148
+ # Person.sum("2 * age")
149
+ def calculate(operation, column_name, options = {})
150
+ if options.except(:distinct).present?
151
+ apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct])
152
+ else
153
+ relation = with_default_scope
154
+
155
+ if relation.equal?(self)
156
+ if eager_loading? || (includes_values.present? && references_eager_loaded_tables?)
157
+ construct_relation_for_association_calculations.calculate(operation, column_name, options)
158
+ else
159
+ perform_calculation(operation, column_name, options)
160
+ end
161
+ else
162
+ relation.calculate(operation, column_name, options)
163
+ end
164
+ end
165
+ rescue ThrowResult
166
+ 0
167
+ end
168
+
169
+ # This method is designed to perform select by a single column as direct SQL query
170
+ # Returns <tt>Array</tt> with values of the specified column name
171
+ # The values has same data type as column.
172
+ #
173
+ # Examples:
174
+ #
175
+ # Person.pluck(:id) # SELECT people.id FROM people
176
+ # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people
177
+ # Person.where(:confirmed => true).limit(5).pluck(:id)
178
+ #
179
+ def pluck(column_name)
180
+ column_name = column_name.to_s
181
+ klass.connection.select_all(select(column_name).arel).map! do |attributes|
182
+ klass.type_cast_attribute(attributes.keys.first, klass.initialize_attributes(attributes))
183
+ end
184
+ end
185
+
186
+ private
187
+
188
+ def perform_calculation(operation, column_name, options = {})
189
+ operation = operation.to_s.downcase
190
+
191
+ distinct = options[:distinct]
192
+
193
+ if operation == "count"
194
+ column_name ||= (select_for_count || :all)
195
+
196
+ unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
197
+ distinct = true
198
+ end
199
+
200
+ column_name = primary_key if column_name == :all && distinct
201
+
202
+ distinct = nil if column_name =~ /\s*DISTINCT\s+/i
203
+ end
204
+
205
+ if @group_values.any?
206
+ execute_grouped_calculation(operation, column_name, distinct)
207
+ else
208
+ execute_simple_calculation(operation, column_name, distinct)
209
+ end
210
+ end
211
+
212
+ def aggregate_column(column_name)
213
+ if @klass.column_names.include?(column_name.to_s)
214
+ Arel::Attribute.new(@klass.unscoped.table, column_name)
215
+ else
216
+ Arel.sql(column_name == :all ? "*" : column_name.to_s)
217
+ end
218
+ end
219
+
220
+ def operation_over_aggregate_column(column, operation, distinct)
221
+ operation == 'count' ? column.count(distinct) : column.send(operation)
222
+ end
223
+
224
+ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
225
+ # Postgresql doesn't like ORDER BY when there are no GROUP BY
226
+ relation = reorder(nil)
227
+
228
+ if operation == "count" && (relation.limit_value || relation.offset_value)
229
+ # Shortcut when limit is zero.
230
+ return 0 if relation.limit_value == 0
231
+
232
+ query_builder = build_count_subquery(relation, column_name, distinct)
233
+ else
234
+ column = aggregate_column(column_name)
235
+
236
+ select_value = operation_over_aggregate_column(column, operation, distinct)
237
+
238
+ relation.select_values = [select_value]
239
+
240
+ query_builder = relation.arel
241
+ end
242
+
243
+ type_cast_calculated_value(@klass.connection.select_value(query_builder), column_for(column_name), operation)
244
+ end
245
+
246
+ def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
247
+ group_attr = @group_values
248
+ association = @klass.reflect_on_association(group_attr.first.to_sym)
249
+ associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
250
+ group_fields = Array(associated ? association.foreign_key : group_attr)
251
+ group_aliases = group_fields.map { |field| column_alias_for(field) }
252
+ group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
253
+ [aliaz, column_for(field)]
254
+ }
255
+
256
+ group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields
257
+
258
+ if operation == 'count' && column_name == :all
259
+ aggregate_alias = 'count_all'
260
+ else
261
+ aggregate_alias = column_alias_for(operation, column_name)
262
+ end
263
+
264
+ select_values = [
265
+ operation_over_aggregate_column(
266
+ aggregate_column(column_name),
267
+ operation,
268
+ distinct).as(aggregate_alias)
269
+ ]
270
+ select_values += @select_values unless @having_values.empty?
271
+
272
+ select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
273
+ "#{field} AS #{aliaz}"
274
+ }
275
+
276
+ relation = except(:group).group(group.join(','))
277
+ relation.select_values = select_values
278
+
279
+ calculated_data = @klass.connection.select_all(relation)
280
+
281
+ if association
282
+ key_ids = calculated_data.collect { |row| row[group_aliases.first] }
283
+ key_records = association.klass.base_class.find(key_ids)
284
+ key_records = Hash[key_records.map { |r| [r.id, r] }]
285
+ end
286
+
287
+ ActiveSupport::OrderedHash[calculated_data.map do |row|
288
+ key = group_columns.map { |aliaz, column|
289
+ type_cast_calculated_value(row[aliaz], column)
290
+ }
291
+ key = key.first if key.size == 1
292
+ key = key_records[key] if associated
293
+ [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
294
+ end]
295
+ end
296
+
297
+ # Converts the given keys to the value that the database adapter returns as
298
+ # a usable column name:
299
+ #
300
+ # column_alias_for("users.id") # => "users_id"
301
+ # column_alias_for("sum(id)") # => "sum_id"
302
+ # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
303
+ # column_alias_for("count(*)") # => "count_all"
304
+ # column_alias_for("count", "id") # => "count_id"
305
+ def column_alias_for(*keys)
306
+ table_name = keys.join(' ')
307
+ table_name.downcase!
308
+ table_name.gsub!(/\*/, 'all')
309
+ table_name.gsub!(/\W+/, ' ')
310
+ table_name.strip!
311
+ table_name.gsub!(/ +/, '_')
312
+
313
+ @klass.connection.table_alias_for(table_name)
314
+ end
315
+
316
+ def column_for(field)
317
+ field_name = field.to_s.split('.').last
318
+ @klass.columns.detect { |c| c.name.to_s == field_name }
319
+ end
320
+
321
+ def type_cast_calculated_value(value, column, operation = nil)
322
+ case operation
323
+ when 'count' then value.to_i
324
+ when 'sum' then type_cast_using_column(value || '0', column)
325
+ when 'average' then value.respond_to?(:to_d) ? value.to_d : value
326
+ else type_cast_using_column(value, column)
327
+ end
328
+ end
329
+
330
+ def type_cast_using_column(value, column)
331
+ column ? column.type_cast(value) : value
332
+ end
333
+
334
+ def select_for_count
335
+ if @select_values.present?
336
+ select = @select_values.join(", ")
337
+ select if select !~ /(,|\*)/
338
+ end
339
+ end
340
+
341
+ def build_count_subquery(relation, column_name, distinct)
342
+ column_alias = Arel.sql('count_column')
343
+ subquery_alias = Arel.sql('subquery_for_count')
344
+
345
+ aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
346
+ relation.select_values = [aliased_column]
347
+ subquery = relation.arel.as(subquery_alias)
348
+
349
+ sm = Arel::SelectManager.new relation.engine
350
+ select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
351
+ sm.project(select_value).from(subquery)
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,49 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module ActiveRecord
4
+ module Delegation
5
+ # Set up common delegations for performance (avoids method_missing)
6
+ delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
7
+ delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
8
+ :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass
9
+
10
+ def self.delegate_to_scoped_klass(method)
11
+ if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
12
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
13
+ def #{method}(*args, &block)
14
+ scoping { @klass.#{method}(*args, &block) }
15
+ end
16
+ RUBY
17
+ else
18
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
19
+ def #{method}(*args, &block)
20
+ scoping { @klass.send(#{method.inspect}, *args, &block) }
21
+ end
22
+ RUBY
23
+ end
24
+ end
25
+
26
+ def respond_to?(method, include_private = false)
27
+ super || Array.method_defined?(method) ||
28
+ @klass.respond_to?(method, include_private) ||
29
+ arel.respond_to?(method, include_private)
30
+ end
31
+
32
+ protected
33
+
34
+ def method_missing(method, *args, &block)
35
+ if Array.method_defined?(method)
36
+ ::ActiveRecord::Delegation.delegate method, :to => :to_a
37
+ to_a.send(method, *args, &block)
38
+ elsif @klass.respond_to?(method)
39
+ ::ActiveRecord::Delegation.delegate_to_scoped_klass(method)
40
+ scoping { @klass.send(method, *args, &block) }
41
+ elsif arel.respond_to?(method)
42
+ ::ActiveRecord::Delegation.delegate method, :to => :arel
43
+ arel.send(method, *args, &block)
44
+ else
45
+ super
46
+ end
47
+ end
48
+ end
49
+ end