square-activerecord 3.0.7

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 (94) hide show
  1. data/CHANGELOG +6140 -0
  2. data/README.rdoc +222 -0
  3. data/examples/associations.png +0 -0
  4. data/examples/performance.rb +179 -0
  5. data/examples/simple.rb +14 -0
  6. data/lib/active_record.rb +124 -0
  7. data/lib/active_record/aggregations.rb +277 -0
  8. data/lib/active_record/association_preload.rb +430 -0
  9. data/lib/active_record/associations.rb +2307 -0
  10. data/lib/active_record/associations/association_collection.rb +572 -0
  11. data/lib/active_record/associations/association_proxy.rb +299 -0
  12. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +82 -0
  14. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +143 -0
  15. data/lib/active_record/associations/has_many_association.rb +128 -0
  16. data/lib/active_record/associations/has_many_through_association.rb +115 -0
  17. data/lib/active_record/associations/has_one_association.rb +143 -0
  18. data/lib/active_record/associations/has_one_through_association.rb +40 -0
  19. data/lib/active_record/associations/through_association_scope.rb +154 -0
  20. data/lib/active_record/attribute_methods.rb +60 -0
  21. data/lib/active_record/attribute_methods/before_type_cast.rb +30 -0
  22. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  23. data/lib/active_record/attribute_methods/primary_key.rb +56 -0
  24. data/lib/active_record/attribute_methods/query.rb +39 -0
  25. data/lib/active_record/attribute_methods/read.rb +145 -0
  26. data/lib/active_record/attribute_methods/time_zone_conversion.rb +64 -0
  27. data/lib/active_record/attribute_methods/write.rb +43 -0
  28. data/lib/active_record/autosave_association.rb +369 -0
  29. data/lib/active_record/base.rb +1904 -0
  30. data/lib/active_record/callbacks.rb +284 -0
  31. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +364 -0
  32. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
  33. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  34. data/lib/active_record/connection_adapters/abstract/database_statements.rb +333 -0
  35. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  36. data/lib/active_record/connection_adapters/abstract/quoting.rb +73 -0
  37. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
  38. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +539 -0
  39. data/lib/active_record/connection_adapters/abstract_adapter.rb +217 -0
  40. data/lib/active_record/connection_adapters/mysql_adapter.rb +657 -0
  41. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1031 -0
  42. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -0
  43. data/lib/active_record/connection_adapters/sqlite_adapter.rb +401 -0
  44. data/lib/active_record/counter_cache.rb +115 -0
  45. data/lib/active_record/dynamic_finder_match.rb +56 -0
  46. data/lib/active_record/dynamic_scope_match.rb +23 -0
  47. data/lib/active_record/errors.rb +172 -0
  48. data/lib/active_record/fixtures.rb +1006 -0
  49. data/lib/active_record/locale/en.yml +40 -0
  50. data/lib/active_record/locking/optimistic.rb +172 -0
  51. data/lib/active_record/locking/pessimistic.rb +55 -0
  52. data/lib/active_record/log_subscriber.rb +48 -0
  53. data/lib/active_record/migration.rb +617 -0
  54. data/lib/active_record/named_scope.rb +138 -0
  55. data/lib/active_record/nested_attributes.rb +419 -0
  56. data/lib/active_record/observer.rb +125 -0
  57. data/lib/active_record/persistence.rb +290 -0
  58. data/lib/active_record/query_cache.rb +36 -0
  59. data/lib/active_record/railtie.rb +91 -0
  60. data/lib/active_record/railties/controller_runtime.rb +38 -0
  61. data/lib/active_record/railties/databases.rake +512 -0
  62. data/lib/active_record/reflection.rb +411 -0
  63. data/lib/active_record/relation.rb +394 -0
  64. data/lib/active_record/relation/batches.rb +89 -0
  65. data/lib/active_record/relation/calculations.rb +295 -0
  66. data/lib/active_record/relation/finder_methods.rb +363 -0
  67. data/lib/active_record/relation/predicate_builder.rb +48 -0
  68. data/lib/active_record/relation/query_methods.rb +303 -0
  69. data/lib/active_record/relation/spawn_methods.rb +132 -0
  70. data/lib/active_record/schema.rb +59 -0
  71. data/lib/active_record/schema_dumper.rb +195 -0
  72. data/lib/active_record/serialization.rb +60 -0
  73. data/lib/active_record/serializers/xml_serializer.rb +244 -0
  74. data/lib/active_record/session_store.rb +340 -0
  75. data/lib/active_record/test_case.rb +67 -0
  76. data/lib/active_record/timestamp.rb +88 -0
  77. data/lib/active_record/transactions.rb +359 -0
  78. data/lib/active_record/validations.rb +84 -0
  79. data/lib/active_record/validations/associated.rb +48 -0
  80. data/lib/active_record/validations/uniqueness.rb +190 -0
  81. data/lib/active_record/version.rb +10 -0
  82. data/lib/rails/generators/active_record.rb +19 -0
  83. data/lib/rails/generators/active_record/migration.rb +15 -0
  84. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  85. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  86. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  87. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  88. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  89. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  90. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  91. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  92. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  93. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  94. metadata +223 -0
@@ -0,0 +1,89 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
3
+ 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).
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
+
24
+ self
25
+ end
26
+
27
+ # 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>
29
+ # option; the default is 1000.
30
+ #
31
+ # You can control the starting point for the batch processing by
32
+ # supplying the <tt>:start</tt> option. This is especially useful if you
33
+ # want multiple workers dealing with the same processing queue. You can
34
+ # 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>
36
+ # option on that worker).
37
+ #
38
+ # It's not possible to set the order. That is automatically set to
39
+ # 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
41
+ # primary keys. You can't set the limit either, that's used to control
42
+ # the the batch sizes.
43
+ #
44
+ # Example:
45
+ #
46
+ # Person.where("age > 21").find_in_batches do |group|
47
+ # sleep(50) # Make sure it doesn't get too crowded in there!
48
+ # group.each { |person| person.party_all_night! }
49
+ # end
50
+ def find_in_batches(options = {})
51
+ relation = self
52
+
53
+ unless arel.orders.blank? && arel.taken.blank?
54
+ ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
55
+ end
56
+
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
+ batch_size = options.delete(:batch_size) || 1000
66
+
67
+ relation = relation.except(:order).order(batch_order).limit(batch_size)
68
+ records = relation.where(primary_key.gteq(start)).all
69
+
70
+ while records.any?
71
+ yield records
72
+
73
+ break if records.size < batch_size
74
+
75
+ if primary_key_offset = records.last.id
76
+ records = relation.where(primary_key.gt(primary_key_offset)).to_a
77
+ else
78
+ raise "Primary key not included in the custom select clause"
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def batch_order
86
+ "#{@klass.quoted_table_name}.#{@klass.quoted_primary_key} ASC"
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,295 @@
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.join_sql =~ /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
+
179
+ if @group_values.any?
180
+ execute_grouped_calculation(operation, column_name, distinct)
181
+ else
182
+ execute_simple_calculation(operation, column_name, distinct)
183
+ end
184
+ end
185
+
186
+ def aggregate_column(column_name)
187
+ if @klass.column_names.include?(column_name.to_s)
188
+ Arel::Attribute.new(@klass.unscoped.table, column_name)
189
+ else
190
+ Arel.sql(column_name == :all ? "*" : column_name.to_s)
191
+ end
192
+ end
193
+
194
+ def operation_over_aggregate_column(column, operation, distinct)
195
+ operation == 'count' ? column.count(distinct) : column.send(operation)
196
+ end
197
+
198
+ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
199
+ column = aggregate_column(column_name)
200
+
201
+ # Postgresql doesn't like ORDER BY when there are no GROUP BY
202
+ relation = except(:order)
203
+ select_value = operation_over_aggregate_column(column, operation, distinct)
204
+
205
+ relation.select_values = [select_value]
206
+
207
+ type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation)
208
+ end
209
+
210
+ def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
211
+ group_attr = @group_values
212
+ association = @klass.reflect_on_association(group_attr.first.to_sym)
213
+ associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
214
+ group_fields = Array(associated ? association.primary_key_name : group_attr)
215
+ group_aliases = []
216
+ group_columns = {}
217
+
218
+ group_fields.each do |field|
219
+ group_aliases << column_alias_for(field)
220
+ group_columns[column_alias_for(field)] = column_for(field)
221
+ end
222
+
223
+ group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields
224
+
225
+ if operation == 'count' && column_name == :all
226
+ aggregate_alias = 'count_all'
227
+ else
228
+ aggregate_alias = column_alias_for(operation, column_name)
229
+ end
230
+
231
+ relation = except(:group).group(group.join(','))
232
+ relation.select_values = [ operation_over_aggregate_column(aggregate_column(column_name), operation, distinct).as(aggregate_alias) ]
233
+ group_fields.each_index{ |i| relation.select_values << "#{group_fields[i]} AS #{group_aliases[i]}" }
234
+
235
+ calculated_data = @klass.connection.select_all(relation.to_sql)
236
+
237
+ if association
238
+ key_ids = calculated_data.collect { |row| row[group_aliases.first] }
239
+ key_records = association.klass.base_class.find(key_ids)
240
+ key_records = Hash[key_records.map { |r| [r.id, r] }]
241
+ end
242
+
243
+ ActiveSupport::OrderedHash[calculated_data.map do |row|
244
+ key = group_aliases.map{|group_alias| type_cast_calculated_value(row[group_alias], group_columns[group_alias])}
245
+ key = key.first if key.size == 1
246
+ key = key_records[key] if associated
247
+ [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
248
+ end]
249
+ end
250
+
251
+ # Converts the given keys to the value that the database adapter returns as
252
+ # a usable column name:
253
+ #
254
+ # column_alias_for("users.id") # => "users_id"
255
+ # column_alias_for("sum(id)") # => "sum_id"
256
+ # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
257
+ # column_alias_for("count(*)") # => "count_all"
258
+ # column_alias_for("count", "id") # => "count_id"
259
+ def column_alias_for(*keys)
260
+ table_name = keys.join(' ')
261
+ table_name.downcase!
262
+ table_name.gsub!(/\*/, 'all')
263
+ table_name.gsub!(/\W+/, ' ')
264
+ table_name.strip!
265
+ table_name.gsub!(/ +/, '_')
266
+
267
+ @klass.connection.table_alias_for(table_name)
268
+ end
269
+
270
+ def column_for(field)
271
+ field_name = field.to_s.split('.').last
272
+ @klass.columns.detect { |c| c.name.to_s == field_name }
273
+ end
274
+
275
+ def type_cast_calculated_value(value, column, operation = nil)
276
+ case operation
277
+ when 'count' then value.to_i
278
+ when 'sum' then type_cast_using_column(value || '0', column)
279
+ when 'average' then value.respond_to?(:to_d) ? value.to_d : value
280
+ else type_cast_using_column(value, column)
281
+ end
282
+ end
283
+
284
+ def type_cast_using_column(value, column)
285
+ column ? column.type_cast(value) : value
286
+ end
287
+
288
+ def select_for_count
289
+ if @select_values.present?
290
+ select = @select_values.join(", ")
291
+ select if select !~ /(,|\*)/
292
+ end
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,363 @@
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.where("administrator = 1").order("created_on DESC").find(1)
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.first # returns the first object fetched by SELECT * FROM people
67
+ # Person.where(["user_name = ?", user_name]).first
68
+ # Person.where(["user_name = :u", { :u => user_name }]).first
69
+ # Person.order("created_on DESC").offset(5).first
70
+ #
71
+ # # find last
72
+ # Person.last # returns the last object fetched by SELECT * FROM people
73
+ # Person.where(["user_name = ?", user_name]).last
74
+ # Person.order("created_on DESC").offset(5).last
75
+ #
76
+ # # find all
77
+ # Person.all # returns an array of objects for all the rows fetched by SELECT * FROM people
78
+ # Person.where(["category IN (?)", categories]).limit(50).all
79
+ # Person.where({ :friends => ["Bob", "Steve", "Fred"] }).all
80
+ # Person.offset(10).limit(10).all
81
+ # Person.includes([:account, :friends]).all
82
+ # Person.group("category").all
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.lock(true).find(1)
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
+ join_dependency = construct_join_dependency_for_association_find
175
+ relation = construct_relation_for_association_find(join_dependency)
176
+ relation = relation.except(:select).select("1").limit(1)
177
+
178
+ case id
179
+ when Array, Hash
180
+ relation = relation.where(id)
181
+ else
182
+ relation = relation.where(table[primary_key.name].eq(id)) if id
183
+ end
184
+
185
+ connection.select_value(relation.to_sql) ? true : false
186
+ end
187
+
188
+ protected
189
+
190
+ def find_with_associations
191
+ join_dependency = construct_join_dependency_for_association_find
192
+ rows = construct_relation_for_association_find(join_dependency).to_a
193
+ join_dependency.instantiate(rows)
194
+ rescue ThrowResult
195
+ []
196
+ end
197
+
198
+ def construct_join_dependency_for_association_find
199
+ including = (@eager_load_values + @includes_values).uniq
200
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil)
201
+ end
202
+
203
+ def construct_relation_for_association_calculations
204
+ including = (@eager_load_values + @includes_values).uniq
205
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.join_sql)
206
+ relation = except(:includes, :eager_load, :preload)
207
+ apply_join_dependency(relation, join_dependency)
208
+ end
209
+
210
+ def construct_relation_for_association_find(join_dependency)
211
+ relation = except(:includes, :eager_load, :preload, :select).select(column_aliases(join_dependency))
212
+ apply_join_dependency(relation, join_dependency)
213
+ end
214
+
215
+ def apply_join_dependency(relation, join_dependency)
216
+ for association in join_dependency.join_associations
217
+ relation = association.join_relation(relation)
218
+ end
219
+
220
+ limitable_reflections = using_limitable_reflections?(join_dependency.reflections)
221
+
222
+ if !limitable_reflections && relation.limit_value
223
+ limited_id_condition = construct_limited_ids_condition(relation.except(:select))
224
+ relation = relation.where(limited_id_condition)
225
+ end
226
+
227
+ relation = relation.except(:limit, :offset) unless limitable_reflections
228
+
229
+ relation
230
+ end
231
+
232
+ def construct_limited_ids_condition(relation)
233
+ orders = relation.order_values.join(", ")
234
+ values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders)
235
+
236
+ ids_array = relation.select(values).collect {|row| row[@klass.primary_key]}
237
+ ids_array.empty? ? raise(ThrowResult) : primary_key.in(ids_array)
238
+ end
239
+
240
+ def find_by_attributes(match, attributes, *args)
241
+ conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}]
242
+ result = where(conditions).send(match.finder)
243
+
244
+ if match.bang? && result.blank?
245
+ raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
246
+ else
247
+ result
248
+ end
249
+ end
250
+
251
+ def find_or_instantiator_by_attributes(match, attributes, *args)
252
+ protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
253
+ args.each_with_index do |arg, i|
254
+ if arg.is_a?(Hash)
255
+ protected_attributes_for_create = args[i].with_indifferent_access
256
+ else
257
+ unprotected_attributes_for_create[attributes[i]] = args[i]
258
+ end
259
+ end
260
+
261
+ conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys
262
+
263
+ record = where(conditions).first
264
+
265
+ unless record
266
+ record = @klass.new do |r|
267
+ r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
268
+ r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
269
+ end
270
+ yield(record) if block_given?
271
+ record.save if match.instantiator == :create
272
+ end
273
+
274
+ record
275
+ end
276
+
277
+ def find_with_ids(*ids)
278
+ return to_a.find { |*block_args| yield(*block_args) } if block_given?
279
+
280
+ expects_array = ids.first.kind_of?(Array)
281
+ return ids.first if expects_array && ids.first.empty?
282
+
283
+ ids = ids.flatten.compact.uniq
284
+
285
+ case ids.size
286
+ when 0
287
+ raise RecordNotFound, "Couldn't find #{@klass.name} without an ID"
288
+ when 1
289
+ result = find_one(ids.first)
290
+ expects_array ? [ result ] : result
291
+ else
292
+ find_some(ids)
293
+ end
294
+ end
295
+
296
+ def find_one(id)
297
+ id = id.id if ActiveRecord::Base === id
298
+
299
+ record = where(primary_key.eq(id)).first
300
+
301
+ unless record
302
+ conditions = arel.where_sql
303
+ conditions = " [#{conditions}]" if conditions
304
+ raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}"
305
+ end
306
+
307
+ record
308
+ end
309
+
310
+ def find_some(ids)
311
+ result = where(primary_key.in(ids)).all
312
+
313
+ expected_size =
314
+ if @limit_value && ids.size > @limit_value
315
+ @limit_value
316
+ else
317
+ ids.size
318
+ end
319
+
320
+ # 11 ids with limit 3, offset 9 should give 2 results.
321
+ if @offset_value && (ids.size - @offset_value < expected_size)
322
+ expected_size = ids.size - @offset_value
323
+ end
324
+
325
+ if result.size == expected_size
326
+ result
327
+ else
328
+ conditions = arel.wheres.map { |x| x.value }.join(', ')
329
+ conditions = " [WHERE #{conditions}]" if conditions.present?
330
+
331
+ error = "Couldn't find all #{@klass.name.pluralize} with IDs "
332
+ error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
333
+ raise RecordNotFound, error
334
+ end
335
+ end
336
+
337
+ def find_first
338
+ if loaded?
339
+ @records.first
340
+ else
341
+ @first ||= limit(1).to_a[0]
342
+ end
343
+ end
344
+
345
+ def find_last
346
+ if loaded?
347
+ @records.last
348
+ else
349
+ @last ||= reverse_order.limit(1).to_a[0]
350
+ end
351
+ end
352
+
353
+ def column_aliases(join_dependency)
354
+ join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name|
355
+ "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ")
356
+ end
357
+
358
+ def using_limitable_reflections?(reflections)
359
+ reflections.none? { |r| r.collection? }
360
+ end
361
+
362
+ end
363
+ end