activerecord 1.13.2 → 1.14.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 (144) hide show
  1. data/CHANGELOG +452 -10
  2. data/RUNNING_UNIT_TESTS +1 -1
  3. data/lib/active_record.rb +5 -2
  4. data/lib/active_record/acts/list.rb +1 -1
  5. data/lib/active_record/acts/tree.rb +29 -25
  6. data/lib/active_record/aggregations.rb +3 -2
  7. data/lib/active_record/associations.rb +783 -337
  8. data/lib/active_record/associations/association_collection.rb +7 -12
  9. data/lib/active_record/associations/association_proxy.rb +62 -24
  10. data/lib/active_record/associations/belongs_to_association.rb +27 -46
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
  12. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +38 -38
  13. data/lib/active_record/associations/has_many_association.rb +61 -56
  14. data/lib/active_record/associations/has_many_through_association.rb +144 -0
  15. data/lib/active_record/associations/has_one_association.rb +22 -16
  16. data/lib/active_record/base.rb +482 -182
  17. data/lib/active_record/calculations.rb +225 -0
  18. data/lib/active_record/callbacks.rb +7 -7
  19. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +162 -47
  20. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  21. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +2 -1
  22. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +21 -1
  23. data/lib/active_record/connection_adapters/abstract_adapter.rb +34 -2
  24. data/lib/active_record/connection_adapters/db2_adapter.rb +107 -61
  25. data/lib/active_record/connection_adapters/mysql_adapter.rb +29 -6
  26. data/lib/active_record/connection_adapters/openbase_adapter.rb +349 -0
  27. data/lib/active_record/connection_adapters/{oci_adapter.rb → oracle_adapter.rb} +125 -59
  28. data/lib/active_record/connection_adapters/postgresql_adapter.rb +24 -21
  29. data/lib/active_record/connection_adapters/sqlite_adapter.rb +47 -8
  30. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +36 -16
  31. data/lib/active_record/connection_adapters/sybase_adapter.rb +684 -0
  32. data/lib/active_record/fixtures.rb +42 -17
  33. data/lib/active_record/locking.rb +36 -15
  34. data/lib/active_record/migration.rb +111 -8
  35. data/lib/active_record/observer.rb +25 -1
  36. data/lib/active_record/reflection.rb +103 -41
  37. data/lib/active_record/schema.rb +2 -2
  38. data/lib/active_record/schema_dumper.rb +55 -18
  39. data/lib/active_record/timestamp.rb +6 -6
  40. data/lib/active_record/validations.rb +65 -40
  41. data/lib/active_record/vendor/db2.rb +10 -5
  42. data/lib/active_record/vendor/simple.rb +693 -702
  43. data/lib/active_record/version.rb +2 -2
  44. data/rakefile +4 -4
  45. data/test/aaa_create_tables_test.rb +25 -6
  46. data/test/abstract_unit.rb +39 -1
  47. data/test/adapter_test.rb +31 -4
  48. data/test/associations_cascaded_eager_loading_test.rb +106 -0
  49. data/test/associations_go_eager_test.rb +85 -16
  50. data/test/associations_join_model_test.rb +338 -0
  51. data/test/associations_test.rb +129 -50
  52. data/test/base_test.rb +204 -49
  53. data/test/binary_test.rb +1 -1
  54. data/test/calculations_test.rb +169 -0
  55. data/test/callbacks_test.rb +5 -23
  56. data/test/class_inheritable_attributes_test.rb +1 -1
  57. data/test/column_alias_test.rb +1 -1
  58. data/test/connections/native_mysql/connection.rb +1 -0
  59. data/test/connections/native_openbase/connection.rb +22 -0
  60. data/test/connections/{native_oci → native_oracle}/connection.rb +7 -9
  61. data/test/connections/native_sqlite/connection.rb +1 -1
  62. data/test/connections/native_sqlite3/connection.rb +1 -0
  63. data/test/connections/native_sqlite3/in_memory_connection.rb +1 -0
  64. data/test/connections/native_sybase/connection.rb +24 -0
  65. data/test/defaults_test.rb +18 -0
  66. data/test/deprecated_associations_test.rb +2 -2
  67. data/test/deprecated_finder_test.rb +0 -6
  68. data/test/finder_test.rb +26 -23
  69. data/test/fixtures/accounts.yml +10 -0
  70. data/test/fixtures/author.rb +31 -6
  71. data/test/fixtures/author_favorites.yml +4 -0
  72. data/test/fixtures/categories/special_categories.yml +9 -0
  73. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  74. data/test/fixtures/categories_posts.yml +4 -0
  75. data/test/fixtures/categorization.rb +5 -0
  76. data/test/fixtures/categorizations.yml +11 -0
  77. data/test/fixtures/category.rb +6 -0
  78. data/test/fixtures/company.rb +17 -5
  79. data/test/fixtures/company_in_module.rb +19 -5
  80. data/test/fixtures/db_definitions/db2.drop.sql +3 -0
  81. data/test/fixtures/db_definitions/db2.sql +121 -100
  82. data/test/fixtures/db_definitions/db22.sql +2 -2
  83. data/test/fixtures/db_definitions/firebird.drop.sql +4 -0
  84. data/test/fixtures/db_definitions/firebird.sql +26 -0
  85. data/test/fixtures/db_definitions/mysql.drop.sql +3 -0
  86. data/test/fixtures/db_definitions/mysql.sql +21 -1
  87. data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
  88. data/test/fixtures/db_definitions/openbase.sql +282 -0
  89. data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
  90. data/test/fixtures/db_definitions/openbase2.sql +7 -0
  91. data/test/fixtures/db_definitions/{oci.drop.sql → oracle.drop.sql} +6 -0
  92. data/test/fixtures/db_definitions/{oci.sql → oracle.sql} +25 -4
  93. data/test/fixtures/db_definitions/{oci2.drop.sql → oracle2.drop.sql} +0 -0
  94. data/test/fixtures/db_definitions/{oci2.sql → oracle2.sql} +0 -0
  95. data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
  96. data/test/fixtures/db_definitions/postgresql.sql +22 -1
  97. data/test/fixtures/db_definitions/schema.rb +32 -0
  98. data/test/fixtures/db_definitions/sqlite.drop.sql +3 -0
  99. data/test/fixtures/db_definitions/sqlite.sql +18 -0
  100. data/test/fixtures/db_definitions/sqlserver.drop.sql +3 -0
  101. data/test/fixtures/db_definitions/sqlserver.sql +23 -3
  102. data/test/fixtures/db_definitions/sybase.drop.sql +31 -0
  103. data/test/fixtures/db_definitions/sybase.sql +204 -0
  104. data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
  105. data/test/fixtures/db_definitions/sybase2.sql +5 -0
  106. data/test/fixtures/developers.yml +6 -1
  107. data/test/fixtures/developers_projects.yml +4 -0
  108. data/test/fixtures/funny_jokes.yml +14 -0
  109. data/test/fixtures/joke.rb +6 -0
  110. data/test/fixtures/legacy_thing.rb +3 -0
  111. data/test/fixtures/legacy_things.yml +3 -0
  112. data/test/fixtures/mixin.rb +1 -1
  113. data/test/fixtures/person.rb +4 -1
  114. data/test/fixtures/post.rb +26 -1
  115. data/test/fixtures/project.rb +1 -0
  116. data/test/fixtures/reader.rb +4 -0
  117. data/test/fixtures/readers.yml +4 -0
  118. data/test/fixtures/reply.rb +2 -1
  119. data/test/fixtures/tag.rb +5 -0
  120. data/test/fixtures/tagging.rb +6 -0
  121. data/test/fixtures/taggings.yml +18 -0
  122. data/test/fixtures/tags.yml +7 -0
  123. data/test/fixtures/tasks.yml +2 -2
  124. data/test/fixtures/topic.rb +2 -2
  125. data/test/fixtures/topics.yml +1 -0
  126. data/test/fixtures_test.rb +47 -13
  127. data/test/inheritance_test.rb +2 -2
  128. data/test/locking_test.rb +15 -1
  129. data/test/method_scoping_test.rb +248 -13
  130. data/test/migration_test.rb +68 -11
  131. data/test/mixin_nested_set_test.rb +1 -1
  132. data/test/modules_test.rb +6 -1
  133. data/test/readonly_test.rb +1 -1
  134. data/test/reflection_test.rb +63 -9
  135. data/test/schema_dumper_test.rb +41 -0
  136. data/test/{synonym_test_oci.rb → synonym_test_oracle.rb} +1 -1
  137. data/test/threaded_connections_test.rb +10 -0
  138. data/test/unconnected_test.rb +12 -5
  139. data/test/validations_test.rb +197 -10
  140. metadata +295 -260
  141. data/test/fixtures/db_definitions/create_oracle_db.bat +0 -0
  142. data/test/fixtures/db_definitions/create_oracle_db.sh +0 -0
  143. data/test/fixtures/fixture_database.sqlite +0 -0
  144. data/test/fixtures/fixture_database_2.sqlite +0 -0
@@ -9,6 +9,9 @@ module YAML #:nodoc:
9
9
  end
10
10
  end
11
11
 
12
+ class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
13
+ end
14
+
12
15
  # Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavours:
13
16
  #
14
17
  # 1. YAML fixtures
@@ -220,8 +223,10 @@ class Fixtures < YAML::Omap
220
223
  if load_instances
221
224
  ActiveRecord::Base.silence do
222
225
  fixtures.each do |name, fixture|
223
- if model = fixture.find
224
- object.instance_variable_set "@#{name}", model
226
+ begin
227
+ object.instance_variable_set "@#{name}", fixture.find
228
+ rescue FixtureClassNotFound
229
+ nil
225
230
  end
226
231
  end
227
232
  end
@@ -237,14 +242,13 @@ class Fixtures < YAML::Omap
237
242
  cattr_accessor :all_loaded_fixtures
238
243
  self.all_loaded_fixtures = {}
239
244
 
240
- def self.create_fixtures(fixtures_directory, *table_names)
241
- table_names = table_names.flatten.map { |n| n.to_s }
245
+ def self.create_fixtures(fixtures_directory, table_names, class_names = {})
246
+ table_names = [table_names].flatten.map { |n| n.to_s }
242
247
  connection = block_given? ? yield : ActiveRecord::Base.connection
243
-
244
248
  ActiveRecord::Base.silence do
245
249
  fixtures_map = {}
246
250
  fixtures = table_names.map do |table_name|
247
- fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, File.join(fixtures_directory, table_name.to_s))
251
+ fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
248
252
  end
249
253
  all_loaded_fixtures.merge! fixtures_map
250
254
 
@@ -267,10 +271,10 @@ class Fixtures < YAML::Omap
267
271
 
268
272
  attr_reader :table_name
269
273
 
270
- def initialize(connection, table_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
274
+ def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
271
275
  @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
272
-
273
- @class_name = ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize
276
+ @class_name = class_name ||
277
+ (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
274
278
  @table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
275
279
  read_fixture_files
276
280
  end
@@ -286,11 +290,18 @@ class Fixtures < YAML::Omap
286
290
  end
287
291
 
288
292
  private
293
+
289
294
  def read_fixture_files
290
295
  if File.file?(yaml_file_path)
291
296
  # YAML fixtures
292
297
  begin
293
- if yaml = YAML::load(erb_render(IO.read(yaml_file_path)))
298
+ yaml_string = ""
299
+ Dir["#{@fixture_path}/**/*.yml"].select {|f| test(?f,f) }.each do |subfixture_path|
300
+ yaml_string << IO.read(subfixture_path)
301
+ end
302
+ yaml_string << IO.read(yaml_file_path)
303
+
304
+ if yaml = YAML::load(erb_render(yaml_string))
294
305
  yaml = yaml.value if yaml.respond_to?(:type_id) and yaml.respond_to?(:value)
295
306
  yaml.each do |name, data|
296
307
  self[name] = Fixture.new(data, @class_name)
@@ -385,9 +396,11 @@ class Fixture #:nodoc:
385
396
  end
386
397
 
387
398
  def find
388
- if Object.const_defined?(@class_name)
389
- klass = Object.const_get(@class_name)
399
+ klass = @class_name.is_a?(Class) ? @class_name : Object.const_get(@class_name) rescue nil
400
+ if klass
390
401
  klass.find(self[klass.primary_key])
402
+ else
403
+ raise FixtureClassNotFound, "The class #{@class_name.inspect} was not found."
391
404
  end
392
405
  end
393
406
 
@@ -416,6 +429,7 @@ module Test #:nodoc:
416
429
  class TestCase #:nodoc:
417
430
  cattr_accessor :fixture_path
418
431
  class_inheritable_accessor :fixture_table_names
432
+ class_inheritable_accessor :fixture_class_names
419
433
  class_inheritable_accessor :use_transactional_fixtures
420
434
  class_inheritable_accessor :use_instantiated_fixtures # true, false, or :no_instances
421
435
  class_inheritable_accessor :pre_loaded_fixtures
@@ -424,9 +438,16 @@ module Test #:nodoc:
424
438
  self.use_transactional_fixtures = false
425
439
  self.use_instantiated_fixtures = true
426
440
  self.pre_loaded_fixtures = false
427
-
441
+
442
+ self.fixture_class_names = {}
443
+
428
444
  @@already_loaded_fixtures = {}
429
-
445
+ self.fixture_class_names = {}
446
+
447
+ def self.set_fixture_class(class_names = {})
448
+ self.fixture_class_names = self.fixture_class_names.merge(class_names)
449
+ end
450
+
430
451
  def self.fixtures(*table_names)
431
452
  table_names = table_names.flatten.map { |n| n.to_s }
432
453
  self.fixture_table_names |= table_names
@@ -453,7 +474,11 @@ module Test #:nodoc:
453
474
  force_reload = optionals.shift
454
475
  @fixture_cache[table_name] ||= Hash.new
455
476
  @fixture_cache[table_name][fixture] = nil if force_reload
456
- @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
477
+ if @loaded_fixtures[table_name][fixture.to_s]
478
+ @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
479
+ else
480
+ raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
481
+ end
457
482
  end
458
483
  end
459
484
  end
@@ -508,7 +533,7 @@ module Test #:nodoc:
508
533
  ActiveRecord::Base.connection.rollback_db_transaction
509
534
  ActiveRecord::Base.unlock_mutex
510
535
  end
511
- ActiveRecord::Base.clear_connection_cache!
536
+ ActiveRecord::Base.verify_active_connections!
512
537
  end
513
538
 
514
539
  alias_method :teardown, :teardown_with_fixtures
@@ -537,7 +562,7 @@ module Test #:nodoc:
537
562
  private
538
563
  def load_fixtures
539
564
  @loaded_fixtures = {}
540
- fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names)
565
+ fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
541
566
  unless fixtures.nil?
542
567
  if fixtures.instance_of?(Fixtures)
543
568
  @loaded_fixtures[fixtures.table_name] = fixtures
@@ -18,6 +18,8 @@ module ActiveRecord
18
18
  # You must ensure that your database schema defaults the lock_version column to 0.
19
19
  #
20
20
  # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
21
+ # To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method.
22
+ # This method uses the same syntax as <tt>set_table_name</tt>
21
23
  module Locking
22
24
  def self.append_features(base) #:nodoc:
23
25
  super
@@ -28,22 +30,24 @@ module ActiveRecord
28
30
  end
29
31
 
30
32
  def update_with_lock #:nodoc:
31
- if locking_enabled?
32
- previous_value = self.lock_version
33
- self.lock_version = previous_value + 1
34
-
35
- affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
36
- UPDATE #{self.class.table_name}
37
- SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))}
38
- WHERE #{self.class.primary_key} = #{quote(id)} AND lock_version = #{quote(previous_value)}
39
- end_sql
33
+ return update_without_lock unless locking_enabled?
40
34
 
41
- unless affected_rows == 1
42
- raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
43
- end
44
- else
45
- update_without_lock
35
+ lock_col = self.class.locking_column
36
+ previous_value = send(lock_col)
37
+ send(lock_col + '=', previous_value + 1)
38
+
39
+ affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
40
+ UPDATE #{self.class.table_name}
41
+ SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))}
42
+ WHERE #{self.class.primary_key} = #{quote(id)}
43
+ AND #{lock_col} = #{quote(previous_value)}
44
+ end_sql
45
+
46
+ unless affected_rows == 1
47
+ raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
46
48
  end
49
+
50
+ return true
47
51
  end
48
52
  end
49
53
 
@@ -52,7 +56,24 @@ module ActiveRecord
52
56
  cattr_accessor :lock_optimistically
53
57
 
54
58
  def locking_enabled? #:nodoc:
55
- lock_optimistically && respond_to?(:lock_version)
59
+ lock_optimistically && respond_to?(self.class.locking_column)
60
+ end
61
+
62
+ class << self
63
+ def set_locking_column(value = nil, &block)
64
+ define_attr_method :locking_column, value, &block
65
+ end
66
+
67
+ def locking_column #:nodoc:
68
+ reset_locking_column
69
+ end
70
+
71
+ def reset_locking_column #:nodoc:
72
+ default = 'lock_version'
73
+ set_locking_column(default)
74
+ default
75
+ end
56
76
  end
77
+
57
78
  end
58
79
  end
@@ -93,7 +93,7 @@ module ActiveRecord
93
93
  # pending migrations, creating the <tt>schema_info</tt> table if missing.
94
94
  #
95
95
  # To roll the database back to a previous migration version, use
96
- # <tt>rake migrate version=X</tt> where <tt>X</tt> is the version to which
96
+ # <tt>rake migrate VERSION=X</tt> where <tt>X</tt> is the version to which
97
97
  # you wish to downgrade. If any of the migrations throw an
98
98
  # <tt>IrreversibleMigration</tt> exception, that step will fail and you'll
99
99
  # have some manual work to do.
@@ -101,7 +101,7 @@ module ActiveRecord
101
101
  # == Database support
102
102
  #
103
103
  # Migrations are currently supported in MySQL, PostgreSQL, SQLite,
104
- # SQL Server, and Oracle (all supported databases except DB2).
104
+ # SQL Server, Sybase, and Oracle (all supported databases except DB2).
105
105
  #
106
106
  # == More examples
107
107
  #
@@ -159,16 +159,118 @@ module ActiveRecord
159
159
  # end
160
160
  # end
161
161
  # end
162
+ #
163
+ # == Controlling verbosity
164
+ #
165
+ # By default, migrations will describe the actions they are taking, writing
166
+ # them to the console as they happen, along with benchmarks describing how
167
+ # long each step took.
168
+ #
169
+ # You can quiet them down by setting ActiveRecord::Migration.verbose = false.
170
+ #
171
+ # You can also insert your own messages and benchmarks by using the #say_with_time
172
+ # method:
173
+ #
174
+ # def self.up
175
+ # ...
176
+ # say_with_time "Updating salaries..." do
177
+ # Person.find(:all).each do |p|
178
+ # p.salary = SalaryCalculator.compute(p)
179
+ # end
180
+ # end
181
+ # ...
182
+ # end
183
+ #
184
+ # The phrase "Updating salaries..." would then be printed, along with the
185
+ # benchmark for the block when the block completes.
162
186
  class Migration
187
+ @@verbose = true
188
+ cattr_accessor :verbose
189
+
163
190
  class << self
164
- def up() end
165
- def down() end
191
+ def up_using_benchmarks #:nodoc:
192
+ migrate(:up)
193
+ end
194
+
195
+ def down_using_benchmarks #:nodoc:
196
+ migrate(:down)
197
+ end
198
+
199
+ # Execute this migration in the named direction
200
+ def migrate(direction)
201
+ return unless respond_to?(direction)
202
+
203
+ case direction
204
+ when :up then announce "migrating"
205
+ when :down then announce "reverting"
206
+ end
207
+
208
+ result = nil
209
+ time = Benchmark.measure { result = send("real_#{direction}") }
210
+
211
+ case direction
212
+ when :up then announce "migrated (%.4fs)" % time.real; write
213
+ when :down then announce "reverted (%.4fs)" % time.real; write
214
+ end
215
+
216
+ result
217
+ end
218
+
219
+ # Because the method added may do an alias_method, it can be invoked
220
+ # recursively. We use @ignore_new_methods as a guard to indicate whether
221
+ # it is safe for the call to proceed.
222
+ def singleton_method_added(sym) #:nodoc:
223
+ return if @ignore_new_methods
224
+
225
+ begin
226
+ @ignore_new_methods = true
227
+
228
+ case sym
229
+ when :up, :down
230
+ klass = (class << self; self; end)
231
+ klass.send(:alias_method, "real_#{sym}", sym)
232
+ klass.send(:alias_method, sym, "#{sym}_using_benchmarks")
233
+ end
234
+ ensure
235
+ @ignore_new_methods = false
236
+ end
237
+ end
238
+
239
+ def write(text="")
240
+ puts(text) if verbose
241
+ end
166
242
 
167
- private
168
- def method_missing(method, *arguments, &block)
169
- arguments[0] = Migrator.proper_table_name(arguments.first) unless arguments.empty?
243
+ def announce(message)
244
+ text = "#{name}: #{message}"
245
+ write "== %s %s" % [ text, "=" * (75 - text.length) ]
246
+ end
247
+
248
+ def say(message, subitem=false)
249
+ write "#{subitem ? " ->" : "--"} #{message}"
250
+ end
251
+
252
+ def say_with_time(message)
253
+ say(message)
254
+ result = nil
255
+ time = Benchmark.measure { result = yield }
256
+ say "%.4fs" % time.real, :subitem
257
+ result
258
+ end
259
+
260
+ def suppress_messages
261
+ save = verbose
262
+ self.verbose = false
263
+ yield
264
+ ensure
265
+ self.verbose = save
266
+ end
267
+
268
+ def method_missing(method, *arguments, &block)
269
+ say_with_time "#{method}(#{arguments.map { |a| a.inspect }.join(", ")})" do
270
+ arguments[0] = Migrator.proper_table_name(arguments.first) unless arguments.empty? || method == :execute
170
271
  ActiveRecord::Base.connection.send(method, *arguments, &block)
171
272
  end
273
+ end
172
274
  end
173
275
  end
174
276
 
@@ -176,6 +278,7 @@ module ActiveRecord
176
278
  class << self
177
279
  def migrate(migrations_path, target_version = nil)
178
280
  Base.connection.initialize_schema_information
281
+
179
282
  case
180
283
  when target_version.nil?, current_version < target_version
181
284
  up(migrations_path, target_version)
@@ -225,7 +328,7 @@ module ActiveRecord
225
328
  next if irrelevant_migration?(version)
226
329
 
227
330
  Base.logger.info "Migrating to #{migration_class} (#{version})"
228
- migration_class.send(@direction)
331
+ migration_class.migrate(@direction)
229
332
  set_schema_version(version)
230
333
  end
231
334
  end
@@ -42,6 +42,18 @@ module ActiveRecord
42
42
  #
43
43
  # This Observer sends an email when a Comment#save is finished.
44
44
  #
45
+ # class ContactObserver < ActiveRecord::Observer
46
+ # def after_create(contact)
47
+ # contact.logger.info('New contact added!')
48
+ # end
49
+ #
50
+ # def after_destroy(contact)
51
+ # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
52
+ # end
53
+ # end
54
+ #
55
+ # This Observer uses logger to log when specific callbacks are triggered.
56
+ #
45
57
  # == Observing a class that can't be inferred
46
58
  #
47
59
  # Observers will by default be mapped to the class with which they share a name. So CommentObserver will
@@ -72,15 +84,27 @@ module ActiveRecord
72
84
  #
73
85
  # The observer can implement callback methods for each of the methods described in the Callbacks module.
74
86
  #
75
- # == Triggering Observers
87
+ # == Storing Observers in Rails
88
+ #
89
+ # If you're using Active Record within Rails, observer classes are usually stored in app/models with the
90
+ # naming convention of app/models/audit_observer.rb.
91
+ #
92
+ # == Configuration
76
93
  #
77
94
  # In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration setting in your
78
95
  # <tt>config/environment.rb</tt> file.
79
96
  #
80
97
  # config.active_record.observers = :comment_observer, :signup_observer
98
+ #
99
+ # Observers will not be invoked unless you define these in your application configuration.
100
+ #
81
101
  class Observer
82
102
  include Singleton
83
103
 
104
+ # Observer subclasses should be reloaded by the dispatcher in Rails
105
+ # when Dependencies.mechanism = :load.
106
+ include Reloadable::Subclasses
107
+
84
108
  # Attaches the observer to the supplied model classes.
85
109
  def self.observe(*models)
86
110
  define_method(:observed_class) { models }
@@ -1,36 +1,7 @@
1
1
  module ActiveRecord
2
2
  module Reflection # :nodoc:
3
- def self.append_features(base)
4
- super
3
+ def self.included(base)
5
4
  base.extend(ClassMethods)
6
-
7
- base.class_eval do
8
- class << self
9
- alias_method :composed_of_without_reflection, :composed_of
10
-
11
- def composed_of_with_reflection(part_id, options = {})
12
- composed_of_without_reflection(part_id, options)
13
- reflect_on_all_aggregations << AggregateReflection.new(:composed_of, part_id, options, self)
14
- end
15
-
16
- alias_method :composed_of, :composed_of_with_reflection
17
- end
18
- end
19
-
20
- for association_type in %w( belongs_to has_one has_many has_and_belongs_to_many )
21
- base.module_eval <<-"end_eval"
22
- class << self
23
- alias_method :#{association_type}_without_reflection, :#{association_type}
24
-
25
- def #{association_type}_with_reflection(association_id, options = {}, &block)
26
- #{association_type}_without_reflection(association_id, options, &block)
27
- reflect_on_all_associations << AssociationReflection.new(:#{association_type}, association_id, options, self)
28
- end
29
-
30
- alias_method :#{association_type}, :#{association_type}_with_reflection
31
- end
32
- end_eval
33
- end
34
5
  end
35
6
 
36
7
  # Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations.
@@ -39,26 +10,46 @@ module ActiveRecord
39
10
  #
40
11
  # You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class.
41
12
  module ClassMethods
13
+ def create_reflection(macro, name, options, active_record)
14
+ case macro
15
+ when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
16
+ reflection = AssociationReflection.new(macro, name, options, active_record)
17
+ when :composed_of
18
+ reflection = AggregateReflection.new(macro, name, options, active_record)
19
+ end
20
+ write_inheritable_hash :reflections, name => reflection
21
+ reflection
22
+ end
23
+
24
+ def reflections
25
+ read_inheritable_attribute(:reflections) or write_inheritable_attribute(:reflections, {})
26
+ end
27
+
42
28
  # Returns an array of AggregateReflection objects for all the aggregations in the class.
43
29
  def reflect_on_all_aggregations
44
- read_inheritable_attribute(:aggregations) or write_inheritable_attribute(:aggregations, [])
30
+ reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
45
31
  end
46
32
 
47
33
  # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example:
48
34
  # Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection
49
35
  def reflect_on_aggregation(aggregation)
50
- reflect_on_all_aggregations.find { |reflection| reflection.name == aggregation } unless reflect_on_all_aggregations.nil?
36
+ reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
51
37
  end
52
38
 
53
- # Returns an array of AssociationReflection objects for all the aggregations in the class.
54
- def reflect_on_all_associations
55
- read_inheritable_attribute(:associations) or write_inheritable_attribute(:associations, [])
39
+ # Returns an array of AssociationReflection objects for all the aggregations in the class. If you only want to reflect on a
40
+ # certain association type, pass in the symbol (:has_many, :has_one, :belongs_to) for that as the first parameter. Example:
41
+ # Account.reflect_on_all_associations # returns an array of all associations
42
+ # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
43
+ def reflect_on_all_associations(macro = nil)
44
+ association_reflections = reflections.values.select { |reflection| reflection.is_a?(AssociationReflection) }
45
+ macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
56
46
  end
57
47
 
58
48
  # Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
59
49
  # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
50
+ # Invoice.reflect_on_association(:line_items).macro # returns :has_many
60
51
  def reflect_on_association(association)
61
- reflect_on_all_associations.find { |reflection| reflection.name == association }
52
+ reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
62
53
  end
63
54
  end
64
55
 
@@ -92,6 +83,10 @@ module ActiveRecord
92
83
  # Returns the class for the macro, so "composed_of :balance, :class_name => 'Money'" would return the Money class and
93
84
  # "has_many :clients" would return the Client class.
94
85
  def klass() end
86
+
87
+ def class_name
88
+ @class_name ||= name_to_class_name(name.id2name)
89
+ end
95
90
 
96
91
  def ==(other_aggregation)
97
92
  name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
@@ -102,7 +97,7 @@ module ActiveRecord
102
97
  # Holds all the meta-data about an aggregation as it was specified in the Active Record class.
103
98
  class AggregateReflection < MacroReflection #:nodoc:
104
99
  def klass
105
- Object.const_get(options[:class_name] || name_to_class_name(name.id2name))
100
+ @klass ||= Object.const_get(class_name)
106
101
  end
107
102
 
108
103
  private
@@ -114,23 +109,90 @@ module ActiveRecord
114
109
  # Holds all the meta-data about an association as it was specified in the Active Record class.
115
110
  class AssociationReflection < MacroReflection #:nodoc:
116
111
  def klass
117
- @klass ||= active_record.send(:compute_type, (name_to_class_name(name.id2name)))
112
+ @klass ||= active_record.send(:compute_type, class_name)
118
113
  end
119
114
 
120
115
  def table_name
121
116
  @table_name ||= klass.table_name
122
117
  end
123
118
 
119
+ def primary_key_name
120
+ return @primary_key_name if @primary_key_name
121
+ case
122
+ when macro == :belongs_to
123
+ @primary_key_name = options[:foreign_key] || class_name.foreign_key
124
+ when options[:as]
125
+ @primary_key_name = options[:foreign_key] || "#{options[:as]}_id"
126
+ else
127
+ @primary_key_name = options[:foreign_key] || active_record.name.foreign_key
128
+ end
129
+ end
130
+
131
+ def association_foreign_key
132
+ @association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key
133
+ end
134
+
135
+ def counter_cache_column
136
+ if options[:counter_cache] == true
137
+ "#{active_record.name.underscore.pluralize}_count"
138
+ elsif options[:counter_cache]
139
+ options[:counter_cache]
140
+ end
141
+ end
142
+
143
+ def through_reflection
144
+ @through_reflection ||= options[:through] ? active_record.reflect_on_association(options[:through]) : false
145
+ end
146
+
147
+ # Gets an array of possible :through source reflection names
148
+ #
149
+ # [singularized, pluralized]
150
+ def source_reflection_names
151
+ @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
152
+ end
153
+
154
+ # Gets the source of the through reflection. It checks both a singularized and pluralized form for :belongs_to or :has_many.
155
+ # (The :tags association on Tagging below)
156
+ #
157
+ # class Post
158
+ # has_many :tags, :through => :taggings
159
+ # end
160
+ #
161
+ def source_reflection
162
+ return nil unless through_reflection
163
+ @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
164
+ end
165
+
166
+ def check_validity!
167
+ if options[:through]
168
+ if through_reflection.nil?
169
+ raise HasManyThroughAssociationNotFoundError.new(self)
170
+ end
171
+
172
+ if source_reflection.nil?
173
+ raise HasManyThroughSourceAssociationNotFoundError.new(self)
174
+ end
175
+
176
+ if source_reflection.options[:polymorphic]
177
+ raise HasManyThroughAssociationPolymorphicError.new(class_name, @reflection, source_reflection)
178
+ end
179
+ end
180
+ end
181
+
124
182
  private
125
183
  def name_to_class_name(name)
126
184
  if name =~ /::/
127
185
  name
128
186
  else
129
- unless class_name = options[:class_name]
187
+ if options[:class_name]
188
+ options[:class_name]
189
+ elsif through_reflection # get the class_name of the belongs_to association of the through reflection
190
+ source_reflection.class_name
191
+ else
130
192
  class_name = name.to_s.camelize
131
- class_name = class_name.singularize if [:has_many, :has_and_belongs_to_many].include?(macro)
193
+ class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
194
+ class_name
132
195
  end
133
- active_record.send(:type_name_with_module, class_name)
134
196
  end
135
197
  end
136
198
  end