activerecord 1.1.0 → 1.2.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 (68) hide show
  1. data/CHANGELOG +250 -0
  2. data/README +17 -9
  3. data/dev-utils/eval_debugger.rb +1 -1
  4. data/install.rb +3 -1
  5. data/lib/active_record.rb +9 -2
  6. data/lib/active_record/acts/list.rb +178 -0
  7. data/lib/active_record/acts/tree.rb +44 -0
  8. data/lib/active_record/associations.rb +45 -8
  9. data/lib/active_record/associations/association_collection.rb +18 -9
  10. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +14 -13
  11. data/lib/active_record/associations/has_many_association.rb +21 -12
  12. data/lib/active_record/base.rb +137 -37
  13. data/lib/active_record/callbacks.rb +30 -25
  14. data/lib/active_record/connection_adapters/abstract_adapter.rb +57 -33
  15. data/lib/active_record/connection_adapters/mysql_adapter.rb +4 -0
  16. data/lib/active_record/connection_adapters/sqlite_adapter.rb +3 -2
  17. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +298 -0
  18. data/lib/active_record/fixtures.rb +241 -147
  19. data/lib/active_record/support/class_inheritable_attributes.rb +5 -2
  20. data/lib/active_record/support/inflector.rb +13 -12
  21. data/lib/active_record/support/misc.rb +6 -0
  22. data/lib/active_record/timestamp.rb +33 -0
  23. data/lib/active_record/transactions.rb +1 -1
  24. data/lib/active_record/validations.rb +294 -16
  25. data/rakefile +3 -7
  26. data/test/abstract_unit.rb +1 -4
  27. data/test/associations_test.rb +17 -4
  28. data/test/base_test.rb +37 -5
  29. data/test/connections/native_sqlserver/connection.rb +15 -0
  30. data/test/deprecated_associations_test.rb +40 -38
  31. data/test/finder_test.rb +82 -4
  32. data/test/fixtures/accounts.yml +8 -0
  33. data/test/fixtures/company.rb +6 -0
  34. data/test/fixtures/company_in_module.rb +1 -1
  35. data/test/fixtures/db_definitions/mysql.sql +13 -0
  36. data/test/fixtures/db_definitions/postgresql.sql +13 -0
  37. data/test/fixtures/db_definitions/sqlite.sql +14 -0
  38. data/test/fixtures/db_definitions/sqlserver.sql +110 -0
  39. data/test/fixtures/db_definitions/sqlserver2.sql +4 -0
  40. data/test/fixtures/developer.rb +2 -2
  41. data/test/fixtures/developers.yml +13 -0
  42. data/test/fixtures/fixture_database.sqlite +0 -0
  43. data/test/fixtures/fixture_database_2.sqlite +0 -0
  44. data/test/fixtures/mixin.rb +17 -0
  45. data/test/fixtures/mixins.yml +14 -0
  46. data/test/fixtures/naked/csv/accounts.csv +1 -0
  47. data/test/fixtures/naked/yml/accounts.yml +1 -0
  48. data/test/fixtures/naked/yml/companies.yml +1 -0
  49. data/test/fixtures/naked/yml/courses.yml +1 -0
  50. data/test/fixtures/project.rb +6 -0
  51. data/test/fixtures/reply.rb +14 -1
  52. data/test/fixtures/topic.rb +2 -2
  53. data/test/fixtures/topics/first +1 -0
  54. data/test/fixtures_test.rb +42 -12
  55. data/test/inflector_test.rb +2 -1
  56. data/test/inheritance_test.rb +22 -12
  57. data/test/mixin_test.rb +138 -0
  58. data/test/pk_test.rb +4 -2
  59. data/test/reflection_test.rb +3 -3
  60. data/test/transactions_test.rb +15 -0
  61. data/test/validations_test.rb +229 -4
  62. metadata +24 -10
  63. data/lib/active_record/associations.rb.orig +0 -555
  64. data/test/deprecated_associations_test.rb.orig +0 -334
  65. data/test/fixtures/accounts/signals37 +0 -3
  66. data/test/fixtures/accounts/unknown +0 -2
  67. data/test/fixtures/developers/david +0 -2
  68. data/test/fixtures/developers/jamis +0 -2
@@ -0,0 +1,44 @@
1
+ module ActiveRecord
2
+ module Acts #:nodoc:
3
+ module Tree #:nodoc:
4
+ def self.append_features(base)
5
+ super
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ # Specify this act if you want to model a tree structure by providing a parent association and an children
10
+ # association. This act assumes that requires that you have a foreign key column, which by default is called parent_id.
11
+ #
12
+ # class Category < ActiveRecord::Base
13
+ # acts_as_tree :order => "name"
14
+ # end
15
+ #
16
+ # Example :
17
+ # root
18
+ # \_ child1
19
+ # \_ sub-child1
20
+ #
21
+ # root = Category.create("name" => "root")
22
+ # child1 = root.children.create("name" => "child1")
23
+ # subchild1 = child1.children.create("name" => "subchild1")
24
+ #
25
+ # root.parent # => nil
26
+ # child1.parent # => root
27
+ # root.children # => [child1]
28
+ # root.children.first.children.first # => subchild1
29
+ module ClassMethods
30
+ # Configuration options are:
31
+ #
32
+ # * <tt>foreign_key</tt> - specifies the column name to use for track of the tree (default: parent_id)
33
+ # * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
34
+ def acts_as_tree(options = {})
35
+ configuration = { :foreign_key => "parent_id", :order => nil }
36
+ configuration.update(options) if options.is_a?(Hash)
37
+
38
+ belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key]
39
+ has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => true
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -3,6 +3,10 @@ require 'active_record/associations/has_many_association'
3
3
  require 'active_record/associations/has_and_belongs_to_many_association'
4
4
  require 'active_record/deprecated_associations'
5
5
 
6
+ unless Object.respond_to?(:require_association)
7
+ Object.send(:define_method, :require_association) { |file_name| ActiveRecord::Base.require_association(file_name) }
8
+ end
9
+
6
10
  module ActiveRecord
7
11
  module Associations # :nodoc:
8
12
  def self.append_features(base)
@@ -166,6 +170,8 @@ module ActiveRecord
166
170
  # May not be set if :dependent is also set.
167
171
  # * <tt>:finder_sql</tt> - specify a complete SQL statement to fetch the association. This is a good way to go for complex
168
172
  # associations that depends on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
173
+ # * <tt>:counter_sql</tt> - specify a complete SQL statement to fetch the size of the association. If +:finder_sql+ is
174
+ # specified but +:counter_sql+, +:counter_sql+ will be generated by replacing SELECT ... FROM with SELECT COUNT(*) FROM.
169
175
  #
170
176
  # Option examples:
171
177
  # has_many :comments, :order => "posted_on"
@@ -177,16 +183,18 @@ module ActiveRecord
177
183
  # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
178
184
  # 'ORDER BY p.first_name'
179
185
  def has_many(association_id, options = {})
180
- validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql ], options.keys)
186
+ validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql, :counter_sql ], options.keys)
181
187
  association_name, association_class_name, association_class_primary_key_name =
182
188
  associate_identification(association_id, options[:class_name], options[:foreign_key])
183
189
 
190
+ require_association_class(association_class_name)
191
+
184
192
  if options[:dependent] and options[:exclusively_dependent]
185
- raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.'
193
+ raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' # ' ruby-mode
186
194
  elsif options[:dependent]
187
195
  module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
188
196
  elsif options[:exclusively_dependent]
189
- module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = '\#{record.id}')) }"
197
+ module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
190
198
  end
191
199
 
192
200
  define_method(association_name) do |*params|
@@ -260,6 +268,8 @@ module ActiveRecord
260
268
  association_name, association_class_name, class_primary_key_name =
261
269
  associate_identification(association_id, options[:class_name], options[:foreign_key], false)
262
270
 
271
+ require_association_class(association_class_name)
272
+
263
273
  has_one_writer_method(association_name, association_class_name, class_primary_key_name)
264
274
  build_method("build_", association_name, association_class_name, class_primary_key_name)
265
275
  create_method("create_", association_name, association_class_name, class_primary_key_name)
@@ -310,12 +320,14 @@ module ActiveRecord
310
320
  association_name, association_class_name, class_primary_key_name =
311
321
  associate_identification(association_id, options[:class_name], options[:foreign_key], false)
312
322
 
323
+ require_association_class(association_class_name)
324
+
313
325
  association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
314
326
 
315
327
  if options[:remote]
316
328
  association_finder = <<-"end_eval"
317
329
  #{association_class_name}.find_first(
318
- "#{class_primary_key_name} = '\#{id}'#{options[:conditions] ? " AND " + options[:conditions] : ""}",
330
+ "#{class_primary_key_name} = \#{quoted_id}#{options[:conditions] ? " AND " + options[:conditions] : ""}",
319
331
  #{options[:order] ? "\"" + options[:order] + "\"" : "nil" }
320
332
  )
321
333
  end_eval
@@ -411,9 +423,10 @@ module ActiveRecord
411
423
  association_name, association_class_name, association_class_primary_key_name =
412
424
  associate_identification(association_id, options[:class_name], options[:foreign_key])
413
425
 
414
- join_table = options[:join_table] ||
426
+ require_association_class(association_class_name)
427
+
428
+ join_table = options[:join_table] ||
415
429
  join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
416
-
417
430
 
418
431
  define_method(association_name) do |*params|
419
432
  force_reload = params.first unless params.empty?
@@ -428,7 +441,7 @@ module ActiveRecord
428
441
  association
429
442
  end
430
443
 
431
- before_destroy_sql = "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = '\\\#{self.id}'"
444
+ before_destroy_sql = "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = \\\#{self.quoted_id}"
432
445
  module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
433
446
 
434
447
  # deprecated api
@@ -438,6 +451,20 @@ module ActiveRecord
438
451
  deprecated_has_collection_method(association_name)
439
452
  end
440
453
 
454
+ # Loads the <tt>file_name</tt> if reload_associations is true or requires if it's false.
455
+ def require_association(file_name)
456
+ if !associations_loaded.include?(file_name)
457
+ associations_loaded << file_name
458
+ reload_associations ? silence_warnings { load("#{file_name}.rb") } : require(file_name)
459
+ end
460
+ end
461
+
462
+ # Resets the list of dependencies loaded (typically to be called by the end of a request), so when require_association is
463
+ # called for that dependency it'll be loaded anew.
464
+ def reset_associations_loaded
465
+ self.associations_loaded = []
466
+ end
467
+
441
468
  private
442
469
  # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
443
470
  def validate_options(valid_option_keys, supplied_option_keys)
@@ -552,6 +579,16 @@ module ActiveRecord
552
579
  end
553
580
  end_eval
554
581
  end
582
+
583
+ def require_association_class(class_name)
584
+ return unless class_name
585
+
586
+ begin
587
+ require_association(Inflector.underscore(class_name))
588
+ rescue LoadError
589
+ # Failed to load the associated class -- let's hope the developer is doing the requiring himself.
590
+ end
591
+ end
555
592
  end
556
593
  end
557
- end
594
+ end
@@ -37,11 +37,14 @@ module ActiveRecord
37
37
  # Add +records+ to this association. Returns +self+ so method calls may be chained.
38
38
  # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
39
39
  def <<(*records)
40
- flatten_deeper(records).each do |record|
41
- raise_on_type_mismatch(record)
42
- insert_record(record)
43
- @collection << record if loaded?
40
+ @owner.transaction do
41
+ flatten_deeper(records).each do |record|
42
+ raise_on_type_mismatch(record)
43
+ insert_record(record)
44
+ @collection << record if loaded?
45
+ end
44
46
  end
47
+
45
48
  self
46
49
  end
47
50
 
@@ -51,13 +54,19 @@ module ActiveRecord
51
54
  # Remove +records+ from this association. Does not destroy +records+.
52
55
  def delete(*records)
53
56
  records = flatten_deeper(records)
54
- records.each { |record| raise_on_type_mismatch(record) }
55
- delete_records(records)
56
- records.each { |record| @collection.delete(record) } if loaded?
57
+
58
+ @owner.transaction do
59
+ records.each { |record| raise_on_type_mismatch(record) }
60
+ delete_records(records)
61
+ records.each { |record| @collection.delete(record) } if loaded?
62
+ end
57
63
  end
58
64
 
59
65
  def destroy_all
60
- each { |record| record.destroy }
66
+ @owner.transaction do
67
+ each { |record| record.destroy }
68
+ end
69
+
61
70
  @collection = []
62
71
  end
63
72
 
@@ -81,7 +90,7 @@ module ActiveRecord
81
90
  end
82
91
 
83
92
  def quoted_record_ids(records)
84
- records.map { |record| "'#{@association_class.send(:sanitize, record.id)}'" }.join(',')
93
+ records.map { |record| record.quoted_id }.join(',')
85
94
  end
86
95
 
87
96
  def interpolate_sql_options!(options, *keys)
@@ -3,17 +3,17 @@ module ActiveRecord
3
3
  class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
4
4
  def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options)
5
5
  super(owner, association_name, association_class_name, association_class_primary_key_name, options)
6
-
7
- @association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name.downcase)) + "_id"
6
+
7
+ @association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
8
8
  association_table_name = options[:table_name] || @association_class.table_name(association_class_name)
9
9
  @join_table = join_table
10
- @order = options[:order] || "t.#{@owner.class.primary_key}"
10
+ @order = options[:order] || "t.#{@association_class.primary_key}"
11
11
 
12
12
  interpolate_sql_options!(options, :finder_sql, :delete_sql)
13
13
  @finder_sql = options[:finder_sql] ||
14
14
  "SELECT t.*, j.* FROM #{association_table_name} t, #{@join_table} j " +
15
- "WHERE t.#{@owner.class.primary_key} = j.#{@association_foreign_key} AND " +
16
- "j.#{association_class_primary_key_name} = '#{@owner.id}' " +
15
+ "WHERE t.#{@association_class.primary_key} = j.#{@association_foreign_key} AND " +
16
+ "j.#{association_class_primary_key_name} = #{@owner.quoted_id} " +
17
17
  (options[:conditions] ? " AND " + options[:conditions] : "") + " " +
18
18
  "ORDER BY #{@order}"
19
19
  end
@@ -26,11 +26,11 @@ module ActiveRecord
26
26
  each { |record| @owner.connection.execute(sql) }
27
27
  elsif @options[:conditions]
28
28
  sql =
29
- "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' " +
29
+ "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = #{@owner.quoted_id} " +
30
30
  "AND #{@association_foreign_key} IN (#{collect { |record| record.id }.join(", ")})"
31
31
  @owner.connection.execute(sql)
32
32
  else
33
- sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}'"
33
+ sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = #{@owner.quoted_id}"
34
34
  @owner.connection.execute(sql)
35
35
  end
36
36
 
@@ -46,7 +46,7 @@ module ActiveRecord
46
46
  if loaded?
47
47
  find_all { |record| record.id == association_id.to_i }.first
48
48
  else
49
- find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} = '#{association_id}' ORDER BY")).first
49
+ find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} = #{@owner.send(:quote, association_id)} ORDER BY")).first
50
50
  end
51
51
  end
52
52
  end
@@ -80,28 +80,29 @@ module ActiveRecord
80
80
  if @options[:insert_sql]
81
81
  @owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
82
82
  else
83
- sql = "INSERT INTO #{@join_table} (#{@association_class_primary_key_name}, #{@association_foreign_key}) VALUES ('#{@owner.id}','#{record.id}')"
83
+ sql = "INSERT INTO #{@join_table} (#{@association_class_primary_key_name}, #{@association_foreign_key}) " +
84
+ "VALUES (#{@owner.quoted_id},#{record.quoted_id})"
84
85
  @owner.connection.execute(sql)
85
86
  end
86
87
  end
87
88
 
88
89
  def insert_record_with_join_attributes(record, join_attributes)
89
- attributes = { @association_class_primary_key_name => @owner.id, @association_foreign_key => record.id }.update(join_attributes)
90
+ attributes = { @association_class_primary_key_name => @owner.id, @association_foreign_key => record.id }.update(join_attributes)
90
91
  sql =
91
92
  "INSERT INTO #{@join_table} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
92
93
  "VALUES (#{attributes.values.collect { |value| @owner.send(:quote, value) }.join(', ')})"
93
94
  @owner.connection.execute(sql)
94
95
  end
95
-
96
+
96
97
  def delete_records(records)
97
98
  if sql = @options[:delete_sql]
98
99
  records.each { |record| @owner.connection.execute(sql) }
99
100
  else
100
101
  ids = quoted_record_ids(records)
101
- sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_foreign_key} IN (#{ids})"
102
+ sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_foreign_key} IN (#{ids})"
102
103
  @owner.connection.execute(sql)
103
104
  end
104
105
  end
105
106
  end
106
107
  end
107
- end
108
+ end
@@ -7,10 +7,16 @@ module ActiveRecord
7
7
 
8
8
  if options[:finder_sql]
9
9
  @finder_sql = interpolate_sql(options[:finder_sql])
10
- @counter_sql = @finder_sql.gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
11
10
  else
12
- @finder_sql = "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
13
- @counter_sql = "#{@association_class_primary_key_name} = '#{@owner.id}'#{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
11
+ @finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id} #{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
12
+ end
13
+
14
+ if options[:counter_sql]
15
+ @counter_sql = interpolate_sql(options[:counter_sql])
16
+ elsif options[:finder_sql]
17
+ @counter_sql = options[:counter_sql] = @finder_sql.gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
18
+ else
19
+ @counter_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
14
20
  end
15
21
  end
16
22
 
@@ -34,8 +40,8 @@ module ActiveRecord
34
40
  @collection.find_all(&block)
35
41
  else
36
42
  @association_class.find_all(
37
- "#{@association_class_primary_key_name} = '#{@owner.id}' " +
38
- "#{@conditions ? " AND " + @conditions : ""} #{runtime_conditions ? " AND " + @association_class.send(:sanitize_conditions, runtime_conditions) : ""}",
43
+ "#{@association_class_primary_key_name} = #{@owner.quoted_id}" +
44
+ "#{@conditions ? " AND " + @conditions : ""}#{runtime_conditions ? " AND " + @association_class.send(:sanitize_conditions, runtime_conditions) : ""}",
39
45
  orderings,
40
46
  limit,
41
47
  joins
@@ -49,7 +55,7 @@ module ActiveRecord
49
55
  @collection.find(&block)
50
56
  else
51
57
  @association_class.find_on_conditions(association_id,
52
- "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
58
+ "#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@conditions ? " AND " + @conditions : ""}"
53
59
  )
54
60
  end
55
61
  end
@@ -57,7 +63,7 @@ module ActiveRecord
57
63
  # Removes all records from this association. Returns +self+ so
58
64
  # method calls may be chained.
59
65
  def clear
60
- @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}'")
66
+ @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = #{@owner.quoted_id}")
61
67
  @collection = []
62
68
  self
63
69
  end
@@ -70,21 +76,21 @@ module ActiveRecord
70
76
  @association_class.find_all(@finder_sql, @options[:order] ? @options[:order] : nil)
71
77
  end
72
78
  end
73
-
79
+
74
80
  def count_records
75
81
  if has_cached_counter?
76
82
  @owner.send(:read_attribute, cached_counter_attribute_name)
77
- elsif @options[:finder_sql]
83
+ elsif @options[:counter_sql]
78
84
  @association_class.count_by_sql(@counter_sql)
79
85
  else
80
86
  @association_class.count(@counter_sql)
81
87
  end
82
88
  end
83
-
89
+
84
90
  def has_cached_counter?
85
91
  @owner.attribute_present?(cached_counter_attribute_name)
86
92
  end
87
-
93
+
88
94
  def cached_counter_attribute_name
89
95
  "#{@association_name}_count"
90
96
  end
@@ -95,7 +101,10 @@ module ActiveRecord
95
101
 
96
102
  def delete_records(records)
97
103
  ids = quoted_record_ids(records)
98
- @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_class.primary_key} IN (#{ids})")
104
+ @association_class.update_all(
105
+ "#{@association_class_primary_key_name} = NULL",
106
+ "#{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_class.primary_key} IN (#{ids})"
107
+ )
99
108
  end
100
109
  end
101
110
  end
@@ -6,6 +6,8 @@ require 'yaml'
6
6
  module ActiveRecord #:nodoc:
7
7
  class ActiveRecordError < StandardError #:nodoc:
8
8
  end
9
+ class SubclassNotFound < ActiveRecordError #:nodoc:
10
+ end
9
11
  class AssociationTypeMismatch < ActiveRecordError #:nodoc:
10
12
  end
11
13
  class SerializationTypeMismatch < ActiveRecordError #:nodoc:
@@ -22,6 +24,8 @@ module ActiveRecord #:nodoc:
22
24
  end
23
25
  class StatementInvalid < ActiveRecordError #:nodoc:
24
26
  end
27
+ class PreparedStatementInvalid < ActiveRecordError #:nodoc:
28
+ end
25
29
 
26
30
  # Active Record objects doesn't specify their attributes directly, but rather infer them from the table definition with
27
31
  # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
@@ -63,15 +67,15 @@ module ActiveRecord #:nodoc:
63
67
  # end
64
68
  #
65
69
  # def self.authenticate_safely(user_name, password)
66
- # find_first([ "user_name = '%s' AND password = '%s'", user_name, password ])
70
+ # find_first([ "user_name = ? AND password = ?", user_name, password ])
67
71
  # end
68
72
  # end
69
73
  #
70
- # The +authenticate_unsafely+ method inserts the parameters directly into the query and is thus susceptible to SQL-injection
71
- # attacks if the +user_name+ and +password+ parameters come directly from a HTTP request. The +authenticate_safely+ method, on
72
- # the other hand, will sanitize the +user_name+ and +password+ before inserting them in the query, which will ensure that
74
+ # The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query and is thus susceptible to SQL-injection
75
+ # attacks if the <tt>user_name</tt> and +password+ parameters come directly from a HTTP request. The <tt>authenticate_safely</tt> method,
76
+ # on the other hand, will sanitize the <tt>user_name</tt> and +password+ before inserting them in the query, which will ensure that
73
77
  # an attacker can't escape the query and fake the login (or worse).
74
- #
78
+ #
75
79
  # == Overwriting default accessors
76
80
  #
77
81
  # All column values are automatically available through basic accessors on the Active Record object, but some times you
@@ -126,6 +130,9 @@ module ActiveRecord #:nodoc:
126
130
  # When you do Firm.create("name" => "37signals"), this record with be saved in the companies table with type = "Firm". You can then
127
131
  # fetch this row again using Company.find_first "name = '37signals'" and it will return a Firm object.
128
132
  #
133
+ # If you don't have a type column defined in your table, single-table inheritance won't be triggered. In that case, it'll work just
134
+ # like normal subclasses with no special magic for differentiating between them or reloading the right type with find.
135
+ #
129
136
  # Note, all the attributes for all the cases are kept in the same table. Read more:
130
137
  # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
131
138
  #
@@ -187,6 +194,9 @@ module ActiveRecord #:nodoc:
187
194
 
188
195
  @@subclasses = {}
189
196
 
197
+ cattr_accessor :configurations
198
+ @@primary_key_prefix_type = {}
199
+
190
200
  # Accessor for the prefix type that will be prepended to every primary key column name. The options are :table_name and
191
201
  # :table_name_with_underscore. If the first is specified, the Product class will look for "productid" instead of "id" as
192
202
  # the primary column. If the latter is specified, the Product class will look for "product_id" instead of "id". Remember
@@ -211,6 +221,15 @@ module ActiveRecord #:nodoc:
211
221
  cattr_accessor :pluralize_table_names
212
222
  @@pluralize_table_names = true
213
223
 
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
232
+
214
233
  class << self # Class methods
215
234
  # Returns objects for the records responding to either a specific id (1), a list of ids (1, 5, 6) or an array of ids.
216
235
  # If only one ID is specified, that object is returned directly. If more than one ID is specified, an array is returned.
@@ -218,12 +237,14 @@ module ActiveRecord #:nodoc:
218
237
  # Person.find(1) # returns the object for ID = 1
219
238
  # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
220
239
  # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
240
+ # Person.find([1]) # returns an array for objects the object with ID = 1
221
241
  # +RecordNotFound+ is raised if no record can be found.
222
242
  def find(*ids)
243
+ expects_array = ids.first.kind_of?(Array)
223
244
  ids = ids.flatten.compact.uniq
224
245
 
225
246
  if ids.length > 1
226
- ids_list = ids.map{ |id| "'#{sanitize(id)}'" }.join(", ")
247
+ ids_list = ids.map{ |id| "#{sanitize(id)}" }.join(", ")
227
248
  objects = find_all("#{primary_key} IN (#{ids_list})", primary_key)
228
249
 
229
250
  if objects.length == ids.length
@@ -233,11 +254,11 @@ module ActiveRecord #:nodoc:
233
254
  end
234
255
  elsif ids.length == 1
235
256
  id = ids.first
236
- sql = "SELECT * FROM #{table_name} WHERE #{primary_key} = '#{sanitize(id)}'"
257
+ sql = "SELECT * FROM #{table_name} WHERE #{primary_key} = #{sanitize(id)}"
237
258
  sql << " AND #{type_condition}" unless descends_from_active_record?
238
259
 
239
260
  if record = connection.select_one(sql, "#{name} Find")
240
- instantiate(record)
261
+ expects_array ? [instantiate(record)] : instantiate(record)
241
262
  else
242
263
  raise RecordNotFound, "Couldn't find #{name} with ID = #{id}"
243
264
  end
@@ -251,28 +272,32 @@ module ActiveRecord #:nodoc:
251
272
  # Example:
252
273
  # Person.find_on_conditions 5, "first_name LIKE '%dav%' AND last_name = 'heinemeier'"
253
274
  def find_on_conditions(id, conditions)
254
- find_first("#{primary_key} = '#{sanitize(id)}' AND #{sanitize_conditions(conditions)}") ||
275
+ find_first("#{primary_key} = #{sanitize(id)} AND #{sanitize_conditions(conditions)}") ||
255
276
  raise(RecordNotFound, "Couldn't find #{name} with #{primary_key} = #{id} on the condition of #{conditions}")
256
277
  end
257
278
 
258
279
  # Returns an array of all the objects that could be instantiated from the associated
259
280
  # table in the database. The +conditions+ can be used to narrow the selection of objects (WHERE-part),
260
281
  # such as by "color = 'red'", and arrangement of the selection can be done through +orderings+ (ORDER BY-part),
261
- # such as by "last_name, first_name DESC". A maximum of returned objects can be specified in +limit+. Example:
282
+ # such as by "last_name, first_name DESC". A maximum of returned objects and their offset can be specified in
283
+ # +limit+ (LIMIT...OFFSET-part). Examples:
262
284
  # Project.find_all "category = 'accounts'", "last_accessed DESC", 15
285
+ # Project.find_all ["category = ?", category_name], "created ASC", ["? OFFSET ?", 15, 20]
263
286
  def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)
264
287
  sql = "SELECT * FROM #{table_name} "
265
288
  sql << "#{joins} " if joins
266
289
  add_conditions!(sql, conditions)
267
290
  sql << "ORDER BY #{orderings} " unless orderings.nil?
268
- sql << "LIMIT #{limit} " unless limit.nil?
291
+ sql << "LIMIT #{sanitize_conditions(limit)} " unless limit.nil?
269
292
 
270
293
  find_by_sql(sql)
271
294
  end
272
295
 
273
- # Works like find_all, but requires a complete SQL string. Example:
296
+ # Works like find_all, but requires a complete SQL string. Examples:
274
297
  # Post.find_by_sql "SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id"
298
+ # Post.find_by_sql ["SELECT * FROM posts WHERE author = ? AND created > ?", author_id, start_date]
275
299
  def find_by_sql(sql)
300
+ sql = sanitize_conditions(sql)
276
301
  connection.select_all(sql, "#{name} Load").inject([]) { |objects, record| objects << instantiate(record) }
277
302
  end
278
303
 
@@ -344,6 +369,7 @@ module ActiveRecord #:nodoc:
344
369
  # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
345
370
  # Product.count "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
346
371
  def count_by_sql(sql)
372
+ sql = sanitize_conditions(sql)
347
373
  count = connection.select_one(sql, "#{name} Count").values.first
348
374
  return count ? count.to_i : 0
349
375
  end
@@ -354,12 +380,12 @@ module ActiveRecord #:nodoc:
354
380
  # for looping over a collection where each element require a number of aggregate values. Like the DiscussionBoard
355
381
  # that needs to list both the number of posts and comments.
356
382
  def increment_counter(counter_name, id)
357
- update_all "#{counter_name} = #{counter_name} + 1", "#{primary_key} = #{id}"
383
+ update_all "#{counter_name} = #{counter_name} + 1", "#{primary_key} = #{quote(id)}"
358
384
  end
359
385
 
360
386
  # Works like increment_counter, but decrements instead.
361
387
  def decrement_counter(counter_name, id)
362
- update_all "#{counter_name} = #{counter_name} - 1", "#{primary_key} = #{id}"
388
+ update_all "#{counter_name} = #{counter_name} - 1", "#{primary_key} = #{quote(id)}"
363
389
  end
364
390
 
365
391
  # Attributes named in this macro are protected from mass-assignment, such as <tt>new(attributes)</tt> and
@@ -499,6 +525,15 @@ module ActiveRecord #:nodoc:
499
525
  methods
500
526
  end
501
527
  end
528
+
529
+ # Resets all the cached information about columns, which will cause they to be reloaded on the next request.
530
+ def reset_column_information
531
+ @columns = @columns_hash = @content_columns = @dynamic_methods_hash = nil
532
+ end
533
+
534
+ def reset_column_information_and_inheritable_attributes_for_all_subclasses
535
+ subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
536
+ end
502
537
 
503
538
  # Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
504
539
  # Person.human_attribute_name("first_name") # => "First name"
@@ -507,13 +542,16 @@ module ActiveRecord #:nodoc:
507
542
  end
508
543
 
509
544
  def descends_from_active_record? # :nodoc:
510
- superclass == Base
545
+ superclass == Base || !columns_hash.has_key?(inheritance_column)
546
+ end
547
+
548
+ def quote(object)
549
+ connection.quote(object)
511
550
  end
512
551
 
513
- # Used to sanitize objects before they're used in an SELECT SQL-statement.
552
+ # Used to sanitize objects before they're used in an SELECT SQL-statement. Delegates to <tt>connection.quote</tt>.
514
553
  def sanitize(object) # :nodoc:
515
- return object if Fixnum === object
516
- object.to_s.gsub(/([;:])/, "").gsub('##', '\#\#').gsub(/'/, "''") # ' (for ruby-mode)
554
+ connection.quote(object)
517
555
  end
518
556
 
519
557
  # Used to aggregate logging and benchmark, so you can measure and represent multiple statements in a single block.
@@ -537,7 +575,20 @@ module ActiveRecord #:nodoc:
537
575
  # Finder methods must instantiate through this method to work with the single-table inheritance model
538
576
  # that makes it possible to create objects of different types from the same table.
539
577
  def instantiate(record)
540
- object = record_with_type?(record) ? compute_type(record[inheritance_column]).allocate : allocate
578
+ require_association_class(record[inheritance_column])
579
+
580
+ begin
581
+ object = record_with_type?(record) ? compute_type(record[inheritance_column]).allocate : allocate
582
+ rescue NameError
583
+ raise(
584
+ SubclassNotFound,
585
+ "The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " +
586
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
587
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
588
+ "or overwrite #{self.to_s}.inheritance_column to use another column for that information."
589
+ )
590
+ end
591
+
541
592
  object.instance_variable_set("@attributes", record)
542
593
  return object
543
594
  end
@@ -562,7 +613,7 @@ module ActiveRecord #:nodoc:
562
613
 
563
614
  def type_condition
564
615
  " (" + subclasses.inject("#{inheritance_column} = '#{Inflector.demodulize(name)}' ") do |condition, subclass|
565
- condition << "OR #{inheritance_column} = '#{Inflector.demodulize(subclass.name)}'"
616
+ condition << "OR #{inheritance_column} = '#{Inflector.demodulize(subclass.name)}' "
566
617
  end + ") "
567
618
  end
568
619
 
@@ -602,13 +653,54 @@ module ActiveRecord #:nodoc:
602
653
  # Accepts either a condition array or string. The string is returned untouched, but the array has each of
603
654
  # the condition values sanitized.
604
655
  def sanitize_conditions(conditions)
605
- if Array === conditions
606
- statement, values = conditions[0], conditions[1..-1]
607
- values.collect! { |value| sanitize(value) }
608
- conditions = statement % values
656
+ return conditions unless conditions.is_a?(Array)
657
+
658
+ statement, *values = conditions
659
+
660
+ if values[0].is_a?(Hash) && statement =~ /:\w+/
661
+ replace_named_bind_variables(statement, values[0])
662
+ elsif statement =~ /\?/
663
+ replace_bind_variables(statement, values)
664
+ else
665
+ statement % values.collect { |value| connection.quote_string(value.to_s) }
666
+ end
667
+ end
668
+
669
+ def replace_bind_variables(statement, values)
670
+ orig_statement = statement.clone
671
+ expected_number_of_variables = statement.count('?')
672
+ provided_number_of_variables = values.size
673
+
674
+ unless expected_number_of_variables == provided_number_of_variables
675
+ raise PreparedStatementInvalid, "wrong number of bind variables (#{provided_number_of_variables} for #{expected_number_of_variables})"
676
+ end
677
+
678
+ until values.empty?
679
+ statement.sub!(/\?/, encode_quoted_value(values.shift))
609
680
  end
610
681
 
611
- return conditions
682
+ statement.gsub('?') { |all, match| connection.quote(values.shift) }
683
+ end
684
+
685
+ def replace_named_bind_variables(statement, values_hash)
686
+ orig_statement = statement.clone
687
+ values_hash.keys.each do |k|
688
+ if statement.sub!(/:#{k.id2name}/, encode_quoted_value(values_hash.delete(k))).nil?
689
+ raise PreparedStatementInvalid, ":#{k} is not a variable in [#{orig_statement}]"
690
+ end
691
+ end
692
+
693
+ if statement =~ /(:\w+)/
694
+ raise PreparedStatementInvalid, "No value provided for #{$1} in [#{orig_statement}]"
695
+ end
696
+
697
+ return statement
698
+ end
699
+
700
+ def encode_quoted_value(value)
701
+ quoted_value = connection.quote(value)
702
+ quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'")
703
+ quoted_value
612
704
  end
613
705
  end
614
706
 
@@ -628,7 +720,11 @@ module ActiveRecord #:nodoc:
628
720
  # Every Active Record class must use "id" as their primary ID. This getter overwrites the native
629
721
  # id method, which isn't being used in this context.
630
722
  def id
631
- read_attribute(self.class.primary_key) unless new_record?
723
+ read_attribute(self.class.primary_key)
724
+ end
725
+
726
+ def quoted_id
727
+ quote(id, self.class.columns_hash[self.class.primary_key])
632
728
  end
633
729
 
634
730
  # Sets the primary ID.
@@ -654,7 +750,7 @@ module ActiveRecord #:nodoc:
654
750
  unless new_record?
655
751
  connection.delete(
656
752
  "DELETE FROM #{self.class.table_name} " +
657
- "WHERE #{self.class.primary_key} = '#{id}'",
753
+ "WHERE #{self.class.primary_key} = #{quote(id)}",
658
754
  "#{self.class.name} Destroy"
659
755
  )
660
756
  end
@@ -755,7 +851,7 @@ module ActiveRecord #:nodoc:
755
851
  def respond_to?(method)
756
852
  self.class.column_methods_hash[method.to_sym] || respond_to_without_attributes?(method)
757
853
  end
758
-
854
+
759
855
  private
760
856
  def create_or_update
761
857
  if new_record? then create else update end
@@ -765,8 +861,8 @@ module ActiveRecord #:nodoc:
765
861
  def update
766
862
  connection.update(
767
863
  "UPDATE #{self.class.table_name} " +
768
- "SET #{quoted_comma_pair_list(connection, attributes_with_quotes)} " +
769
- "WHERE #{self.class.primary_key} = '#{id}'",
864
+ "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
865
+ "WHERE #{self.class.primary_key} = #{quote(id)}",
770
866
  "#{self.class.name} Update"
771
867
  )
772
868
  end
@@ -825,12 +921,16 @@ module ActiveRecord #:nodoc:
825
921
  # Returns the value of attribute identified by <tt>attr_name</tt> after it has been type cast (for example,
826
922
  # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
827
923
  def read_attribute(attr_name) #:doc:
828
- if column = column_for_attribute(attr_name)
829
- @attributes[attr_name] = unserializable_attribute?(attr_name, column) ?
830
- unserialize_attribute(attr_name) : column.type_cast(@attributes[attr_name])
924
+ if @attributes.keys.include? attr_name
925
+ if column = column_for_attribute(attr_name)
926
+ @attributes[attr_name] = unserializable_attribute?(attr_name, column) ?
927
+ unserialize_attribute(attr_name) : column.type_cast(@attributes[attr_name])
928
+ end
929
+
930
+ @attributes[attr_name]
931
+ else
932
+ nil
831
933
  end
832
-
833
- @attributes[attr_name]
834
934
  end
835
935
 
836
936
  # Returns true if the attribute is of a text column and marked for serialization.
@@ -897,10 +997,10 @@ module ActiveRecord #:nodoc:
897
997
 
898
998
  # Returns copy of the attributes hash where all the values have been safely quoted for use in
899
999
  # an SQL statement.
900
- def attributes_with_quotes
1000
+ def attributes_with_quotes(include_primary_key = true)
901
1001
  columns_hash = self.class.columns_hash
902
1002
  @attributes.inject({}) do |attrs_quoted, pair|
903
- attrs_quoted[pair.first] = quote(pair.last, columns_hash[pair.first])
1003
+ attrs_quoted[pair.first] = quote(pair.last, columns_hash[pair.first]) unless !include_primary_key && pair.first == self.class.primary_key
904
1004
  attrs_quoted
905
1005
  end
906
1006
  end