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.
- data/CHANGELOG +250 -0
- data/README +17 -9
- data/dev-utils/eval_debugger.rb +1 -1
- data/install.rb +3 -1
- data/lib/active_record.rb +9 -2
- data/lib/active_record/acts/list.rb +178 -0
- data/lib/active_record/acts/tree.rb +44 -0
- data/lib/active_record/associations.rb +45 -8
- data/lib/active_record/associations/association_collection.rb +18 -9
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +14 -13
- data/lib/active_record/associations/has_many_association.rb +21 -12
- data/lib/active_record/base.rb +137 -37
- data/lib/active_record/callbacks.rb +30 -25
- data/lib/active_record/connection_adapters/abstract_adapter.rb +57 -33
- data/lib/active_record/connection_adapters/mysql_adapter.rb +4 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +3 -2
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +298 -0
- data/lib/active_record/fixtures.rb +241 -147
- data/lib/active_record/support/class_inheritable_attributes.rb +5 -2
- data/lib/active_record/support/inflector.rb +13 -12
- data/lib/active_record/support/misc.rb +6 -0
- data/lib/active_record/timestamp.rb +33 -0
- data/lib/active_record/transactions.rb +1 -1
- data/lib/active_record/validations.rb +294 -16
- data/rakefile +3 -7
- data/test/abstract_unit.rb +1 -4
- data/test/associations_test.rb +17 -4
- data/test/base_test.rb +37 -5
- data/test/connections/native_sqlserver/connection.rb +15 -0
- data/test/deprecated_associations_test.rb +40 -38
- data/test/finder_test.rb +82 -4
- data/test/fixtures/accounts.yml +8 -0
- data/test/fixtures/company.rb +6 -0
- data/test/fixtures/company_in_module.rb +1 -1
- data/test/fixtures/db_definitions/mysql.sql +13 -0
- data/test/fixtures/db_definitions/postgresql.sql +13 -0
- data/test/fixtures/db_definitions/sqlite.sql +14 -0
- data/test/fixtures/db_definitions/sqlserver.sql +110 -0
- data/test/fixtures/db_definitions/sqlserver2.sql +4 -0
- data/test/fixtures/developer.rb +2 -2
- data/test/fixtures/developers.yml +13 -0
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/fixture_database_2.sqlite +0 -0
- data/test/fixtures/mixin.rb +17 -0
- data/test/fixtures/mixins.yml +14 -0
- data/test/fixtures/naked/csv/accounts.csv +1 -0
- data/test/fixtures/naked/yml/accounts.yml +1 -0
- data/test/fixtures/naked/yml/companies.yml +1 -0
- data/test/fixtures/naked/yml/courses.yml +1 -0
- data/test/fixtures/project.rb +6 -0
- data/test/fixtures/reply.rb +14 -1
- data/test/fixtures/topic.rb +2 -2
- data/test/fixtures/topics/first +1 -0
- data/test/fixtures_test.rb +42 -12
- data/test/inflector_test.rb +2 -1
- data/test/inheritance_test.rb +22 -12
- data/test/mixin_test.rb +138 -0
- data/test/pk_test.rb +4 -2
- data/test/reflection_test.rb +3 -3
- data/test/transactions_test.rb +15 -0
- data/test/validations_test.rb +229 -4
- metadata +24 -10
- data/lib/active_record/associations.rb.orig +0 -555
- data/test/deprecated_associations_test.rb.orig +0 -334
- data/test/fixtures/accounts/signals37 +0 -3
- data/test/fixtures/accounts/unknown +0 -2
- data/test/fixtures/developers/david +0 -2
- 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} =
|
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} =
|
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
|
-
|
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} =
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
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|
|
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
|
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.#{@
|
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.#{@
|
16
|
-
"j.#{association_class_primary_key_name} =
|
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} =
|
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} =
|
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} =
|
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})
|
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} =
|
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} =
|
13
|
-
|
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} =
|
38
|
-
"#{@conditions ? " AND " + @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} =
|
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} =
|
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[:
|
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(
|
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
|
data/lib/active_record/base.rb
CHANGED
@@ -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 =
|
70
|
+
# find_first([ "user_name = ? AND password = ?", user_name, password ])
|
67
71
|
# end
|
68
72
|
# end
|
69
73
|
#
|
70
|
-
# The
|
71
|
-
# attacks if the
|
72
|
-
# the other hand, will sanitize the
|
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| "
|
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} =
|
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} =
|
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
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
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
|
-
|
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)
|
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} =
|
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} =
|
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
|
829
|
-
|
830
|
-
|
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
|