activerecord 1.0.0 → 3.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 (178) hide show
  1. data/CHANGELOG +5518 -76
  2. data/README.rdoc +222 -0
  3. data/examples/performance.rb +162 -0
  4. data/examples/simple.rb +14 -0
  5. data/lib/active_record/aggregations.rb +192 -80
  6. data/lib/active_record/association_preload.rb +403 -0
  7. data/lib/active_record/associations/association_collection.rb +545 -53
  8. data/lib/active_record/associations/association_proxy.rb +295 -0
  9. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +78 -0
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +127 -36
  12. data/lib/active_record/associations/has_many_association.rb +108 -84
  13. data/lib/active_record/associations/has_many_through_association.rb +116 -0
  14. data/lib/active_record/associations/has_one_association.rb +143 -0
  15. data/lib/active_record/associations/has_one_through_association.rb +40 -0
  16. data/lib/active_record/associations/through_association_scope.rb +154 -0
  17. data/lib/active_record/associations.rb +2086 -368
  18. data/lib/active_record/attribute_methods/before_type_cast.rb +33 -0
  19. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  20. data/lib/active_record/attribute_methods/primary_key.rb +50 -0
  21. data/lib/active_record/attribute_methods/query.rb +39 -0
  22. data/lib/active_record/attribute_methods/read.rb +116 -0
  23. data/lib/active_record/attribute_methods/time_zone_conversion.rb +61 -0
  24. data/lib/active_record/attribute_methods/write.rb +37 -0
  25. data/lib/active_record/attribute_methods.rb +60 -0
  26. data/lib/active_record/autosave_association.rb +369 -0
  27. data/lib/active_record/base.rb +1603 -721
  28. data/lib/active_record/callbacks.rb +176 -225
  29. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +365 -0
  30. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
  31. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  32. data/lib/active_record/connection_adapters/abstract/database_statements.rb +329 -0
  33. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  34. data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -0
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
  36. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +543 -0
  37. data/lib/active_record/connection_adapters/abstract_adapter.rb +165 -279
  38. data/lib/active_record/connection_adapters/mysql_adapter.rb +594 -82
  39. data/lib/active_record/connection_adapters/postgresql_adapter.rb +988 -135
  40. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -0
  41. data/lib/active_record/connection_adapters/sqlite_adapter.rb +365 -71
  42. data/lib/active_record/counter_cache.rb +115 -0
  43. data/lib/active_record/dynamic_finder_match.rb +53 -0
  44. data/lib/active_record/dynamic_scope_match.rb +32 -0
  45. data/lib/active_record/errors.rb +172 -0
  46. data/lib/active_record/fixtures.rb +941 -105
  47. data/lib/active_record/locale/en.yml +40 -0
  48. data/lib/active_record/locking/optimistic.rb +172 -0
  49. data/lib/active_record/locking/pessimistic.rb +55 -0
  50. data/lib/active_record/log_subscriber.rb +48 -0
  51. data/lib/active_record/migration.rb +617 -0
  52. data/lib/active_record/named_scope.rb +138 -0
  53. data/lib/active_record/nested_attributes.rb +417 -0
  54. data/lib/active_record/observer.rb +105 -36
  55. data/lib/active_record/persistence.rb +291 -0
  56. data/lib/active_record/query_cache.rb +36 -0
  57. data/lib/active_record/railtie.rb +91 -0
  58. data/lib/active_record/railties/controller_runtime.rb +38 -0
  59. data/lib/active_record/railties/databases.rake +512 -0
  60. data/lib/active_record/reflection.rb +364 -87
  61. data/lib/active_record/relation/batches.rb +89 -0
  62. data/lib/active_record/relation/calculations.rb +286 -0
  63. data/lib/active_record/relation/finder_methods.rb +355 -0
  64. data/lib/active_record/relation/predicate_builder.rb +41 -0
  65. data/lib/active_record/relation/query_methods.rb +261 -0
  66. data/lib/active_record/relation/spawn_methods.rb +112 -0
  67. data/lib/active_record/relation.rb +393 -0
  68. data/lib/active_record/schema.rb +59 -0
  69. data/lib/active_record/schema_dumper.rb +195 -0
  70. data/lib/active_record/serialization.rb +60 -0
  71. data/lib/active_record/serializers/xml_serializer.rb +244 -0
  72. data/lib/active_record/session_store.rb +340 -0
  73. data/lib/active_record/test_case.rb +67 -0
  74. data/lib/active_record/timestamp.rb +88 -0
  75. data/lib/active_record/transactions.rb +329 -75
  76. data/lib/active_record/validations/associated.rb +48 -0
  77. data/lib/active_record/validations/uniqueness.rb +185 -0
  78. data/lib/active_record/validations.rb +58 -179
  79. data/lib/active_record/version.rb +9 -0
  80. data/lib/active_record.rb +100 -24
  81. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  82. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  83. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  84. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  85. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  86. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  87. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  88. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  89. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  90. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  91. data/lib/rails/generators/active_record.rb +27 -0
  92. metadata +216 -158
  93. data/README +0 -361
  94. data/RUNNING_UNIT_TESTS +0 -36
  95. data/dev-utils/eval_debugger.rb +0 -9
  96. data/examples/associations.rb +0 -87
  97. data/examples/shared_setup.rb +0 -15
  98. data/examples/validation.rb +0 -88
  99. data/install.rb +0 -60
  100. data/lib/active_record/deprecated_associations.rb +0 -70
  101. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  102. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  103. data/lib/active_record/support/clean_logger.rb +0 -10
  104. data/lib/active_record/support/inflector.rb +0 -70
  105. data/lib/active_record/vendor/mysql.rb +0 -1117
  106. data/lib/active_record/vendor/simple.rb +0 -702
  107. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  108. data/lib/active_record/wrappings.rb +0 -59
  109. data/rakefile +0 -122
  110. data/test/abstract_unit.rb +0 -16
  111. data/test/aggregations_test.rb +0 -34
  112. data/test/all.sh +0 -8
  113. data/test/associations_test.rb +0 -477
  114. data/test/base_test.rb +0 -513
  115. data/test/class_inheritable_attributes_test.rb +0 -33
  116. data/test/connections/native_mysql/connection.rb +0 -24
  117. data/test/connections/native_postgresql/connection.rb +0 -24
  118. data/test/connections/native_sqlite/connection.rb +0 -24
  119. data/test/deprecated_associations_test.rb +0 -336
  120. data/test/finder_test.rb +0 -67
  121. data/test/fixtures/accounts/signals37 +0 -3
  122. data/test/fixtures/accounts/unknown +0 -2
  123. data/test/fixtures/auto_id.rb +0 -4
  124. data/test/fixtures/column_name.rb +0 -3
  125. data/test/fixtures/companies/first_client +0 -6
  126. data/test/fixtures/companies/first_firm +0 -4
  127. data/test/fixtures/companies/second_client +0 -6
  128. data/test/fixtures/company.rb +0 -37
  129. data/test/fixtures/company_in_module.rb +0 -33
  130. data/test/fixtures/course.rb +0 -3
  131. data/test/fixtures/courses/java +0 -2
  132. data/test/fixtures/courses/ruby +0 -2
  133. data/test/fixtures/customer.rb +0 -30
  134. data/test/fixtures/customers/david +0 -6
  135. data/test/fixtures/db_definitions/mysql.sql +0 -96
  136. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  137. data/test/fixtures/db_definitions/postgresql.sql +0 -113
  138. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  139. data/test/fixtures/db_definitions/sqlite.sql +0 -85
  140. data/test/fixtures/db_definitions/sqlite2.sql +0 -4
  141. data/test/fixtures/default.rb +0 -2
  142. data/test/fixtures/developer.rb +0 -8
  143. data/test/fixtures/developers/david +0 -2
  144. data/test/fixtures/developers/jamis +0 -2
  145. data/test/fixtures/developers_projects/david_action_controller +0 -2
  146. data/test/fixtures/developers_projects/david_active_record +0 -2
  147. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  148. data/test/fixtures/entrant.rb +0 -3
  149. data/test/fixtures/entrants/first +0 -3
  150. data/test/fixtures/entrants/second +0 -3
  151. data/test/fixtures/entrants/third +0 -3
  152. data/test/fixtures/fixture_database.sqlite +0 -0
  153. data/test/fixtures/fixture_database_2.sqlite +0 -0
  154. data/test/fixtures/movie.rb +0 -5
  155. data/test/fixtures/movies/first +0 -2
  156. data/test/fixtures/movies/second +0 -2
  157. data/test/fixtures/project.rb +0 -3
  158. data/test/fixtures/projects/action_controller +0 -2
  159. data/test/fixtures/projects/active_record +0 -2
  160. data/test/fixtures/reply.rb +0 -21
  161. data/test/fixtures/subscriber.rb +0 -5
  162. data/test/fixtures/subscribers/first +0 -2
  163. data/test/fixtures/subscribers/second +0 -2
  164. data/test/fixtures/topic.rb +0 -20
  165. data/test/fixtures/topics/first +0 -9
  166. data/test/fixtures/topics/second +0 -8
  167. data/test/fixtures_test.rb +0 -20
  168. data/test/inflector_test.rb +0 -104
  169. data/test/inheritance_test.rb +0 -125
  170. data/test/lifecycle_test.rb +0 -110
  171. data/test/modules_test.rb +0 -21
  172. data/test/multiple_db_test.rb +0 -46
  173. data/test/pk_test.rb +0 -57
  174. data/test/reflection_test.rb +0 -78
  175. data/test/thread_safety_test.rb +0 -33
  176. data/test/transactions_test.rb +0 -83
  177. data/test/unconnected_test.rb +0 -24
  178. data/test/validations_test.rb +0 -126
@@ -0,0 +1,286 @@
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(column_name, options = {})
93
+ calculate(:sum, column_name, options)
94
+ end
95
+
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.
99
+ #
100
+ # There are two basic forms of output:
101
+ # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
102
+ # 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
+ #
106
+ # values = Person.maximum(:age, :group => 'last_name')
107
+ # puts values["Drake"]
108
+ # => 43
109
+ #
110
+ # drake = Family.find_by_last_name('Drake')
111
+ # values = Person.maximum(:age, :group => :family) # Person belongs_to :family
112
+ # puts values[drake]
113
+ # => 43
114
+ #
115
+ # values.each do |family, max_age|
116
+ # ...
117
+ # end
118
+ #
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
+ # Person.calculate(:count, :all) # The same as Person.count
137
+ # 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
+ #
141
+ # # Selects the minimum age for any family without any minors
142
+ # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name)
143
+ #
144
+ # Person.sum("2 * age")
145
+ 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?
150
+ construct_relation_for_association_calculations.calculate(operation, column_name, options)
151
+ else
152
+ perform_calculation(operation, column_name, options)
153
+ end
154
+ end
155
+ rescue ThrowResult
156
+ 0
157
+ end
158
+
159
+ private
160
+
161
+ def perform_calculation(operation, column_name, options = {})
162
+ operation = operation.to_s.downcase
163
+
164
+ distinct = nil
165
+
166
+ if operation == "count"
167
+ column_name ||= (select_for_count || :all)
168
+
169
+ if arel.joins(arel) =~ /LEFT OUTER/i
170
+ distinct = true
171
+ column_name = @klass.primary_key if column_name == :all
172
+ end
173
+
174
+ distinct = nil if column_name =~ /\s*DISTINCT\s+/i
175
+ end
176
+
177
+ distinct = options[:distinct] || distinct
178
+ column_name = :all if column_name.blank? && operation == "count"
179
+
180
+ if @group_values.any?
181
+ return execute_grouped_calculation(operation, column_name)
182
+ else
183
+ return execute_simple_calculation(operation, column_name, distinct)
184
+ end
185
+ end
186
+
187
+ 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)
190
+ else
191
+ Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s)
192
+ end
193
+
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)
197
+ end
198
+
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)
206
+
207
+ group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field
208
+
209
+ aggregate_alias = column_alias_for(operation, column_name)
210
+
211
+ select_statement = if operation == 'count' && column_name == :all
212
+ "COUNT(*) AS count_all"
213
+ else
214
+ Arel::Attribute.new(@klass.unscoped, column_name).send(operation).as(aggregate_alias).to_sql
215
+ end
216
+
217
+ select_statement << ", #{group_field} AS #{group_alias}"
218
+
219
+ relation = except(:group).select(select_statement).group(group)
220
+
221
+ calculated_data = @klass.connection.select_all(relation.to_sql)
222
+
223
+ if association
224
+ key_ids = calculated_data.collect { |row| row[group_alias] }
225
+ key_records = association.klass.base_class.find(key_ids)
226
+ key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
227
+ end
228
+
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
236
+ end
237
+
238
+ # Converts the given keys to the value that the database adapter returns as
239
+ # a usable column name:
240
+ #
241
+ # column_alias_for("users.id") # => "users_id"
242
+ # column_alias_for("sum(id)") # => "sum_id"
243
+ # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
244
+ # column_alias_for("count(*)") # => "count_all"
245
+ # column_alias_for("count", "id") # => "count_id"
246
+ def column_alias_for(*keys)
247
+ table_name = keys.join(' ')
248
+ table_name.downcase!
249
+ table_name.gsub!(/\*/, 'all')
250
+ table_name.gsub!(/\W+/, ' ')
251
+ table_name.strip!
252
+ table_name.gsub!(/ +/, '_')
253
+
254
+ @klass.connection.table_alias_for(table_name)
255
+ end
256
+
257
+ def column_for(field)
258
+ field_name = field.to_s.split('.').last
259
+ @klass.columns.detect { |c| c.name.to_s == field_name }
260
+ end
261
+
262
+ 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
272
+ end
273
+ end
274
+
275
+ def type_cast_using_column(value, column)
276
+ column ? column.type_cast(value) : value
277
+ end
278
+
279
+ def select_for_count
280
+ if @select_values.present?
281
+ select = @select_values.join(", ")
282
+ select if select !~ /(,|\*)/
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,355 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require 'active_support/core_ext/hash/indifferent_access'
3
+
4
+ module ActiveRecord
5
+ module FinderMethods
6
+ # Find operates with four different retrieval approaches:
7
+ #
8
+ # * Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
9
+ # If no record can be found for all of the listed ids, then RecordNotFound will be raised.
10
+ # * Find first - This will return the first record matched by the options used. These options can either be specific
11
+ # conditions or merely an order. If no record can be matched, +nil+ is returned. Use
12
+ # <tt>Model.find(:first, *args)</tt> or its shortcut <tt>Model.first(*args)</tt>.
13
+ # * Find last - This will return the last record matched by the options used. These options can either be specific
14
+ # conditions or merely an order. If no record can be matched, +nil+ is returned. Use
15
+ # <tt>Model.find(:last, *args)</tt> or its shortcut <tt>Model.last(*args)</tt>.
16
+ # * Find all - This will return all the records matched by the options used.
17
+ # If no records are found, an empty array is returned. Use
18
+ # <tt>Model.find(:all, *args)</tt> or its shortcut <tt>Model.all(*args)</tt>.
19
+ #
20
+ # All approaches accept an options hash as their last parameter.
21
+ #
22
+ # ==== Parameters
23
+ #
24
+ # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>[ "user_name = ?", username ]</tt>,
25
+ # or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro.
26
+ # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name".
27
+ # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
28
+ # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a
29
+ # <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause.
30
+ # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
31
+ # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5,
32
+ # it would skip rows 0 through 4.
33
+ # * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed),
34
+ # named associations in the same form used for the <tt>:include</tt> option, which will perform an
35
+ # <tt>INNER JOIN</tt> on the associated table(s),
36
+ # or an array containing a mixture of both strings and named associations.
37
+ # If the value is a string, then the records will be returned read-only since they will
38
+ # have attributes that do not correspond to the table's columns.
39
+ # Pass <tt>:readonly => false</tt> to override.
40
+ # * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer
41
+ # to already defined associations. See eager loading under Associations.
42
+ # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you,
43
+ # for example, want to do a join but not include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name").
44
+ # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed
45
+ # to an alternate table name (or even the name of a database view).
46
+ # * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated.
47
+ # * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE".
48
+ # <tt>:lock => true</tt> gives connection's default exclusive lock, usually "FOR UPDATE".
49
+ #
50
+ # ==== Examples
51
+ #
52
+ # # find by id
53
+ # Person.find(1) # returns the object for ID = 1
54
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
55
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
56
+ # Person.find([1]) # returns an array for the object with ID = 1
57
+ # Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC")
58
+ #
59
+ # Note that returned records may not be in the same order as the ids you
60
+ # provide since database rows are unordered. Give an explicit <tt>:order</tt>
61
+ # to ensure the results are sorted.
62
+ #
63
+ # ==== Examples
64
+ #
65
+ # # find first
66
+ # Person.find(:first) # returns the first object fetched by SELECT * FROM people
67
+ # Person.find(:first, :conditions => [ "user_name = ?", user_name])
68
+ # Person.find(:first, :conditions => [ "user_name = :u", { :u => user_name }])
69
+ # Person.find(:first, :order => "created_on DESC", :offset => 5)
70
+ #
71
+ # # find last
72
+ # Person.find(:last) # returns the last object fetched by SELECT * FROM people
73
+ # Person.find(:last, :conditions => [ "user_name = ?", user_name])
74
+ # Person.find(:last, :order => "created_on DESC", :offset => 5)
75
+ #
76
+ # # find all
77
+ # Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people
78
+ # Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50)
79
+ # Person.find(:all, :conditions => { :friends => ["Bob", "Steve", "Fred"] }
80
+ # Person.find(:all, :offset => 10, :limit => 10)
81
+ # Person.find(:all, :include => [ :account, :friends ])
82
+ # Person.find(:all, :group => "category")
83
+ #
84
+ # Example for find with a lock: Imagine two concurrent transactions:
85
+ # each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting
86
+ # in two saves of <tt>person.visits = 3</tt>. By locking the row, the second
87
+ # transaction has to wait until the first is finished; we get the
88
+ # expected <tt>person.visits == 4</tt>.
89
+ #
90
+ # Person.transaction do
91
+ # person = Person.find(1, :lock => true)
92
+ # person.visits += 1
93
+ # person.save!
94
+ # end
95
+ def find(*args)
96
+ return to_a.find { |*block_args| yield(*block_args) } if block_given?
97
+
98
+ options = args.extract_options!
99
+
100
+ if options.present?
101
+ apply_finder_options(options).find(*args)
102
+ else
103
+ case args.first
104
+ when :first, :last, :all
105
+ send(args.first)
106
+ else
107
+ find_with_ids(*args)
108
+ end
109
+ end
110
+ end
111
+
112
+ # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
113
+ # same arguments to this method as you can to <tt>find(:first)</tt>.
114
+ def first(*args)
115
+ if args.any?
116
+ if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
117
+ to_a.first(*args)
118
+ else
119
+ apply_finder_options(args.first).first
120
+ end
121
+ else
122
+ find_first
123
+ end
124
+ end
125
+
126
+ # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the
127
+ # same arguments to this method as you can to <tt>find(:last)</tt>.
128
+ def last(*args)
129
+ if args.any?
130
+ if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash))
131
+ to_a.last(*args)
132
+ else
133
+ apply_finder_options(args.first).last
134
+ end
135
+ else
136
+ find_last
137
+ end
138
+ end
139
+
140
+ # A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the
141
+ # same arguments to this method as you can to <tt>find(:all)</tt>.
142
+ def all(*args)
143
+ args.any? ? apply_finder_options(args.first).to_a : to_a
144
+ end
145
+
146
+ # Returns true if a record exists in the table that matches the +id+ or
147
+ # conditions given, or false otherwise. The argument can take five forms:
148
+ #
149
+ # * Integer - Finds the record with this primary key.
150
+ # * String - Finds the record with a primary key corresponding to this
151
+ # string (such as <tt>'5'</tt>).
152
+ # * Array - Finds the record that matches these +find+-style conditions
153
+ # (such as <tt>['color = ?', 'red']</tt>).
154
+ # * Hash - Finds the record that matches these +find+-style conditions
155
+ # (such as <tt>{:color => 'red'}</tt>).
156
+ # * No args - Returns false if the table is empty, true otherwise.
157
+ #
158
+ # For more information about specifying conditions as a Hash or Array,
159
+ # see the Conditions section in the introduction to ActiveRecord::Base.
160
+ #
161
+ # Note: You can't pass in a condition as a string (like <tt>name =
162
+ # 'Jamie'</tt>), since it would be sanitized and then queried against
163
+ # the primary key column, like <tt>id = 'name = \'Jamie\''</tt>.
164
+ #
165
+ # ==== Examples
166
+ # Person.exists?(5)
167
+ # Person.exists?('5')
168
+ # Person.exists?(:name => "David")
169
+ # Person.exists?(['name LIKE ?', "%#{query}%"])
170
+ # Person.exists?
171
+ def exists?(id = nil)
172
+ id = id.id if ActiveRecord::Base === id
173
+
174
+ case id
175
+ when Array, Hash
176
+ where(id).exists?
177
+ else
178
+ relation = select(primary_key).limit(1)
179
+ relation = relation.where(primary_key.eq(id)) if id
180
+ relation.first ? true : false
181
+ end
182
+ end
183
+
184
+ protected
185
+
186
+ def find_with_associations
187
+ including = (@eager_load_values + @includes_values).uniq
188
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil)
189
+ rows = construct_relation_for_association_find(join_dependency).to_a
190
+ join_dependency.instantiate(rows)
191
+ rescue ThrowResult
192
+ []
193
+ end
194
+
195
+ def construct_relation_for_association_calculations
196
+ including = (@eager_load_values + @includes_values).uniq
197
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel))
198
+ relation = except(:includes, :eager_load, :preload)
199
+ apply_join_dependency(relation, join_dependency)
200
+ end
201
+
202
+ def construct_relation_for_association_find(join_dependency)
203
+ relation = except(:includes, :eager_load, :preload, :select).select(column_aliases(join_dependency))
204
+ apply_join_dependency(relation, join_dependency)
205
+ end
206
+
207
+ def apply_join_dependency(relation, join_dependency)
208
+ for association in join_dependency.join_associations
209
+ relation = association.join_relation(relation)
210
+ end
211
+
212
+ limitable_reflections = using_limitable_reflections?(join_dependency.reflections)
213
+
214
+ if !limitable_reflections && relation.limit_value
215
+ limited_id_condition = construct_limited_ids_condition(relation.except(:select))
216
+ relation = relation.where(limited_id_condition)
217
+ end
218
+
219
+ relation = relation.except(:limit, :offset) unless limitable_reflections
220
+
221
+ relation
222
+ end
223
+
224
+ def construct_limited_ids_condition(relation)
225
+ orders = relation.order_values.join(", ")
226
+ values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders)
227
+
228
+ ids_array = relation.select(values).collect {|row| row[@klass.primary_key]}
229
+ ids_array.empty? ? raise(ThrowResult) : primary_key.in(ids_array)
230
+ end
231
+
232
+ def find_by_attributes(match, attributes, *args)
233
+ conditions = attributes.inject({}) {|h, a| h[a] = args[attributes.index(a)]; h}
234
+ result = where(conditions).send(match.finder)
235
+
236
+ if match.bang? && result.blank?
237
+ raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
238
+ else
239
+ result
240
+ end
241
+ end
242
+
243
+ def find_or_instantiator_by_attributes(match, attributes, *args)
244
+ protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
245
+ args.each_with_index do |arg, i|
246
+ if arg.is_a?(Hash)
247
+ protected_attributes_for_create = args[i].with_indifferent_access
248
+ else
249
+ unprotected_attributes_for_create[attributes[i]] = args[i]
250
+ end
251
+ end
252
+
253
+ conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys
254
+
255
+ record = where(conditions).first
256
+
257
+ unless record
258
+ record = @klass.new do |r|
259
+ r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
260
+ r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
261
+ end
262
+ yield(record) if block_given?
263
+ record.save if match.instantiator == :create
264
+ end
265
+
266
+ record
267
+ end
268
+
269
+ def find_with_ids(*ids)
270
+ return to_a.find { |*block_args| yield(*block_args) } if block_given?
271
+
272
+ expects_array = ids.first.kind_of?(Array)
273
+ return ids.first if expects_array && ids.first.empty?
274
+
275
+ ids = ids.flatten.compact.uniq
276
+
277
+ case ids.size
278
+ when 0
279
+ raise RecordNotFound, "Couldn't find #{@klass.name} without an ID"
280
+ when 1
281
+ result = find_one(ids.first)
282
+ expects_array ? [ result ] : result
283
+ else
284
+ find_some(ids)
285
+ end
286
+ end
287
+
288
+ def find_one(id)
289
+ id = id.id if ActiveRecord::Base === id
290
+
291
+ record = where(primary_key.eq(id)).first
292
+
293
+ unless record
294
+ conditions = arel.wheres.map { |x| x.value }.join(', ')
295
+ conditions = " [WHERE #{conditions}]" if conditions.present?
296
+ raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}"
297
+ end
298
+
299
+ record
300
+ end
301
+
302
+ def find_some(ids)
303
+ result = where(primary_key.in(ids)).all
304
+
305
+ expected_size =
306
+ if @limit_value && ids.size > @limit_value
307
+ @limit_value
308
+ else
309
+ ids.size
310
+ end
311
+
312
+ # 11 ids with limit 3, offset 9 should give 2 results.
313
+ if @offset_value && (ids.size - @offset_value < expected_size)
314
+ expected_size = ids.size - @offset_value
315
+ end
316
+
317
+ if result.size == expected_size
318
+ result
319
+ else
320
+ conditions = arel.wheres.map { |x| x.value }.join(', ')
321
+ conditions = " [WHERE #{conditions}]" if conditions.present?
322
+
323
+ error = "Couldn't find all #{@klass.name.pluralize} with IDs "
324
+ error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
325
+ raise RecordNotFound, error
326
+ end
327
+ end
328
+
329
+ def find_first
330
+ if loaded?
331
+ @records.first
332
+ else
333
+ @first ||= limit(1).to_a[0]
334
+ end
335
+ end
336
+
337
+ def find_last
338
+ if loaded?
339
+ @records.last
340
+ else
341
+ @last ||= reverse_order.limit(1).to_a[0]
342
+ end
343
+ end
344
+
345
+ def column_aliases(join_dependency)
346
+ join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name|
347
+ "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ")
348
+ end
349
+
350
+ def using_limitable_reflections?(reflections)
351
+ reflections.none? { |r| r.collection? }
352
+ end
353
+
354
+ end
355
+ end
@@ -0,0 +1,41 @@
1
+ module ActiveRecord
2
+ class PredicateBuilder
3
+
4
+ def initialize(engine)
5
+ @engine = engine
6
+ end
7
+
8
+ def build_from_hash(attributes, default_table)
9
+ predicates = attributes.map do |column, value|
10
+ table = default_table
11
+
12
+ if value.is_a?(Hash)
13
+ table = Arel::Table.new(column, :engine => @engine)
14
+ build_from_hash(value, table)
15
+ else
16
+ column = column.to_s
17
+
18
+ if column.include?('.')
19
+ table_name, column = column.split('.', 2)
20
+ table = Arel::Table.new(table_name, :engine => @engine)
21
+ end
22
+
23
+ attribute = table[column] || Arel::Attribute.new(table, column)
24
+
25
+ case value
26
+ when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation
27
+ values = value.to_a
28
+ attribute.in(values)
29
+ when Range, Arel::Relation
30
+ attribute.in(value)
31
+ else
32
+ attribute.eq(value)
33
+ end
34
+ end
35
+ end
36
+
37
+ predicates.flatten
38
+ end
39
+
40
+ end
41
+ end