activerecord 3.1.12 → 3.2.0.rc1

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 (99) hide show
  1. data/CHANGELOG.md +6263 -103
  2. data/README.rdoc +2 -2
  3. data/examples/performance.rb +55 -31
  4. data/lib/active_record.rb +28 -2
  5. data/lib/active_record/aggregations.rb +2 -2
  6. data/lib/active_record/associations.rb +82 -69
  7. data/lib/active_record/associations/association.rb +2 -37
  8. data/lib/active_record/associations/association_scope.rb +3 -30
  9. data/lib/active_record/associations/builder/association.rb +6 -4
  10. data/lib/active_record/associations/builder/belongs_to.rb +3 -3
  11. data/lib/active_record/associations/builder/collection_association.rb +2 -2
  12. data/lib/active_record/associations/builder/has_many.rb +4 -4
  13. data/lib/active_record/associations/builder/has_one.rb +5 -6
  14. data/lib/active_record/associations/builder/singular_association.rb +3 -16
  15. data/lib/active_record/associations/collection_association.rb +55 -28
  16. data/lib/active_record/associations/collection_proxy.rb +1 -35
  17. data/lib/active_record/associations/has_many_association.rb +5 -1
  18. data/lib/active_record/associations/has_many_through_association.rb +11 -8
  19. data/lib/active_record/associations/join_dependency.rb +1 -1
  20. data/lib/active_record/associations/preloader/association.rb +3 -1
  21. data/lib/active_record/attribute_assignment.rb +221 -0
  22. data/lib/active_record/attribute_methods.rb +212 -32
  23. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  24. data/lib/active_record/attribute_methods/dirty.rb +3 -3
  25. data/lib/active_record/attribute_methods/primary_key.rb +62 -25
  26. data/lib/active_record/attribute_methods/read.rb +69 -80
  27. data/lib/active_record/attribute_methods/serialization.rb +89 -0
  28. data/lib/active_record/attribute_methods/time_zone_conversion.rb +9 -14
  29. data/lib/active_record/attribute_methods/write.rb +27 -5
  30. data/lib/active_record/autosave_association.rb +23 -8
  31. data/lib/active_record/base.rb +223 -1712
  32. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +98 -132
  33. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +82 -29
  34. data/lib/active_record/connection_adapters/abstract/database_statements.rb +13 -42
  35. data/lib/active_record/connection_adapters/abstract/query_cache.rb +1 -1
  36. data/lib/active_record/connection_adapters/abstract/quoting.rb +7 -4
  37. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +36 -25
  38. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +41 -13
  39. data/lib/active_record/connection_adapters/abstract_adapter.rb +78 -43
  40. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
  41. data/lib/active_record/connection_adapters/mysql2_adapter.rb +138 -578
  42. data/lib/active_record/connection_adapters/mysql_adapter.rb +86 -658
  43. data/lib/active_record/connection_adapters/postgresql_adapter.rb +144 -94
  44. data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
  45. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -6
  46. data/lib/active_record/connection_adapters/sqlite_adapter.rb +43 -22
  47. data/lib/active_record/counter_cache.rb +1 -1
  48. data/lib/active_record/dynamic_matchers.rb +79 -0
  49. data/lib/active_record/errors.rb +11 -1
  50. data/lib/active_record/explain.rb +83 -0
  51. data/lib/active_record/explain_subscriber.rb +21 -0
  52. data/lib/active_record/fixtures.rb +31 -76
  53. data/lib/active_record/fixtures/file.rb +65 -0
  54. data/lib/active_record/identity_map.rb +1 -7
  55. data/lib/active_record/inheritance.rb +167 -0
  56. data/lib/active_record/integration.rb +49 -0
  57. data/lib/active_record/locking/optimistic.rb +19 -11
  58. data/lib/active_record/locking/pessimistic.rb +1 -1
  59. data/lib/active_record/log_subscriber.rb +3 -3
  60. data/lib/active_record/migration.rb +38 -29
  61. data/lib/active_record/migration/command_recorder.rb +7 -7
  62. data/lib/active_record/model_schema.rb +362 -0
  63. data/lib/active_record/nested_attributes.rb +3 -2
  64. data/lib/active_record/persistence.rb +51 -1
  65. data/lib/active_record/querying.rb +58 -0
  66. data/lib/active_record/railtie.rb +24 -28
  67. data/lib/active_record/railties/controller_runtime.rb +3 -1
  68. data/lib/active_record/railties/databases.rake +133 -77
  69. data/lib/active_record/readonly_attributes.rb +26 -0
  70. data/lib/active_record/reflection.rb +7 -15
  71. data/lib/active_record/relation.rb +78 -35
  72. data/lib/active_record/relation/batches.rb +5 -2
  73. data/lib/active_record/relation/calculations.rb +27 -6
  74. data/lib/active_record/relation/delegation.rb +49 -0
  75. data/lib/active_record/relation/finder_methods.rb +5 -4
  76. data/lib/active_record/relation/predicate_builder.rb +13 -16
  77. data/lib/active_record/relation/query_methods.rb +59 -4
  78. data/lib/active_record/result.rb +1 -1
  79. data/lib/active_record/sanitization.rb +194 -0
  80. data/lib/active_record/schema_dumper.rb +5 -2
  81. data/lib/active_record/scoping.rb +152 -0
  82. data/lib/active_record/scoping/default.rb +140 -0
  83. data/lib/active_record/scoping/named.rb +202 -0
  84. data/lib/active_record/serialization.rb +1 -43
  85. data/lib/active_record/serializers/xml_serializer.rb +2 -44
  86. data/lib/active_record/session_store.rb +11 -11
  87. data/lib/active_record/store.rb +50 -0
  88. data/lib/active_record/test_case.rb +11 -7
  89. data/lib/active_record/timestamp.rb +16 -3
  90. data/lib/active_record/transactions.rb +5 -5
  91. data/lib/active_record/translation.rb +22 -0
  92. data/lib/active_record/validations.rb +1 -1
  93. data/lib/active_record/validations/associated.rb +5 -4
  94. data/lib/active_record/validations/uniqueness.rb +4 -4
  95. data/lib/active_record/version.rb +3 -3
  96. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
  97. metadata +48 -38
  98. checksums.yaml +0 -7
  99. data/lib/active_record/named_scope.rb +0 -200
@@ -1,47 +1,44 @@
1
1
  module ActiveRecord
2
2
  class PredicateBuilder # :nodoc:
3
- def self.build_from_hash(engine, attributes, default_table, allow_table_name = true)
3
+ def self.build_from_hash(engine, attributes, default_table)
4
4
  predicates = attributes.map do |column, value|
5
5
  table = default_table
6
6
 
7
- if allow_table_name && value.is_a?(Hash)
7
+ if value.is_a?(Hash)
8
8
  table = Arel::Table.new(column, engine)
9
-
10
- if value.empty?
11
- '1 = 2'
12
- else
13
- build_from_hash(engine, value, table, false)
14
- end
9
+ build_from_hash(engine, value, table)
15
10
  else
16
11
  column = column.to_s
17
12
 
18
- if allow_table_name && column.include?('.')
13
+ if column.include?('.')
19
14
  table_name, column = column.split('.', 2)
20
15
  table = Arel::Table.new(table_name, engine)
21
16
  end
22
17
 
23
- attribute = table[column]
18
+ attribute = table[column.to_sym]
24
19
 
25
20
  case value
26
21
  when ActiveRecord::Relation
27
22
  value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
28
23
  attribute.in(value.arel.ast)
29
24
  when Array, ActiveRecord::Associations::CollectionProxy
30
- values = value.to_a.map { |x|
31
- x.is_a?(ActiveRecord::Base) ? x.id : x
32
- }
25
+ values = value.to_a.map {|x| x.is_a?(ActiveRecord::Base) ? x.id : x}
26
+ ranges, values = values.partition {|v| v.is_a?(Range) || v.is_a?(Arel::Relation)}
27
+
28
+ array_predicates = ranges.map {|range| attribute.in(range)}
33
29
 
34
30
  if values.include?(nil)
35
31
  values = values.compact
36
32
  if values.empty?
37
- attribute.eq nil
33
+ array_predicates << attribute.eq(nil)
38
34
  else
39
- attribute.in(values.compact).or attribute.eq(nil)
35
+ array_predicates << attribute.in(values.compact).or(attribute.eq(nil))
40
36
  end
41
37
  else
42
- attribute.in(values)
38
+ array_predicates << attribute.in(values)
43
39
  end
44
40
 
41
+ array_predicates.inject {|composite, predicate| composite.or(predicate)}
45
42
  when Range, Arel::Relation
46
43
  attribute.in(value)
47
44
  when ActiveRecord::Base
@@ -9,7 +9,8 @@ module ActiveRecord
9
9
  :select_values, :group_values, :order_values, :joins_values,
10
10
  :where_values, :having_values, :bind_values,
11
11
  :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value,
12
- :from_value, :reorder_value, :reverse_order_value
12
+ :from_value, :reorder_value, :reverse_order_value,
13
+ :uniq_value
13
14
 
14
15
  def includes(*args)
15
16
  args.reject! {|a| a.blank? }
@@ -38,7 +39,7 @@ module ActiveRecord
38
39
  end
39
40
 
40
41
  # Works in two unique ways.
41
- #
42
+ #
42
43
  # First: takes a block so it can be used just like Array#select.
43
44
  #
44
45
  # Model.scoped.select { |m| m.field == value }
@@ -176,6 +177,58 @@ module ActiveRecord
176
177
  relation
177
178
  end
178
179
 
180
+ # Specifies whether the records should be unique or not. For example:
181
+ #
182
+ # User.select(:name)
183
+ # # => Might return two records with the same name
184
+ #
185
+ # User.select(:name).uniq
186
+ # # => Returns 1 record per unique name
187
+ #
188
+ # User.select(:name).uniq.uniq(false)
189
+ # # => You can also remove the uniqueness
190
+ def uniq(value = true)
191
+ relation = clone
192
+ relation.uniq_value = value
193
+ relation
194
+ end
195
+
196
+ # Used to extend a scope with additional methods, either through
197
+ # a module or through a block provided.
198
+ #
199
+ # The object returned is a relation, which can be further extended.
200
+ #
201
+ # === Using a module
202
+ #
203
+ # module Pagination
204
+ # def page(number)
205
+ # # pagination code goes here
206
+ # end
207
+ # end
208
+ #
209
+ # scope = Model.scoped.extending(Pagination)
210
+ # scope.page(params[:page])
211
+ #
212
+ # You can also pass a list of modules:
213
+ #
214
+ # scope = Model.scoped.extending(Pagination, SomethingElse)
215
+ #
216
+ # === Using a block
217
+ #
218
+ # scope = Model.scoped.extending do
219
+ # def page(number)
220
+ # # pagination code goes here
221
+ # end
222
+ # end
223
+ # scope.page(params[:page])
224
+ #
225
+ # You can also use a block and a module list:
226
+ #
227
+ # scope = Model.scoped.extending(Pagination) do
228
+ # def per_page(number)
229
+ # # pagination code goes here
230
+ # end
231
+ # end
179
232
  def extending(*modules)
180
233
  modules << Module.new(&Proc.new) if block_given?
181
234
 
@@ -206,7 +259,7 @@ module ActiveRecord
206
259
  arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty?
207
260
 
208
261
  arel.take(connection.sanitize_limit(@limit_value)) if @limit_value
209
- arel.skip(@offset_value.to_i) if @offset_value
262
+ arel.skip(@offset_value) if @offset_value
210
263
 
211
264
  arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
212
265
 
@@ -216,6 +269,7 @@ module ActiveRecord
216
269
 
217
270
  build_select(arel, @select_values.uniq)
218
271
 
272
+ arel.distinct(@uniq_value)
219
273
  arel.from(@from_value) if @from_value
220
274
  arel.lock(@lock_value) if @lock_value
221
275
 
@@ -331,10 +385,11 @@ module ActiveRecord
331
385
 
332
386
  order_query.map do |o|
333
387
  case o
334
- when Arel::Nodes::Ascending, Arel::Nodes::Descending
388
+ when Arel::Nodes::Ordering
335
389
  o.reverse
336
390
  when String, Symbol
337
391
  o.to_s.split(',').collect do |s|
392
+ s.strip!
338
393
  s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
339
394
  end
340
395
  else
@@ -1,7 +1,7 @@
1
1
  module ActiveRecord
2
2
  ###
3
3
  # This class encapsulates a Result returned from calling +exec_query+ on any
4
- # database connection adapter. For example:
4
+ # database connection adapter. For example:
5
5
  #
6
6
  # x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo')
7
7
  # x # => #<ActiveRecord::Result:0xdeadbeef>
@@ -0,0 +1,194 @@
1
+ require 'active_support/concern'
2
+
3
+ module ActiveRecord
4
+ module Sanitization
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def quote_value(value, column = nil) #:nodoc:
9
+ connection.quote(value,column)
10
+ end
11
+
12
+ # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
13
+ def sanitize(object) #:nodoc:
14
+ connection.quote(object)
15
+ end
16
+
17
+ protected
18
+
19
+ # Accepts an array, hash, or string of SQL conditions and sanitizes
20
+ # them into a valid SQL fragment for a WHERE clause.
21
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
22
+ # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'"
23
+ # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
24
+ def sanitize_sql_for_conditions(condition, table_name = self.table_name)
25
+ return nil if condition.blank?
26
+
27
+ case condition
28
+ when Array; sanitize_sql_array(condition)
29
+ when Hash; sanitize_sql_hash_for_conditions(condition, table_name)
30
+ else condition
31
+ end
32
+ end
33
+ alias_method :sanitize_sql, :sanitize_sql_for_conditions
34
+
35
+ # Accepts an array, hash, or string of SQL conditions and sanitizes
36
+ # them into a valid SQL fragment for a SET clause.
37
+ # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'"
38
+ def sanitize_sql_for_assignment(assignments)
39
+ case assignments
40
+ when Array; sanitize_sql_array(assignments)
41
+ when Hash; sanitize_sql_hash_for_assignment(assignments)
42
+ else assignments
43
+ end
44
+ end
45
+
46
+ # Accepts a hash of SQL conditions and replaces those attributes
47
+ # that correspond to a +composed_of+ relationship with their expanded
48
+ # aggregate attribute values.
49
+ # Given:
50
+ # class Person < ActiveRecord::Base
51
+ # composed_of :address, :class_name => "Address",
52
+ # :mapping => [%w(address_street street), %w(address_city city)]
53
+ # end
54
+ # Then:
55
+ # { :address => Address.new("813 abc st.", "chicago") }
56
+ # # => { :address_street => "813 abc st.", :address_city => "chicago" }
57
+ def expand_hash_conditions_for_aggregates(attrs)
58
+ expanded_attrs = {}
59
+ attrs.each do |attr, value|
60
+ unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil?
61
+ mapping = aggregate_mapping(aggregation)
62
+ mapping.each do |field_attr, aggregate_attr|
63
+ if mapping.size == 1 && !value.respond_to?(aggregate_attr)
64
+ expanded_attrs[field_attr] = value
65
+ else
66
+ expanded_attrs[field_attr] = value.send(aggregate_attr)
67
+ end
68
+ end
69
+ else
70
+ expanded_attrs[attr] = value
71
+ end
72
+ end
73
+ expanded_attrs
74
+ end
75
+
76
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
77
+ # { :name => "foo'bar", :group_id => 4 }
78
+ # # => "name='foo''bar' and group_id= 4"
79
+ # { :status => nil, :group_id => [1,2,3] }
80
+ # # => "status IS NULL and group_id IN (1,2,3)"
81
+ # { :age => 13..18 }
82
+ # # => "age BETWEEN 13 AND 18"
83
+ # { 'other_records.id' => 7 }
84
+ # # => "`other_records`.`id` = 7"
85
+ # { :other_records => { :id => 7 } }
86
+ # # => "`other_records`.`id` = 7"
87
+ # And for value objects on a composed_of relationship:
88
+ # { :address => Address.new("123 abc st.", "chicago") }
89
+ # # => "address_street='123 abc st.' and address_city='chicago'"
90
+ def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
91
+ attrs = expand_hash_conditions_for_aggregates(attrs)
92
+
93
+ table = Arel::Table.new(table_name).alias(default_table_name)
94
+ PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
95
+ connection.visitor.accept b
96
+ }.join(' AND ')
97
+ end
98
+ alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
99
+
100
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
101
+ # { :status => nil, :group_id => 1 }
102
+ # # => "status = NULL , group_id = 1"
103
+ def sanitize_sql_hash_for_assignment(attrs)
104
+ attrs.map do |attr, value|
105
+ "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}"
106
+ end.join(', ')
107
+ end
108
+
109
+ # Accepts an array of conditions. The array has each value
110
+ # sanitized and interpolated into the SQL statement.
111
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
112
+ def sanitize_sql_array(ary)
113
+ statement, *values = ary
114
+ if values.first.is_a?(Hash) && statement =~ /:\w+/
115
+ replace_named_bind_variables(statement, values.first)
116
+ elsif statement.include?('?')
117
+ replace_bind_variables(statement, values)
118
+ elsif statement.blank?
119
+ statement
120
+ else
121
+ statement % values.collect { |value| connection.quote_string(value.to_s) }
122
+ end
123
+ end
124
+
125
+ alias_method :sanitize_conditions, :sanitize_sql
126
+
127
+ def replace_bind_variables(statement, values) #:nodoc:
128
+ raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
129
+ bound = values.dup
130
+ c = connection
131
+ statement.gsub('?') { quote_bound_value(bound.shift, c) }
132
+ end
133
+
134
+ def replace_named_bind_variables(statement, bind_vars) #:nodoc:
135
+ statement.gsub(/(:?):([a-zA-Z]\w*)/) do
136
+ if $1 == ':' # skip postgresql casts
137
+ $& # return the whole match
138
+ elsif bind_vars.include?(match = $2.to_sym)
139
+ quote_bound_value(bind_vars[match])
140
+ else
141
+ raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
142
+ end
143
+ end
144
+ end
145
+
146
+ def expand_range_bind_variables(bind_vars) #:nodoc:
147
+ expanded = []
148
+
149
+ bind_vars.each do |var|
150
+ next if var.is_a?(Hash)
151
+
152
+ if var.is_a?(Range)
153
+ expanded << var.first
154
+ expanded << var.last
155
+ else
156
+ expanded << var
157
+ end
158
+ end
159
+
160
+ expanded
161
+ end
162
+
163
+ def quote_bound_value(value, c = connection) #:nodoc:
164
+ if value.respond_to?(:map) && !value.acts_like?(:string)
165
+ if value.respond_to?(:empty?) && value.empty?
166
+ c.quote(nil)
167
+ else
168
+ value.map { |v| c.quote(v) }.join(',')
169
+ end
170
+ else
171
+ c.quote(value)
172
+ end
173
+ end
174
+
175
+ def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
176
+ unless expected == provided
177
+ raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
178
+ end
179
+ end
180
+ end
181
+
182
+ # TODO: Deprecate this
183
+ def quoted_id #:nodoc:
184
+ quote_value(id, column_for_attribute(self.class.primary_key))
185
+ end
186
+
187
+ private
188
+
189
+ # Quote strings appropriately for SQL statements.
190
+ def quote_value(value, column = nil)
191
+ self.class.connection.quote(value, column)
192
+ end
193
+ end
194
+ end
@@ -40,7 +40,7 @@ module ActiveRecord
40
40
  def header(stream)
41
41
  define_params = @version ? ":version => #{@version}" : ""
42
42
 
43
- if stream.respond_to?(:external_encoding) && stream.external_encoding
43
+ if stream.respond_to?(:external_encoding)
44
44
  stream.puts "# encoding: #{stream.external_encoding.name}"
45
45
  end
46
46
 
@@ -110,7 +110,7 @@ HEADER
110
110
  spec = {}
111
111
  spec[:name] = column.name.inspect
112
112
 
113
- # AR has an optimisation which handles zero-scale decimals as integers. This
113
+ # AR has an optimization which handles zero-scale decimals as integers. This
114
114
  # code ensures that the dumper still dumps the column as a decimal.
115
115
  spec[:type] = if column.type == :integer && [/^numeric/, /^decimal/].any? { |e| e.match(column.sql_type) }
116
116
  'decimal'
@@ -190,6 +190,9 @@ HEADER
190
190
  index_lengths = (index.lengths || []).compact
191
191
  statement_parts << (':length => ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
192
192
 
193
+ index_orders = (index.orders || {})
194
+ statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty?
195
+
193
196
  ' ' + statement_parts.join(', ')
194
197
  end
195
198
 
@@ -0,0 +1,152 @@
1
+ require 'active_support/concern'
2
+
3
+ module ActiveRecord
4
+ module Scoping
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include Default
9
+ include Named
10
+ end
11
+
12
+ module ClassMethods
13
+ # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be
14
+ # <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while
15
+ # <tt>:create</tt> parameters are an attributes hash.
16
+ #
17
+ # class Article < ActiveRecord::Base
18
+ # def self.create_with_scope
19
+ # with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do
20
+ # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
21
+ # a = create(1)
22
+ # a.blog_id # => 1
23
+ # end
24
+ # end
25
+ # end
26
+ #
27
+ # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
28
+ # <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged.
29
+ #
30
+ # <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing
31
+ # problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the
32
+ # array of strings format for your joins.
33
+ #
34
+ # class Article < ActiveRecord::Base
35
+ # def self.find_with_scope
36
+ # with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do
37
+ # with_scope(:find => limit(10)) do
38
+ # all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
39
+ # end
40
+ # with_scope(:find => where(:author_id => 3)) do
41
+ # all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
42
+ # end
43
+ # end
44
+ # end
45
+ # end
46
+ #
47
+ # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method.
48
+ #
49
+ # class Article < ActiveRecord::Base
50
+ # def self.find_with_exclusive_scope
51
+ # with_scope(:find => where(:blog_id => 1).limit(1)) do
52
+ # with_exclusive_scope(:find => limit(10)) do
53
+ # all # => SELECT * from articles LIMIT 10
54
+ # end
55
+ # end
56
+ # end
57
+ # end
58
+ #
59
+ # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+.
60
+ def with_scope(scope = {}, action = :merge, &block)
61
+ # If another Active Record class has been passed in, get its current scope
62
+ scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope)
63
+
64
+ previous_scope = self.current_scope
65
+
66
+ if scope.is_a?(Hash)
67
+ # Dup first and second level of hash (method and params).
68
+ scope = scope.dup
69
+ scope.each do |method, params|
70
+ scope[method] = params.dup unless params == true
71
+ end
72
+
73
+ scope.assert_valid_keys([ :find, :create ])
74
+ relation = construct_finder_arel(scope[:find] || {})
75
+ relation.default_scoped = true unless action == :overwrite
76
+
77
+ if previous_scope && previous_scope.create_with_value && scope[:create]
78
+ scope_for_create = if action == :merge
79
+ previous_scope.create_with_value.merge(scope[:create])
80
+ else
81
+ scope[:create]
82
+ end
83
+
84
+ relation = relation.create_with(scope_for_create)
85
+ else
86
+ scope_for_create = scope[:create]
87
+ scope_for_create ||= previous_scope.create_with_value if previous_scope
88
+ relation = relation.create_with(scope_for_create) if scope_for_create
89
+ end
90
+
91
+ scope = relation
92
+ end
93
+
94
+ scope = previous_scope.merge(scope) if previous_scope && action == :merge
95
+
96
+ self.current_scope = scope
97
+ begin
98
+ yield
99
+ ensure
100
+ self.current_scope = previous_scope
101
+ end
102
+ end
103
+
104
+ protected
105
+
106
+ # Works like with_scope, but discards any nested properties.
107
+ def with_exclusive_scope(method_scoping = {}, &block)
108
+ if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) }
109
+ raise ArgumentError, <<-MSG
110
+ New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope:
111
+
112
+ User.unscoped.where(:active => true)
113
+
114
+ Or call unscoped with a block:
115
+
116
+ User.unscoped do
117
+ User.where(:active => true).all
118
+ end
119
+
120
+ MSG
121
+ end
122
+ with_scope(method_scoping, :overwrite, &block)
123
+ end
124
+
125
+ def current_scope #:nodoc:
126
+ Thread.current["#{self}_current_scope"]
127
+ end
128
+
129
+ def current_scope=(scope) #:nodoc:
130
+ Thread.current["#{self}_current_scope"] = scope
131
+ end
132
+
133
+ private
134
+
135
+ def construct_finder_arel(options = {}, scope = nil)
136
+ relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
137
+ relation = scope.merge(relation) if scope
138
+ relation
139
+ end
140
+
141
+ end
142
+
143
+ def populate_with_current_scope_attributes
144
+ return unless self.class.scope_attributes?
145
+
146
+ self.class.scope_attributes.each do |att,value|
147
+ send("#{att}=", value) if respond_to?("#{att}=")
148
+ end
149
+ end
150
+
151
+ end
152
+ end