activerecord 1.0.0 → 3.0.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 (178) hide show
  1. data/CHANGELOG +5518 -76
  2. data/README.rdoc +222 -0
  3. data/examples/performance.rb +162 -0
  4. data/examples/simple.rb +14 -0
  5. data/lib/active_record/aggregations.rb +192 -80
  6. data/lib/active_record/association_preload.rb +403 -0
  7. data/lib/active_record/associations/association_collection.rb +545 -53
  8. data/lib/active_record/associations/association_proxy.rb +295 -0
  9. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +78 -0
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +127 -36
  12. data/lib/active_record/associations/has_many_association.rb +108 -84
  13. data/lib/active_record/associations/has_many_through_association.rb +116 -0
  14. data/lib/active_record/associations/has_one_association.rb +143 -0
  15. data/lib/active_record/associations/has_one_through_association.rb +40 -0
  16. data/lib/active_record/associations/through_association_scope.rb +154 -0
  17. data/lib/active_record/associations.rb +2086 -368
  18. data/lib/active_record/attribute_methods/before_type_cast.rb +33 -0
  19. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  20. data/lib/active_record/attribute_methods/primary_key.rb +50 -0
  21. data/lib/active_record/attribute_methods/query.rb +39 -0
  22. data/lib/active_record/attribute_methods/read.rb +116 -0
  23. data/lib/active_record/attribute_methods/time_zone_conversion.rb +61 -0
  24. data/lib/active_record/attribute_methods/write.rb +37 -0
  25. data/lib/active_record/attribute_methods.rb +60 -0
  26. data/lib/active_record/autosave_association.rb +369 -0
  27. data/lib/active_record/base.rb +1603 -721
  28. data/lib/active_record/callbacks.rb +176 -225
  29. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +365 -0
  30. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
  31. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  32. data/lib/active_record/connection_adapters/abstract/database_statements.rb +329 -0
  33. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  34. data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -0
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
  36. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +543 -0
  37. data/lib/active_record/connection_adapters/abstract_adapter.rb +165 -279
  38. data/lib/active_record/connection_adapters/mysql_adapter.rb +594 -82
  39. data/lib/active_record/connection_adapters/postgresql_adapter.rb +988 -135
  40. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -0
  41. data/lib/active_record/connection_adapters/sqlite_adapter.rb +365 -71
  42. data/lib/active_record/counter_cache.rb +115 -0
  43. data/lib/active_record/dynamic_finder_match.rb +53 -0
  44. data/lib/active_record/dynamic_scope_match.rb +32 -0
  45. data/lib/active_record/errors.rb +172 -0
  46. data/lib/active_record/fixtures.rb +941 -105
  47. data/lib/active_record/locale/en.yml +40 -0
  48. data/lib/active_record/locking/optimistic.rb +172 -0
  49. data/lib/active_record/locking/pessimistic.rb +55 -0
  50. data/lib/active_record/log_subscriber.rb +48 -0
  51. data/lib/active_record/migration.rb +617 -0
  52. data/lib/active_record/named_scope.rb +138 -0
  53. data/lib/active_record/nested_attributes.rb +417 -0
  54. data/lib/active_record/observer.rb +105 -36
  55. data/lib/active_record/persistence.rb +291 -0
  56. data/lib/active_record/query_cache.rb +36 -0
  57. data/lib/active_record/railtie.rb +91 -0
  58. data/lib/active_record/railties/controller_runtime.rb +38 -0
  59. data/lib/active_record/railties/databases.rake +512 -0
  60. data/lib/active_record/reflection.rb +364 -87
  61. data/lib/active_record/relation/batches.rb +89 -0
  62. data/lib/active_record/relation/calculations.rb +286 -0
  63. data/lib/active_record/relation/finder_methods.rb +355 -0
  64. data/lib/active_record/relation/predicate_builder.rb +41 -0
  65. data/lib/active_record/relation/query_methods.rb +261 -0
  66. data/lib/active_record/relation/spawn_methods.rb +112 -0
  67. data/lib/active_record/relation.rb +393 -0
  68. data/lib/active_record/schema.rb +59 -0
  69. data/lib/active_record/schema_dumper.rb +195 -0
  70. data/lib/active_record/serialization.rb +60 -0
  71. data/lib/active_record/serializers/xml_serializer.rb +244 -0
  72. data/lib/active_record/session_store.rb +340 -0
  73. data/lib/active_record/test_case.rb +67 -0
  74. data/lib/active_record/timestamp.rb +88 -0
  75. data/lib/active_record/transactions.rb +329 -75
  76. data/lib/active_record/validations/associated.rb +48 -0
  77. data/lib/active_record/validations/uniqueness.rb +185 -0
  78. data/lib/active_record/validations.rb +58 -179
  79. data/lib/active_record/version.rb +9 -0
  80. data/lib/active_record.rb +100 -24
  81. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  82. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  83. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  84. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  85. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  86. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  87. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  88. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  89. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  90. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  91. data/lib/rails/generators/active_record.rb +27 -0
  92. metadata +216 -158
  93. data/README +0 -361
  94. data/RUNNING_UNIT_TESTS +0 -36
  95. data/dev-utils/eval_debugger.rb +0 -9
  96. data/examples/associations.rb +0 -87
  97. data/examples/shared_setup.rb +0 -15
  98. data/examples/validation.rb +0 -88
  99. data/install.rb +0 -60
  100. data/lib/active_record/deprecated_associations.rb +0 -70
  101. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  102. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  103. data/lib/active_record/support/clean_logger.rb +0 -10
  104. data/lib/active_record/support/inflector.rb +0 -70
  105. data/lib/active_record/vendor/mysql.rb +0 -1117
  106. data/lib/active_record/vendor/simple.rb +0 -702
  107. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  108. data/lib/active_record/wrappings.rb +0 -59
  109. data/rakefile +0 -122
  110. data/test/abstract_unit.rb +0 -16
  111. data/test/aggregations_test.rb +0 -34
  112. data/test/all.sh +0 -8
  113. data/test/associations_test.rb +0 -477
  114. data/test/base_test.rb +0 -513
  115. data/test/class_inheritable_attributes_test.rb +0 -33
  116. data/test/connections/native_mysql/connection.rb +0 -24
  117. data/test/connections/native_postgresql/connection.rb +0 -24
  118. data/test/connections/native_sqlite/connection.rb +0 -24
  119. data/test/deprecated_associations_test.rb +0 -336
  120. data/test/finder_test.rb +0 -67
  121. data/test/fixtures/accounts/signals37 +0 -3
  122. data/test/fixtures/accounts/unknown +0 -2
  123. data/test/fixtures/auto_id.rb +0 -4
  124. data/test/fixtures/column_name.rb +0 -3
  125. data/test/fixtures/companies/first_client +0 -6
  126. data/test/fixtures/companies/first_firm +0 -4
  127. data/test/fixtures/companies/second_client +0 -6
  128. data/test/fixtures/company.rb +0 -37
  129. data/test/fixtures/company_in_module.rb +0 -33
  130. data/test/fixtures/course.rb +0 -3
  131. data/test/fixtures/courses/java +0 -2
  132. data/test/fixtures/courses/ruby +0 -2
  133. data/test/fixtures/customer.rb +0 -30
  134. data/test/fixtures/customers/david +0 -6
  135. data/test/fixtures/db_definitions/mysql.sql +0 -96
  136. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  137. data/test/fixtures/db_definitions/postgresql.sql +0 -113
  138. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  139. data/test/fixtures/db_definitions/sqlite.sql +0 -85
  140. data/test/fixtures/db_definitions/sqlite2.sql +0 -4
  141. data/test/fixtures/default.rb +0 -2
  142. data/test/fixtures/developer.rb +0 -8
  143. data/test/fixtures/developers/david +0 -2
  144. data/test/fixtures/developers/jamis +0 -2
  145. data/test/fixtures/developers_projects/david_action_controller +0 -2
  146. data/test/fixtures/developers_projects/david_active_record +0 -2
  147. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  148. data/test/fixtures/entrant.rb +0 -3
  149. data/test/fixtures/entrants/first +0 -3
  150. data/test/fixtures/entrants/second +0 -3
  151. data/test/fixtures/entrants/third +0 -3
  152. data/test/fixtures/fixture_database.sqlite +0 -0
  153. data/test/fixtures/fixture_database_2.sqlite +0 -0
  154. data/test/fixtures/movie.rb +0 -5
  155. data/test/fixtures/movies/first +0 -2
  156. data/test/fixtures/movies/second +0 -2
  157. data/test/fixtures/project.rb +0 -3
  158. data/test/fixtures/projects/action_controller +0 -2
  159. data/test/fixtures/projects/active_record +0 -2
  160. data/test/fixtures/reply.rb +0 -21
  161. data/test/fixtures/subscriber.rb +0 -5
  162. data/test/fixtures/subscribers/first +0 -2
  163. data/test/fixtures/subscribers/second +0 -2
  164. data/test/fixtures/topic.rb +0 -20
  165. data/test/fixtures/topics/first +0 -9
  166. data/test/fixtures/topics/second +0 -8
  167. data/test/fixtures_test.rb +0 -20
  168. data/test/inflector_test.rb +0 -104
  169. data/test/inheritance_test.rb +0 -125
  170. data/test/lifecycle_test.rb +0 -110
  171. data/test/modules_test.rb +0 -21
  172. data/test/multiple_db_test.rb +0 -46
  173. data/test/pk_test.rb +0 -57
  174. data/test/reflection_test.rb +0 -78
  175. data/test/thread_safety_test.rb +0 -33
  176. data/test/transactions_test.rb +0 -83
  177. data/test/unconnected_test.rb +0 -24
  178. data/test/validations_test.rb +0 -126
@@ -1,104 +1,128 @@
1
1
  module ActiveRecord
2
+ # = Active Record Has Many Association
2
3
  module Associations
4
+ # This is the proxy that handles a has many association.
5
+ #
6
+ # If the association has a <tt>:through</tt> option further specialization
7
+ # is provided by its child HasManyThroughAssociation.
3
8
  class HasManyAssociation < AssociationCollection #:nodoc:
4
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
5
- super(owner, association_name, association_class_name, association_class_primary_key_name, options)
6
- @conditions = options[:conditions]
7
-
8
- if options[:finder_sql]
9
- @counter_sql = options[:finder_sql].gsub(/SELECT (.*) FROM/, "SELECT COUNT(*) FROM")
10
- @finder_sql = options[:finder_sql]
11
- else
12
- @counter_sql = "#{@association_class_primary_key_name} = '#{@owner.id}'#{@conditions ? " AND " + @conditions : ""}"
13
- @finder_sql = "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
14
- end
15
- end
16
-
17
- def <<(record)
18
- raise ActiveRecord::AssociationTypeMismatch unless @association_class === record
19
- record.send(@association_class_primary_key_name + "=", @owner.id)
20
- record.save(false)
21
- @collection_array << record unless @collection_array.nil?
22
- end
23
-
24
- def delete(records)
25
- duplicated_records_array(records).each do |record|
26
- next if record.send(@association_class_primary_key_name) != @owner.id
27
- record.send(@association_class_primary_key_name + "=", nil)
28
- record.save(false)
29
- @collection_array.delete(record) unless @collection_array.nil?
30
- end
31
- end
32
-
33
- def create(attributes = {})
34
- # We can't use the regular Base.create method as the foreign key might be a protected attribute, hence the repetion
35
- record = @association_class.new(attributes || {})
36
- record.send(@association_class_primary_key_name + "=", @owner.id)
37
- record.save
38
-
39
- @collection_array << record unless @collection_array.nil?
40
-
41
- return record
9
+ def initialize(owner, reflection)
10
+ @finder_sql = nil
11
+ super
42
12
  end
43
-
44
- def build(attributes = {})
45
- association = @association_class.new
46
- association.attributes = attributes.merge({ "#{@association_class_primary_key_name}" => @owner.id})
47
- association
48
- end
49
-
50
- def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil, &block)
51
- if block_given? || @options[:finder_sql]
52
- load_collection_to_array
53
- @collection_array.send(:find_all, &block)
54
- else
55
- @association_class.find_all(
56
- "#{@association_class_primary_key_name} = '#{@owner.id}' " +
57
- "#{@conditions ? " AND " + @conditions : ""} #{runtime_conditions ? " AND " + runtime_conditions : ""}",
58
- orderings,
59
- limit,
60
- joins
61
- )
62
- end
63
- end
64
-
65
- def find(association_id = nil, &block)
66
- if block_given? || @options[:finder_sql]
67
- load_collection_to_array
68
- return @collection_array.send(:find, &block)
69
- else
70
- @association_class.find_on_conditions(
71
- association_id, "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
72
- )
73
- end
74
- end
75
-
76
13
  protected
77
- def find_all_records
78
- if @options[:finder_sql]
79
- @association_class.find_by_sql(@finder_sql)
14
+ def owner_quoted_id
15
+ if @reflection.options[:primary_key]
16
+ quote_value(@owner.send(@reflection.options[:primary_key]))
80
17
  else
81
- @association_class.find_all(@finder_sql, @options[:order] ? @options[:order] : nil)
18
+ @owner.quoted_id
82
19
  end
83
20
  end
84
-
21
+
22
+ # Returns the number of records in this collection.
23
+ #
24
+ # If the association has a counter cache it gets that value. Otherwise
25
+ # it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
26
+ # there's one. Some configuration options like :group make it impossible
27
+ # to do an SQL count, in those cases the array count will be used.
28
+ #
29
+ # That does not depend on whether the collection has already been loaded
30
+ # or not. The +size+ method is the one that takes the loaded flag into
31
+ # account and delegates to +count_records+ if needed.
32
+ #
33
+ # If the collection is empty the target is set to an empty array and
34
+ # the loaded flag is set to true as well.
85
35
  def count_records
86
- if has_cached_counter?
36
+ count = if has_cached_counter?
87
37
  @owner.send(:read_attribute, cached_counter_attribute_name)
88
- elsif @options[:finder_sql]
89
- @association_class.count_by_sql(@counter_sql)
38
+ elsif @reflection.options[:counter_sql]
39
+ @reflection.klass.count_by_sql(@counter_sql)
90
40
  else
91
- @association_class.count(@counter_sql)
41
+ @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
92
42
  end
43
+
44
+ # If there's nothing in the database and @target has no new records
45
+ # we are certain the current target is an empty array. This is a
46
+ # documented side-effect of the method that may avoid an extra SELECT.
47
+ @target ||= [] and loaded if count == 0
48
+
49
+ if @reflection.options[:limit]
50
+ count = [ @reflection.options[:limit], count ].min
51
+ end
52
+
53
+ return count
93
54
  end
94
-
55
+
95
56
  def has_cached_counter?
96
57
  @owner.attribute_present?(cached_counter_attribute_name)
97
58
  end
98
-
59
+
99
60
  def cached_counter_attribute_name
100
- @association_name + "_count"
61
+ "#{@reflection.name}_count"
62
+ end
63
+
64
+ def insert_record(record, force = false, validate = true)
65
+ set_belongs_to_association_for(record)
66
+ force ? record.save! : record.save(:validate => validate)
67
+ end
68
+
69
+ # Deletes the records according to the <tt>:dependent</tt> option.
70
+ def delete_records(records)
71
+ case @reflection.options[:dependent]
72
+ when :destroy
73
+ records.each { |r| r.destroy }
74
+ when :delete_all
75
+ @reflection.klass.delete(records.map { |record| record.id })
76
+ else
77
+ relation = Arel::Table.new(@reflection.table_name)
78
+ relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
79
+ and(relation[@reflection.klass.primary_key].in(records.map { |r| r.id }))
80
+ ).update(relation[@reflection.primary_key_name] => nil)
81
+
82
+ @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter?
83
+ end
84
+ end
85
+
86
+ def target_obsolete?
87
+ false
88
+ end
89
+
90
+ def construct_sql
91
+ case
92
+ when @reflection.options[:finder_sql]
93
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
94
+
95
+ when @reflection.options[:as]
96
+ @finder_sql =
97
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
98
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
99
+ @finder_sql << " AND (#{conditions})" if conditions
100
+
101
+ else
102
+ @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
103
+ @finder_sql << " AND (#{conditions})" if conditions
104
+ end
105
+
106
+ construct_counter_sql
107
+ end
108
+
109
+ def construct_scope
110
+ create_scoping = {}
111
+ set_belongs_to_association_for(create_scoping)
112
+ {
113
+ :find => { :conditions => @finder_sql,
114
+ :readonly => false,
115
+ :order => @reflection.options[:order],
116
+ :limit => @reflection.options[:limit],
117
+ :include => @reflection.options[:include]},
118
+ :create => create_scoping
119
+ }
120
+ end
121
+
122
+ def we_can_set_the_inverse_on_this?(record)
123
+ inverse = @reflection.inverse_of
124
+ return !inverse.nil?
101
125
  end
102
126
  end
103
127
  end
104
- end
128
+ end
@@ -0,0 +1,116 @@
1
+ require "active_record/associations/through_association_scope"
2
+ require 'active_support/core_ext/object/blank'
3
+
4
+ module ActiveRecord
5
+ # = Active Record Has Many Through Association
6
+ module Associations
7
+ class HasManyThroughAssociation < HasManyAssociation #:nodoc:
8
+ include ThroughAssociationScope
9
+
10
+ alias_method :new, :build
11
+
12
+ def create!(attrs = nil)
13
+ create_record(attrs, true)
14
+ end
15
+
16
+ def create(attrs = nil)
17
+ create_record(attrs, false)
18
+ end
19
+
20
+ def destroy(*records)
21
+ transaction do
22
+ delete_records(flatten_deeper(records))
23
+ super
24
+ end
25
+ end
26
+
27
+ # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
28
+ # loaded and calling collection.size if it has. If it's more likely than not that the collection does
29
+ # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
30
+ # SELECT query if you use #length.
31
+ def size
32
+ return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
33
+ return @target.size if loaded?
34
+ return count
35
+ end
36
+
37
+ protected
38
+ def create_record(attrs, force = true)
39
+ ensure_owner_is_not_new
40
+
41
+ transaction do
42
+ object = @reflection.klass.new(attrs)
43
+ add_record_to_target_with_callbacks(object) {|r| insert_record(object, force) }
44
+ object
45
+ end
46
+ end
47
+
48
+ def target_reflection_has_associated_record?
49
+ if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank?
50
+ false
51
+ else
52
+ true
53
+ end
54
+ end
55
+
56
+ def construct_find_options!(options)
57
+ options[:joins] = construct_joins(options[:joins])
58
+ options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include]
59
+ end
60
+
61
+ def insert_record(record, force = true, validate = true)
62
+ if record.new_record?
63
+ if force
64
+ record.save!
65
+ else
66
+ return false unless record.save(:validate => validate)
67
+ end
68
+ end
69
+
70
+ through_association = @owner.send(@reflection.through_reflection.name)
71
+ through_record = through_association.create!(construct_join_attributes(record))
72
+ through_association.proxy_target << through_record
73
+ end
74
+
75
+ # TODO - add dependent option support
76
+ def delete_records(records)
77
+ klass = @reflection.through_reflection.klass
78
+ records.each do |associate|
79
+ klass.delete_all(construct_join_attributes(associate))
80
+ end
81
+ end
82
+
83
+ def find_target
84
+ return [] unless target_reflection_has_associated_record?
85
+ with_scope(construct_scope) { @reflection.klass.find(:all) }
86
+ end
87
+
88
+ def construct_sql
89
+ case
90
+ when @reflection.options[:finder_sql]
91
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
92
+
93
+ @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
94
+ @finder_sql << " AND (#{conditions})" if conditions
95
+ else
96
+ @finder_sql = construct_conditions
97
+ end
98
+
99
+ construct_counter_sql
100
+ end
101
+
102
+ def has_cached_counter?
103
+ @owner.attribute_present?(cached_counter_attribute_name)
104
+ end
105
+
106
+ def cached_counter_attribute_name
107
+ "#{@reflection.name}_count"
108
+ end
109
+
110
+ # NOTE - not sure that we can actually cope with inverses here
111
+ def we_can_set_the_inverse_on_this?(record)
112
+ false
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,143 @@
1
+ module ActiveRecord
2
+ # = Active Record Belongs To Has One Association
3
+ module Associations
4
+ class HasOneAssociation < AssociationProxy #:nodoc:
5
+ def initialize(owner, reflection)
6
+ super
7
+ construct_sql
8
+ end
9
+
10
+ def create(attrs = {}, replace_existing = true)
11
+ new_record(replace_existing) do |reflection|
12
+ attrs = merge_with_conditions(attrs)
13
+ reflection.create_association(attrs)
14
+ end
15
+ end
16
+
17
+ def create!(attrs = {}, replace_existing = true)
18
+ new_record(replace_existing) do |reflection|
19
+ attrs = merge_with_conditions(attrs)
20
+ reflection.create_association!(attrs)
21
+ end
22
+ end
23
+
24
+ def build(attrs = {}, replace_existing = true)
25
+ new_record(replace_existing) do |reflection|
26
+ attrs = merge_with_conditions(attrs)
27
+ reflection.build_association(attrs)
28
+ end
29
+ end
30
+
31
+ def replace(obj, dont_save = false)
32
+ load_target
33
+
34
+ unless @target.nil? || @target == obj
35
+ if dependent? && !dont_save
36
+ case @reflection.options[:dependent]
37
+ when :delete
38
+ @target.delete unless @target.new_record?
39
+ @owner.clear_association_cache
40
+ when :destroy
41
+ @target.destroy unless @target.new_record?
42
+ @owner.clear_association_cache
43
+ when :nullify
44
+ @target[@reflection.primary_key_name] = nil
45
+ @target.save unless @owner.new_record? || @target.new_record?
46
+ end
47
+ else
48
+ @target[@reflection.primary_key_name] = nil
49
+ @target.save unless @owner.new_record? || @target.new_record?
50
+ end
51
+ end
52
+
53
+ if obj.nil?
54
+ @target = nil
55
+ else
56
+ raise_on_type_mismatch(obj)
57
+ set_belongs_to_association_for(obj)
58
+ @target = (AssociationProxy === obj ? obj.target : obj)
59
+ end
60
+
61
+ set_inverse_instance(obj, @owner)
62
+ @loaded = true
63
+
64
+ unless @owner.new_record? or obj.nil? or dont_save
65
+ return (obj.save ? self : false)
66
+ else
67
+ return (obj.nil? ? nil : self)
68
+ end
69
+ end
70
+
71
+ protected
72
+ def owner_quoted_id
73
+ if @reflection.options[:primary_key]
74
+ @owner.class.quote_value(@owner.send(@reflection.options[:primary_key]))
75
+ else
76
+ @owner.quoted_id
77
+ end
78
+ end
79
+
80
+ private
81
+ def find_target
82
+ options = @reflection.options.dup
83
+ (options.keys - [:select, :order, :include, :readonly]).each do |key|
84
+ options.delete key
85
+ end
86
+ options[:conditions] = @finder_sql
87
+
88
+ the_target = @reflection.klass.find(:first, options)
89
+ set_inverse_instance(the_target, @owner)
90
+ the_target
91
+ end
92
+
93
+ def construct_sql
94
+ case
95
+ when @reflection.options[:as]
96
+ @finder_sql =
97
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
98
+ "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
99
+ else
100
+ @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
101
+ end
102
+ @finder_sql << " AND (#{conditions})" if conditions
103
+ end
104
+
105
+ def construct_scope
106
+ create_scoping = {}
107
+ set_belongs_to_association_for(create_scoping)
108
+ { :create => create_scoping }
109
+ end
110
+
111
+ def new_record(replace_existing)
112
+ # Make sure we load the target first, if we plan on replacing the existing
113
+ # instance. Otherwise, if the target has not previously been loaded
114
+ # elsewhere, the instance we create will get orphaned.
115
+ load_target if replace_existing
116
+ record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
117
+ yield @reflection
118
+ end
119
+
120
+ if replace_existing
121
+ replace(record, true)
122
+ else
123
+ record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
124
+ self.target = record
125
+ set_inverse_instance(record, @owner)
126
+ end
127
+
128
+ record
129
+ end
130
+
131
+ def we_can_set_the_inverse_on_this?(record)
132
+ inverse = @reflection.inverse_of
133
+ return !inverse.nil?
134
+ end
135
+
136
+ def merge_with_conditions(attrs={})
137
+ attrs ||= {}
138
+ attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
139
+ attrs
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,40 @@
1
+ require "active_record/associations/through_association_scope"
2
+
3
+ module ActiveRecord
4
+ # = Active Record Has One Through Association
5
+ module Associations
6
+ class HasOneThroughAssociation < HasOneAssociation
7
+ include ThroughAssociationScope
8
+
9
+ def replace(new_value)
10
+ create_through_record(new_value)
11
+ @target = new_value
12
+ end
13
+
14
+ private
15
+
16
+ def create_through_record(new_value) #nodoc:
17
+ klass = @reflection.through_reflection.klass
18
+
19
+ current_object = @owner.send(@reflection.through_reflection.name)
20
+
21
+ if current_object
22
+ new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy
23
+ elsif new_value
24
+ if @owner.new_record?
25
+ self.target = new_value
26
+ through_association = @owner.send(:association_instance_get, @reflection.through_reflection.name)
27
+ through_association.build(construct_join_attributes(new_value))
28
+ else
29
+ @owner.send(@reflection.through_reflection.name, klass.create(construct_join_attributes(new_value)))
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+ def find_target
36
+ with_scope(construct_scope) { @reflection.klass.find(:first) }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,154 @@
1
+ module ActiveRecord
2
+ # = Active Record Through Association Scope
3
+ module Associations
4
+ module ThroughAssociationScope
5
+
6
+ protected
7
+
8
+ def construct_scope
9
+ { :create => construct_owner_attributes(@reflection),
10
+ :find => { :conditions => construct_conditions,
11
+ :joins => construct_joins,
12
+ :include => @reflection.options[:include] || @reflection.source_reflection.options[:include],
13
+ :select => construct_select,
14
+ :order => @reflection.options[:order],
15
+ :limit => @reflection.options[:limit],
16
+ :readonly => @reflection.options[:readonly],
17
+ } }
18
+ end
19
+
20
+ # Build SQL conditions from attributes, qualified by table name.
21
+ def construct_conditions
22
+ table_name = @reflection.through_reflection.quoted_table_name
23
+ conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
24
+ "#{table_name}.#{attr} = #{value}"
25
+ end
26
+ conditions << sql_conditions if sql_conditions
27
+ "(" + conditions.join(') AND (') + ")"
28
+ end
29
+
30
+ # Associate attributes pointing to owner, quoted.
31
+ def construct_quoted_owner_attributes(reflection)
32
+ if as = reflection.options[:as]
33
+ { "#{as}_id" => owner_quoted_id,
34
+ "#{as}_type" => reflection.klass.quote_value(
35
+ @owner.class.base_class.name.to_s,
36
+ reflection.klass.columns_hash["#{as}_type"]) }
37
+ elsif reflection.macro == :belongs_to
38
+ { reflection.klass.primary_key => @owner.class.quote_value(@owner[reflection.primary_key_name]) }
39
+ else
40
+ { reflection.primary_key_name => owner_quoted_id }
41
+ end
42
+ end
43
+
44
+ def construct_from
45
+ @reflection.table_name
46
+ end
47
+
48
+ def construct_select(custom_select = nil)
49
+ distinct = "DISTINCT " if @reflection.options[:uniq]
50
+ selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
51
+ end
52
+
53
+ def construct_joins(custom_joins = nil)
54
+ polymorphic_join = nil
55
+ if @reflection.source_reflection.macro == :belongs_to
56
+ reflection_primary_key = @reflection.klass.primary_key
57
+ source_primary_key = @reflection.source_reflection.primary_key_name
58
+ if @reflection.options[:source_type]
59
+ polymorphic_join = "AND %s.%s = %s" % [
60
+ @reflection.through_reflection.quoted_table_name, "#{@reflection.source_reflection.options[:foreign_type]}",
61
+ @owner.class.quote_value(@reflection.options[:source_type])
62
+ ]
63
+ end
64
+ else
65
+ reflection_primary_key = @reflection.source_reflection.primary_key_name
66
+ source_primary_key = @reflection.through_reflection.klass.primary_key
67
+ if @reflection.source_reflection.options[:as]
68
+ polymorphic_join = "AND %s.%s = %s" % [
69
+ @reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",
70
+ @owner.class.quote_value(@reflection.through_reflection.klass.name)
71
+ ]
72
+ end
73
+ end
74
+
75
+ "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
76
+ @reflection.through_reflection.quoted_table_name,
77
+ @reflection.quoted_table_name, reflection_primary_key,
78
+ @reflection.through_reflection.quoted_table_name, source_primary_key,
79
+ polymorphic_join
80
+ ]
81
+ end
82
+
83
+ # Construct attributes for associate pointing to owner.
84
+ def construct_owner_attributes(reflection)
85
+ if as = reflection.options[:as]
86
+ { "#{as}_id" => @owner.id,
87
+ "#{as}_type" => @owner.class.base_class.name.to_s }
88
+ else
89
+ { reflection.primary_key_name => @owner.id }
90
+ end
91
+ end
92
+
93
+ # Construct attributes for :through pointing to owner and associate.
94
+ def construct_join_attributes(associate)
95
+ # TODO: revisit this to allow it for deletion, supposing dependent option is supported
96
+ raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
97
+
98
+ join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
99
+
100
+ if @reflection.options[:source_type]
101
+ join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
102
+ end
103
+
104
+ if @reflection.through_reflection.options[:conditions].is_a?(Hash)
105
+ join_attributes.merge!(@reflection.through_reflection.options[:conditions])
106
+ end
107
+
108
+ join_attributes
109
+ end
110
+
111
+ def conditions
112
+ @conditions = build_conditions unless defined?(@conditions)
113
+ @conditions
114
+ end
115
+
116
+ def build_conditions
117
+ association_conditions = @reflection.options[:conditions]
118
+ through_conditions = build_through_conditions
119
+ source_conditions = @reflection.source_reflection.options[:conditions]
120
+ uses_sti = !@reflection.through_reflection.klass.descends_from_active_record?
121
+
122
+ if association_conditions || through_conditions || source_conditions || uses_sti
123
+ all = []
124
+
125
+ [association_conditions, source_conditions].each do |conditions|
126
+ all << interpolate_sql(sanitize_sql(conditions)) if conditions
127
+ end
128
+
129
+ all << through_conditions if through_conditions
130
+ all << build_sti_condition if uses_sti
131
+
132
+ all.map { |sql| "(#{sql})" } * ' AND '
133
+ end
134
+ end
135
+
136
+ def build_through_conditions
137
+ conditions = @reflection.through_reflection.options[:conditions]
138
+ if conditions.is_a?(Hash)
139
+ interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub(
140
+ @reflection.quoted_table_name,
141
+ @reflection.through_reflection.quoted_table_name)
142
+ elsif conditions
143
+ interpolate_sql(sanitize_sql(conditions))
144
+ end
145
+ end
146
+
147
+ def build_sti_condition
148
+ @reflection.through_reflection.klass.send(:type_condition).to_sql
149
+ end
150
+
151
+ alias_method :sql_conditions, :conditions
152
+ end
153
+ end
154
+ end