activerecord 1.3.0 → 1.4.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 (75) hide show
  1. data/CHANGELOG +77 -2
  2. data/install.rb +5 -0
  3. data/lib/active_record.rb +6 -2
  4. data/lib/active_record/acts/list.rb +56 -45
  5. data/lib/active_record/acts/tree.rb +3 -2
  6. data/lib/active_record/associations.rb +10 -62
  7. data/lib/active_record/associations/association_collection.rb +20 -23
  8. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +36 -10
  9. data/lib/active_record/associations/has_many_association.rb +50 -25
  10. data/lib/active_record/base.rb +118 -80
  11. data/lib/active_record/callbacks.rb +51 -50
  12. data/lib/active_record/connection_adapters/abstract_adapter.rb +33 -12
  13. data/lib/active_record/connection_adapters/db2_adapter.rb +129 -0
  14. data/lib/active_record/connection_adapters/sqlite_adapter.rb +23 -1
  15. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +80 -90
  16. data/lib/active_record/fixtures.rb +1 -1
  17. data/lib/active_record/locking.rb +57 -0
  18. data/lib/active_record/support/class_attribute_accessors.rb +19 -5
  19. data/lib/active_record/support/class_inheritable_attributes.rb +4 -3
  20. data/lib/active_record/support/dependencies.rb +71 -0
  21. data/lib/active_record/support/inflector.rb +1 -0
  22. data/lib/active_record/support/misc.rb +29 -3
  23. data/lib/active_record/support/module_attribute_accessors.rb +57 -0
  24. data/lib/active_record/transactions.rb +18 -11
  25. data/lib/active_record/validations.rb +3 -3
  26. data/lib/active_record/vendor/db2.rb +357 -0
  27. data/lib/active_record/vendor/mysql.rb +1 -1
  28. data/rakefile +17 -5
  29. data/test/associations_test.rb +39 -4
  30. data/test/base_test.rb +13 -4
  31. data/test/binary_test.rb +43 -0
  32. data/test/callbacks_test.rb +230 -0
  33. data/test/connections/native_db2/connection.rb +24 -0
  34. data/test/connections/native_sqlserver/connection.rb +9 -3
  35. data/test/deprecated_associations_test.rb +16 -10
  36. data/test/finder_test.rb +65 -13
  37. data/test/fixtures/associations.png +0 -0
  38. data/test/fixtures/binary.rb +2 -0
  39. data/test/fixtures/companies.yml +21 -0
  40. data/test/fixtures/courses.yml +7 -0
  41. data/test/fixtures/customers.yml +7 -0
  42. data/test/fixtures/db_definitions/db2.sql +124 -0
  43. data/test/fixtures/db_definitions/db22.sql +4 -0
  44. data/test/fixtures/db_definitions/mysql.sql +12 -0
  45. data/test/fixtures/db_definitions/postgresql.sql +13 -0
  46. data/test/fixtures/db_definitions/sqlite.sql +9 -0
  47. data/test/fixtures/db_definitions/sqlserver.sql +13 -0
  48. data/test/fixtures/developers_projects.yml +13 -0
  49. data/test/fixtures/entrants.yml +14 -0
  50. data/test/fixtures/fixture_database.sqlite +0 -0
  51. data/test/fixtures/movies.yml +7 -0
  52. data/test/fixtures/people.yml +3 -0
  53. data/test/fixtures/person.rb +1 -0
  54. data/test/fixtures/projects.yml +7 -0
  55. data/test/fixtures/topics.yml +21 -0
  56. data/test/locking_test.rb +34 -0
  57. data/test/mixin_test.rb +6 -0
  58. data/test/validations_test.rb +1 -1
  59. metadata +33 -29
  60. data/test/fixtures/companies/first_client +0 -6
  61. data/test/fixtures/companies/first_firm +0 -4
  62. data/test/fixtures/companies/second_client +0 -6
  63. data/test/fixtures/courses/java +0 -2
  64. data/test/fixtures/courses/ruby +0 -2
  65. data/test/fixtures/customers/david +0 -6
  66. data/test/fixtures/entrants/first +0 -3
  67. data/test/fixtures/entrants/second +0 -3
  68. data/test/fixtures/entrants/third +0 -3
  69. data/test/fixtures/movies/first +0 -2
  70. data/test/fixtures/movies/second +0 -2
  71. data/test/fixtures/projects/action_controller +0 -2
  72. data/test/fixtures/projects/active_record +0 -2
  73. data/test/fixtures/topics/first +0 -10
  74. data/test/fixtures/topics/second +0 -8
  75. data/test/inflector_test.rb +0 -122
@@ -38,15 +38,42 @@ module ActiveRecord
38
38
  self
39
39
  end
40
40
 
41
- def find(association_id = nil, &block)
42
- if block_given? || @options[:finder_sql]
43
- load_collection
44
- @collection.find(&block)
41
+ def find_first
42
+ load_collection.first
43
+ end
44
+
45
+ def find(*args)
46
+ # Return an Array if multiple ids are given.
47
+ expects_array = args.first.kind_of?(Array)
48
+
49
+ ids = args.flatten.compact.uniq
50
+
51
+ # If no block is given, raise RecordNotFound.
52
+ if ids.empty?
53
+ raise RecordNotFound, "Couldn't find #{@association_class.name} without an ID#{conditions}"
54
+
55
+ # If using a custom finder_sql, scan the entire collection.
56
+ elsif @options[:finder_sql]
57
+ if ids.size == 1
58
+ id = ids.first
59
+ record = load_collection.detect { |record| id == record.id }
60
+ expects_array? ? [record] : record
61
+ else
62
+ load_collection.select { |record| ids.include?(record.id) }
63
+ end
64
+
65
+ # Otherwise, construct a query.
45
66
  else
46
- if loaded?
47
- find_all { |record| record.id == association_id.to_i }.first
67
+ ids_list = ids.map { |id| @owner.send(:quote, id) }.join(',')
68
+ records = find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} IN (#{ids_list}) ORDER BY"))
69
+ if records.size == ids.size
70
+ if ids.size == 1 and !expects_array
71
+ records.first
72
+ else
73
+ records
74
+ end
48
75
  else
49
- find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} = #{@owner.send(:quote, association_id)} ORDER BY")).first
76
+ raise RecordNotFound, "Couldn't find #{@association_class.name} with ID in (#{ids_list})"
50
77
  end
51
78
  end
52
79
  end
@@ -70,10 +97,9 @@ module ActiveRecord
70
97
  records = @association_class.find_by_sql(sql)
71
98
  @options[:uniq] ? uniq(records) : records
72
99
  end
73
-
100
+
74
101
  def count_records
75
- load_collection
76
- @collection.size
102
+ load_collection.size
77
103
  end
78
104
 
79
105
  def insert_record(record)
@@ -3,12 +3,13 @@ module ActiveRecord
3
3
  class HasManyAssociation < AssociationCollection #:nodoc:
4
4
  def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
5
5
  super(owner, association_name, association_class_name, association_class_primary_key_name, options)
6
- @conditions = @association_class.send(:sanitize_conditions, options[:conditions])
6
+ @conditions = sanitize_sql(options[:conditions])
7
7
 
8
8
  if options[:finder_sql]
9
9
  @finder_sql = interpolate_sql(options[:finder_sql])
10
10
  else
11
- @finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id} #{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
11
+ @finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
12
+ @finder_sql << " AND #{@conditions}" if @conditions
12
13
  end
13
14
 
14
15
  if options[:counter_sql]
@@ -35,29 +36,57 @@ module ActiveRecord
35
36
  record
36
37
  end
37
38
 
38
- def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil, &block)
39
- if block_given? || @options[:finder_sql]
40
- load_collection
41
- @collection.find_all(&block)
39
+ def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil)
40
+ if @options[:finder_sql]
41
+ records = @association_class.find_by_sql(@finder_sql)
42
42
  else
43
- @association_class.find_all(
44
- "#{@association_class_primary_key_name} = #{@owner.quoted_id}" +
45
- "#{@conditions ? " AND " + @conditions : ""}#{runtime_conditions ? " AND " + @association_class.send(:sanitize_conditions, runtime_conditions) : ""}",
46
- orderings,
47
- limit,
48
- joins
49
- )
43
+ sql = @finder_sql
44
+ sql << " AND #{sanitize_sql(runtime_conditions)}" if runtime_conditions
45
+ orderings ||= @options[:order]
46
+ records = @association_class.find_all(sql, orderings, limit, joins)
50
47
  end
51
48
  end
52
49
 
53
- def find(association_id = nil, &block)
54
- if block_given? || @options[:finder_sql]
55
- load_collection
56
- @collection.find(&block)
50
+ # Count the number of associated records. All arguments are optional.
51
+ def count(runtime_conditions = nil)
52
+ if @options[:finder_sql]
53
+ @association_class.count_by_sql(@finder_sql)
57
54
  else
58
- @association_class.find_on_conditions(association_id,
59
- "#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@conditions ? " AND " + @conditions : ""}"
60
- )
55
+ sql = @finder_sql
56
+ sql << " AND #{sanitize_sql(runtime_conditions)}" if runtime_conditions
57
+ @association_class.count(sql)
58
+ end
59
+ end
60
+
61
+ # Find the first associated record. All arguments are optional.
62
+ def find_first(conditions = nil, orderings = nil)
63
+ find_all(conditions, orderings, 1).first
64
+ end
65
+
66
+ def find(*args)
67
+ # Return an Array if multiple ids are given.
68
+ expects_array = args.first.kind_of?(Array)
69
+
70
+ ids = args.flatten.compact.uniq
71
+
72
+ # If no ids given, raise RecordNotFound.
73
+ if ids.empty?
74
+ raise RecordNotFound, "Couldn't find #{@association_class.name} without an ID"
75
+
76
+ # If using a custom finder_sql, scan the entire collection.
77
+ elsif @options[:finder_sql]
78
+ if ids.size == 1
79
+ id = ids.first
80
+ record = load_collection.detect { |record| id == record.id }
81
+ expects_array? ? [record] : record
82
+ else
83
+ load_collection.select { |record| ids.include?(record.id) }
84
+ end
85
+
86
+ # Otherwise, delegate to association class with conditions.
87
+ else
88
+ args << { :conditions => "#{@association_class_primary_key_name} = #{@owner.quoted_id} #{@conditions ? " AND " + @conditions : ""}" }
89
+ @association_class.find(*args)
61
90
  end
62
91
  end
63
92
 
@@ -71,11 +100,7 @@ module ActiveRecord
71
100
 
72
101
  protected
73
102
  def find_all_records
74
- if @options[:finder_sql]
75
- @association_class.find_by_sql(@finder_sql)
76
- else
77
- @association_class.find_all(@finder_sql, @options[:order] ? @options[:order] : nil)
78
- end
103
+ find_all
79
104
  end
80
105
 
81
106
  def count_records
@@ -26,6 +26,8 @@ module ActiveRecord #:nodoc:
26
26
  end
27
27
  class PreparedStatementInvalid < ActiveRecordError #:nodoc:
28
28
  end
29
+ class StaleObjectError < ActiveRecordError #:nodoc:
30
+ end
29
31
 
30
32
  # Active Record objects doesn't specify their attributes directly, but rather infer them from the table definition with
31
33
  # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
@@ -95,6 +97,21 @@ module ActiveRecord #:nodoc:
95
97
  # end
96
98
  # end
97
99
  #
100
+ # == Dynamic attribute-based finders
101
+ #
102
+ # Dynamic attribute-based finders are a cleaner way of getting objects by simple queries without turning to SQL. They work by
103
+ # appending the name of an attribute to <tt>find_by_</tt>, so you get finders like <tt>Person.find_by_user_name, Payment.find_by_transaction_id</tt>.
104
+ # So instead of writing <tt>Person.find_first(["user_name = ?", user_name])</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
105
+ #
106
+ # It's also possible to use multiple attributes in the same find by separating them with "_and_", so you get finders like
107
+ # <tt>Person.find_by_user_name_and_password</tt> or even <tt>Payment.find_by_purchaser_and_state_and_country</tt>. So instead of writing
108
+ # <tt>Person.find_first(["user_name = ? AND password = ?", user_name, password])</tt>, you just do
109
+ # <tt>Person.find_by_user_name_and_password(user_name, password)</tt>.
110
+ #
111
+ # It's even possible to use all the additional parameters to find_first and find_all. For example, the full interface for Payment.find_all_by_amount
112
+ # is actually Payment.find_all_by_amount(amount, orderings = nil, limit = nil, joins = nil). And the full interface to Person.find_by_user_name is
113
+ # actually Person.find_by_user_name(user_name, orderings = nil)
114
+ #
98
115
  # == Saving arrays, hashes, and other non-mappeable objects in text columns
99
116
  #
100
117
  # Active Record can serialize any object in text columns using YAML. To do so, you must specify this with a call to the class method +serialize+.
@@ -221,14 +238,10 @@ module ActiveRecord #:nodoc:
221
238
  cattr_accessor :pluralize_table_names
222
239
  @@pluralize_table_names = true
223
240
 
224
- # When turned on (which is default), all associations are included using "load". This mean that any change is instant in cached
225
- # environments like mod_ruby or FastCGI. When set to false, "require" is used, which is faster but requires server restart to
226
- # reflect changes.
227
- @@reload_associations = true
228
- cattr_accessor :reload_associations
229
-
230
- @@associations_loaded = []
231
- cattr_accessor :associations_loaded
241
+ # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates and times from the database.
242
+ # This is set to :local by default.
243
+ cattr_accessor :default_timezone
244
+ @@default_timezone = :local
232
245
 
233
246
  class << self # Class methods
234
247
  # Returns objects for the records responding to either a specific id (1), a list of ids (1, 5, 6) or an array of ids.
@@ -238,44 +251,58 @@ module ActiveRecord #:nodoc:
238
251
  # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
239
252
  # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
240
253
  # Person.find([1]) # returns an array for objects the object with ID = 1
254
+ #
255
+ # The last argument may be a Hash of find options. Currently, +conditions+ is the only option, behaving the same as with +find_all+.
256
+ # Person.find(1, :conditions => "associate_id = 5"
257
+ # Person.find(1, 2, 6, :conditions => "status = 'active'"
258
+ # Person.find([7, 17], :conditions => ["sanitize_me = ?", "bare'quote"]
259
+ #
241
260
  # +RecordNotFound+ is raised if no record can be found.
242
- def find(*ids)
243
- expects_array = ids.first.kind_of?(Array)
244
- ids = ids.flatten.compact.uniq
261
+ def find(*args)
262
+ # Return an Array if ids are passed in an Array.
263
+ expects_array = args.first.kind_of?(Array)
264
+
265
+ # Extract options hash from argument list.
266
+ options = extract_options_from_args!(args)
267
+ conditions = " AND #{sanitize_sql(options[:conditions])}" if options[:conditions]
268
+
269
+ ids = args.flatten.compact.uniq
270
+ case ids.size
245
271
 
246
- if ids.length > 1
247
- ids_list = ids.map{ |id| "#{sanitize(id)}" }.join(", ")
248
- objects = find_all("#{primary_key} IN (#{ids_list})", primary_key)
272
+ # Raise if no ids passed.
273
+ when 0
274
+ raise RecordNotFound, "Couldn't find #{name} without an ID#{conditions}"
249
275
 
250
- if objects.length == ids.length
251
- return objects
276
+ # Find a single id.
277
+ when 1
278
+ unless result = find_first("#{primary_key} = #{sanitize(ids.first)}#{conditions}")
279
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{ids.first}#{conditions}"
280
+ end
281
+
282
+ # Box result if expecting array.
283
+ expects_array ? [result] : result
284
+
285
+ # Find multiple ids.
252
286
  else
253
- raise RecordNotFound, "Couldn't find #{name} with ID in (#{ids_list})"
254
- end
255
- elsif ids.length == 1
256
- id = ids.first
257
- sql = "SELECT * FROM #{table_name} WHERE #{primary_key} = #{sanitize(id)}"
258
- sql << " AND #{type_condition}" unless descends_from_active_record?
259
-
260
- if record = connection.select_one(sql, "#{name} Find")
261
- expects_array ? [instantiate(record)] : instantiate(record)
262
- else
263
- raise RecordNotFound, "Couldn't find #{name} with ID = #{id}"
264
- end
265
- else
266
- raise RecordNotFound, "Couldn't find #{name} without an ID"
287
+ ids_list = ids.map { |id| sanitize(id) }.join(',')
288
+ result = find_all("#{primary_key} IN (#{ids_list})#{conditions}", primary_key)
289
+ if result.size == ids.size
290
+ result
291
+ else
292
+ raise RecordNotFound, "Couldn't find #{name} with ID in (#{ids_list})#{conditions}"
293
+ end
267
294
  end
268
295
  end
269
296
 
297
+ # This method is deprecated in favor of find with the :conditions option.
270
298
  # Works like find, but the record matching +id+ must also meet the +conditions+.
271
299
  # +RecordNotFound+ is raised if no record can be found matching the +id+ or meeting the condition.
272
300
  # Example:
273
301
  # Person.find_on_conditions 5, "first_name LIKE '%dav%' AND last_name = 'heinemeier'"
274
- def find_on_conditions(id, conditions)
275
- find_first("#{primary_key} = #{sanitize(id)} AND #{sanitize_conditions(conditions)}") ||
276
- raise(RecordNotFound, "Couldn't find #{name} with #{primary_key} = #{id} on the condition of #{conditions}")
302
+ def find_on_conditions(ids, conditions)
303
+ find(ids, :conditions => conditions)
277
304
  end
278
-
305
+
279
306
  # Returns an array of all the objects that could be instantiated from the associated
280
307
  # table in the database. The +conditions+ can be used to narrow the selection of objects (WHERE-part),
281
308
  # such as by "color = 'red'", and arrangement of the selection can be done through +orderings+ (ORDER BY-part),
@@ -288,8 +315,9 @@ module ActiveRecord #:nodoc:
288
315
  sql << "#{joins} " if joins
289
316
  add_conditions!(sql, conditions)
290
317
  sql << "ORDER BY #{orderings} " unless orderings.nil?
291
- sql << "LIMIT #{sanitize_conditions(limit)} " unless limit.nil?
292
-
318
+
319
+ connection.add_limit!(sql, sanitize_sql(limit)) unless limit.nil?
320
+
293
321
  find_by_sql(sql)
294
322
  end
295
323
 
@@ -297,8 +325,7 @@ module ActiveRecord #:nodoc:
297
325
  # Post.find_by_sql "SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id"
298
326
  # Post.find_by_sql ["SELECT * FROM posts WHERE author = ? AND created > ?", author_id, start_date]
299
327
  def find_by_sql(sql)
300
- sql = sanitize_conditions(sql)
301
- connection.select_all(sql, "#{name} Load").inject([]) { |objects, record| objects << instantiate(record) }
328
+ connection.select_all(sanitize_sql(sql), "#{name} Load").inject([]) { |objects, record| objects << instantiate(record) }
302
329
  end
303
330
 
304
331
  # Returns the object for the first record responding to the conditions in +conditions+,
@@ -307,13 +334,7 @@ module ActiveRecord #:nodoc:
307
334
  # +orderings+, like "income DESC, name", to control exactly which record is to be used. Example:
308
335
  # Employee.find_first "income > 50000", "income DESC, name"
309
336
  def find_first(conditions = nil, orderings = nil)
310
- sql = "SELECT * FROM #{table_name} "
311
- add_conditions!(sql, conditions)
312
- sql << "ORDER BY #{orderings} " unless orderings.nil?
313
- sql << "LIMIT 1"
314
-
315
- record = connection.select_one(sql, "#{name} Load First")
316
- instantiate(record) unless record.nil?
337
+ find_all(conditions, orderings, 1).first
317
338
  end
318
339
 
319
340
  # Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save
@@ -613,7 +634,7 @@ module ActiveRecord #:nodoc:
613
634
 
614
635
  # Adds a sanitized version of +conditions+ to the +sql+ string. Note that it's the passed +sql+ string is changed.
615
636
  def add_conditions!(sql, conditions)
616
- sql << "WHERE #{sanitize_conditions(conditions)} " unless conditions.nil?
637
+ sql << "WHERE #{sanitize_sql(conditions)} " unless conditions.nil?
617
638
  sql << (conditions.nil? ? "WHERE " : " AND ") + type_condition unless descends_from_active_record?
618
639
  end
619
640
 
@@ -630,6 +651,24 @@ module ActiveRecord #:nodoc:
630
651
  return table_name
631
652
  end
632
653
 
654
+ # Enables dynamic finders like find_by_user_name(user_name) and find_by_user_name_and_password(user_name, password) that are turned into
655
+ # find_first(["user_name = ?", user_name]) and find_first(["user_name = ? AND password = ?", user_name, password]) respectively. Also works
656
+ # for find_all, but using find_all_by_amount(50) that are turned into find_all(["amount = ?", 50]).
657
+ #
658
+ # It's even possible to use all the additional parameters to find_first and find_all. For example, the full interface for find_all_by_amount
659
+ # is actually find_all_by_amount(amount, orderings = nil, limit = nil, joins = nil).
660
+ def method_missing(method_id, *arguments)
661
+ method_name = method_id.id2name
662
+
663
+ if method_name =~ /find_(all_by|by)_([_a-z]+)/
664
+ finder, attributes = ($1 == "all_by" ? :find_all : :find_first), $2.split("_and_")
665
+ attributes.each { |attr_name| super unless column_methods_hash[attr_name.intern] }
666
+ conditions = attributes.collect { |attr_name| "#{attr_name} = ? "}.join(" AND ")
667
+ send(finder, [conditions, *arguments[0...attributes.length]], *arguments[attributes.length..-1])
668
+ else
669
+ super
670
+ end
671
+ end
633
672
 
634
673
  protected
635
674
  def subclasses
@@ -656,51 +695,50 @@ module ActiveRecord #:nodoc:
656
695
  end
657
696
  end
658
697
 
659
- # Accepts either a condition array or string. The string is returned untouched, but the array has each of
660
- # the condition values sanitized.
661
- def sanitize_conditions(conditions)
662
- return conditions unless conditions.is_a?(Array)
663
-
664
- statement, *values = conditions
698
+ # Accepts an array or string. The string is returned untouched, but the array has each value
699
+ # sanitized and interpolated into the sql statement.
700
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
701
+ def sanitize_sql(ary)
702
+ return ary unless ary.is_a?(Array)
665
703
 
666
- if values[0].is_a?(Hash) && statement =~ /:\w+/
667
- replace_named_bind_variables(statement, values[0])
668
- elsif statement =~ /\?/
704
+ statement, *values = ary
705
+ if values.first.is_a?(Hash) and statement =~ /:\w+/
706
+ replace_named_bind_variables(statement, values.first)
707
+ elsif statement.include?('?')
669
708
  replace_bind_variables(statement, values)
670
709
  else
671
710
  statement % values.collect { |value| connection.quote_string(value.to_s) }
672
711
  end
673
712
  end
674
713
 
675
- def replace_bind_variables(statement, values)
676
- orig_statement = statement.clone
677
- expected_number_of_variables = statement.count('?')
678
- provided_number_of_variables = values.size
679
-
680
- unless expected_number_of_variables == provided_number_of_variables
681
- raise PreparedStatementInvalid, "wrong number of bind variables (#{provided_number_of_variables} for #{expected_number_of_variables})"
682
- end
714
+ alias_method :sanitize_conditions, :sanitize_sql
683
715
 
684
- until values.empty?
685
- statement.sub!(/\?/, encode_quoted_value(values.shift))
686
- end
687
-
688
- statement.gsub('?') { |all, match| connection.quote(values.shift) }
716
+ def replace_bind_variables(statement, values)
717
+ raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
718
+ bound = values.dup
719
+ statement.gsub('?') { connection.quote(bound.shift) }
689
720
  end
690
721
 
691
- def replace_named_bind_variables(statement, values_hash)
692
- orig_statement = statement.clone
693
- values_hash.keys.each do |k|
694
- if statement.sub!(/:#{k.id2name}/, encode_quoted_value(values_hash.delete(k))).nil?
695
- raise PreparedStatementInvalid, ":#{k} is not a variable in [#{orig_statement}]"
722
+ def replace_named_bind_variables(statement, bind_vars)
723
+ raise_if_bind_arity_mismatch(statement, statement.scan(/:(\w+)/).uniq.size, bind_vars.size)
724
+ statement.gsub(/:(\w+)/) do
725
+ match = $1.to_sym
726
+ if bind_vars.has_key?(match)
727
+ connection.quote(bind_vars[match])
728
+ else
729
+ raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
696
730
  end
697
731
  end
732
+ end
698
733
 
699
- if statement =~ /(:\w+)/
700
- raise PreparedStatementInvalid, "No value provided for #{$1} in [#{orig_statement}]"
734
+ def raise_if_bind_arity_mismatch(statement, expected, provided)
735
+ unless expected == provided
736
+ raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
701
737
  end
738
+ end
702
739
 
703
- return statement
740
+ def extract_options_from_args!(args)
741
+ if args.last.is_a?(Hash) then args.pop else {} end
704
742
  end
705
743
 
706
744
  def encode_quoted_value(value)
@@ -867,8 +905,8 @@ module ActiveRecord #:nodoc:
867
905
 
868
906
  # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
869
907
  # person.respond_to?("name?") which will all return true.
870
- def respond_to?(method)
871
- self.class.column_methods_hash[method.to_sym] || respond_to_without_attributes?(method)
908
+ def respond_to?(method, include_priv = false)
909
+ self.class.column_methods_hash[method.to_sym] || respond_to_without_attributes?(method, include_priv)
872
910
  end
873
911
 
874
912
  private
@@ -919,7 +957,7 @@ module ActiveRecord #:nodoc:
919
957
  # table with a master_id foreign key can instantiate master through Client#master.
920
958
  def method_missing(method_id, *arguments)
921
959
  method_name = method_id.id2name
922
-
960
+
923
961
  if method_name =~ read_method? && @attributes.include?($1)
924
962
  return read_attribute($1)
925
963
  elsif method_name =~ read_untyped_method? && @attributes.include?($1)
@@ -959,7 +997,7 @@ module ActiveRecord #:nodoc:
959
997
 
960
998
  # Returns true if the attribute is of a text column and marked for serialization.
961
999
  def unserializable_attribute?(attr_name, column)
962
- @attributes[attr_name] && column.send(:type) == :text && @attributes[attr_name].is_a?(String) && self.class.serialized_attributes[attr_name]
1000
+ @attributes[attr_name] && [:text, :string].include?(column.send(:type)) && @attributes[attr_name].is_a?(String) && self.class.serialized_attributes[attr_name]
963
1001
  end
964
1002
 
965
1003
  # Returns the unserialized object of the attribute.