sskirby-activerecord 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. data/CHANGELOG.md +6749 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +222 -0
  4. data/examples/associations.png +0 -0
  5. data/examples/performance.rb +177 -0
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record.rb +147 -0
  8. data/lib/active_record/aggregations.rb +255 -0
  9. data/lib/active_record/associations.rb +1604 -0
  10. data/lib/active_record/associations/alias_tracker.rb +79 -0
  11. data/lib/active_record/associations/association.rb +239 -0
  12. data/lib/active_record/associations/association_scope.rb +119 -0
  13. data/lib/active_record/associations/belongs_to_association.rb +79 -0
  14. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +34 -0
  15. data/lib/active_record/associations/builder/association.rb +55 -0
  16. data/lib/active_record/associations/builder/belongs_to.rb +85 -0
  17. data/lib/active_record/associations/builder/collection_association.rb +75 -0
  18. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +57 -0
  19. data/lib/active_record/associations/builder/has_many.rb +71 -0
  20. data/lib/active_record/associations/builder/has_one.rb +62 -0
  21. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  22. data/lib/active_record/associations/collection_association.rb +574 -0
  23. data/lib/active_record/associations/collection_proxy.rb +132 -0
  24. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +62 -0
  25. data/lib/active_record/associations/has_many_association.rb +108 -0
  26. data/lib/active_record/associations/has_many_through_association.rb +180 -0
  27. data/lib/active_record/associations/has_one_association.rb +73 -0
  28. data/lib/active_record/associations/has_one_through_association.rb +36 -0
  29. data/lib/active_record/associations/join_dependency.rb +214 -0
  30. data/lib/active_record/associations/join_dependency/join_association.rb +154 -0
  31. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  32. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  33. data/lib/active_record/associations/join_helper.rb +55 -0
  34. data/lib/active_record/associations/preloader.rb +177 -0
  35. data/lib/active_record/associations/preloader/association.rb +127 -0
  36. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  37. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  38. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  39. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  40. data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
  41. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  42. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  43. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  44. data/lib/active_record/associations/preloader/through_association.rb +67 -0
  45. data/lib/active_record/associations/singular_association.rb +64 -0
  46. data/lib/active_record/associations/through_association.rb +83 -0
  47. data/lib/active_record/attribute_assignment.rb +221 -0
  48. data/lib/active_record/attribute_methods.rb +272 -0
  49. data/lib/active_record/attribute_methods/before_type_cast.rb +31 -0
  50. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  51. data/lib/active_record/attribute_methods/dirty.rb +101 -0
  52. data/lib/active_record/attribute_methods/primary_key.rb +114 -0
  53. data/lib/active_record/attribute_methods/query.rb +39 -0
  54. data/lib/active_record/attribute_methods/read.rb +135 -0
  55. data/lib/active_record/attribute_methods/serialization.rb +93 -0
  56. data/lib/active_record/attribute_methods/time_zone_conversion.rb +62 -0
  57. data/lib/active_record/attribute_methods/write.rb +69 -0
  58. data/lib/active_record/autosave_association.rb +422 -0
  59. data/lib/active_record/base.rb +716 -0
  60. data/lib/active_record/callbacks.rb +275 -0
  61. data/lib/active_record/coders/yaml_column.rb +41 -0
  62. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +452 -0
  63. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +188 -0
  64. data/lib/active_record/connection_adapters/abstract/database_limits.rb +58 -0
  65. data/lib/active_record/connection_adapters/abstract/database_statements.rb +388 -0
  66. data/lib/active_record/connection_adapters/abstract/query_cache.rb +82 -0
  67. data/lib/active_record/connection_adapters/abstract/quoting.rb +115 -0
  68. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +492 -0
  69. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +598 -0
  70. data/lib/active_record/connection_adapters/abstract_adapter.rb +296 -0
  71. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
  72. data/lib/active_record/connection_adapters/column.rb +270 -0
  73. data/lib/active_record/connection_adapters/mysql2_adapter.rb +288 -0
  74. data/lib/active_record/connection_adapters/mysql_adapter.rb +426 -0
  75. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1261 -0
  76. data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
  77. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -0
  78. data/lib/active_record/connection_adapters/sqlite_adapter.rb +577 -0
  79. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  80. data/lib/active_record/counter_cache.rb +119 -0
  81. data/lib/active_record/dynamic_finder_match.rb +56 -0
  82. data/lib/active_record/dynamic_matchers.rb +79 -0
  83. data/lib/active_record/dynamic_scope_match.rb +23 -0
  84. data/lib/active_record/errors.rb +195 -0
  85. data/lib/active_record/explain.rb +85 -0
  86. data/lib/active_record/explain_subscriber.rb +21 -0
  87. data/lib/active_record/fixtures.rb +906 -0
  88. data/lib/active_record/fixtures/file.rb +65 -0
  89. data/lib/active_record/identity_map.rb +156 -0
  90. data/lib/active_record/inheritance.rb +167 -0
  91. data/lib/active_record/integration.rb +49 -0
  92. data/lib/active_record/locale/en.yml +40 -0
  93. data/lib/active_record/locking/optimistic.rb +183 -0
  94. data/lib/active_record/locking/pessimistic.rb +77 -0
  95. data/lib/active_record/log_subscriber.rb +68 -0
  96. data/lib/active_record/migration.rb +765 -0
  97. data/lib/active_record/migration/command_recorder.rb +105 -0
  98. data/lib/active_record/model_schema.rb +366 -0
  99. data/lib/active_record/nested_attributes.rb +469 -0
  100. data/lib/active_record/observer.rb +121 -0
  101. data/lib/active_record/persistence.rb +372 -0
  102. data/lib/active_record/query_cache.rb +74 -0
  103. data/lib/active_record/querying.rb +58 -0
  104. data/lib/active_record/railtie.rb +119 -0
  105. data/lib/active_record/railties/console_sandbox.rb +6 -0
  106. data/lib/active_record/railties/controller_runtime.rb +49 -0
  107. data/lib/active_record/railties/databases.rake +620 -0
  108. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  109. data/lib/active_record/readonly_attributes.rb +26 -0
  110. data/lib/active_record/reflection.rb +534 -0
  111. data/lib/active_record/relation.rb +534 -0
  112. data/lib/active_record/relation/batches.rb +90 -0
  113. data/lib/active_record/relation/calculations.rb +354 -0
  114. data/lib/active_record/relation/delegation.rb +49 -0
  115. data/lib/active_record/relation/finder_methods.rb +398 -0
  116. data/lib/active_record/relation/predicate_builder.rb +58 -0
  117. data/lib/active_record/relation/query_methods.rb +417 -0
  118. data/lib/active_record/relation/spawn_methods.rb +148 -0
  119. data/lib/active_record/result.rb +34 -0
  120. data/lib/active_record/sanitization.rb +194 -0
  121. data/lib/active_record/schema.rb +58 -0
  122. data/lib/active_record/schema_dumper.rb +204 -0
  123. data/lib/active_record/scoping.rb +152 -0
  124. data/lib/active_record/scoping/default.rb +142 -0
  125. data/lib/active_record/scoping/named.rb +202 -0
  126. data/lib/active_record/serialization.rb +18 -0
  127. data/lib/active_record/serializers/xml_serializer.rb +202 -0
  128. data/lib/active_record/session_store.rb +358 -0
  129. data/lib/active_record/store.rb +50 -0
  130. data/lib/active_record/test_case.rb +73 -0
  131. data/lib/active_record/timestamp.rb +113 -0
  132. data/lib/active_record/transactions.rb +360 -0
  133. data/lib/active_record/translation.rb +22 -0
  134. data/lib/active_record/validations.rb +83 -0
  135. data/lib/active_record/validations/associated.rb +43 -0
  136. data/lib/active_record/validations/uniqueness.rb +180 -0
  137. data/lib/active_record/version.rb +10 -0
  138. data/lib/rails/generators/active_record.rb +25 -0
  139. data/lib/rails/generators/active_record/migration.rb +15 -0
  140. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  141. data/lib/rails/generators/active_record/migration/templates/migration.rb +31 -0
  142. data/lib/rails/generators/active_record/model/model_generator.rb +43 -0
  143. data/lib/rails/generators/active_record/model/templates/migration.rb +15 -0
  144. data/lib/rails/generators/active_record/model/templates/model.rb +7 -0
  145. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  146. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  147. data/lib/rails/generators/active_record/observer/templates/observer.rb +4 -0
  148. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +25 -0
  149. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +12 -0
  150. metadata +242 -0
@@ -0,0 +1,132 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ # Association proxies in Active Record are middlemen between the object that
4
+ # holds the association, known as the <tt>@owner</tt>, and the actual associated
5
+ # object, known as the <tt>@target</tt>. The kind of association any proxy is
6
+ # about is available in <tt>@reflection</tt>. That's an instance of the class
7
+ # ActiveRecord::Reflection::AssociationReflection.
8
+ #
9
+ # For example, given
10
+ #
11
+ # class Blog < ActiveRecord::Base
12
+ # has_many :posts
13
+ # end
14
+ #
15
+ # blog = Blog.first
16
+ #
17
+ # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
18
+ # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
19
+ # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
20
+ #
21
+ # This class has most of the basic instance methods removed, and delegates
22
+ # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
23
+ # corner case, it even removes the +class+ method and that's why you get
24
+ #
25
+ # blog.posts.class # => Array
26
+ #
27
+ # though the object behind <tt>blog.posts</tt> is not an Array, but an
28
+ # ActiveRecord::Associations::HasManyAssociation.
29
+ #
30
+ # The <tt>@target</tt> object is not \loaded until needed. For example,
31
+ #
32
+ # blog.posts.count
33
+ #
34
+ # is computed directly through SQL and does not trigger by itself the
35
+ # instantiation of the actual post records.
36
+ class CollectionProxy # :nodoc:
37
+ alias :proxy_extend :extend
38
+
39
+ instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
40
+
41
+ delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
42
+ :lock, :readonly, :having, :pluck, :to => :scoped
43
+
44
+ delegate :target, :load_target, :loaded?, :to => :@association
45
+
46
+ delegate :select, :find, :first, :last,
47
+ :build, :create, :create!,
48
+ :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq,
49
+ :sum, :count, :size, :length, :empty?,
50
+ :any?, :many?, :include?,
51
+ :to => :@association
52
+
53
+ def initialize(association)
54
+ @association = association
55
+ Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) }
56
+ end
57
+
58
+ alias_method :new, :build
59
+
60
+ def proxy_association
61
+ @association
62
+ end
63
+
64
+ def scoped
65
+ association = @association
66
+ association.scoped.extending do
67
+ define_method(:proxy_association) { association }
68
+ end
69
+ end
70
+
71
+ def respond_to?(name, include_private = false)
72
+ super ||
73
+ (load_target && target.respond_to?(name, include_private)) ||
74
+ proxy_association.klass.respond_to?(name, include_private)
75
+ end
76
+
77
+ def method_missing(method, *args, &block)
78
+ match = DynamicFinderMatch.match(method)
79
+ if match && match.instantiator?
80
+ send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
81
+ proxy_association.send :set_owner_attributes, r
82
+ proxy_association.send :add_to_target, r
83
+ yield(r) if block_given?
84
+ end
85
+ end
86
+
87
+ if target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
88
+ if load_target
89
+ if target.respond_to?(method)
90
+ target.send(method, *args, &block)
91
+ else
92
+ begin
93
+ super
94
+ rescue NoMethodError => e
95
+ raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}")
96
+ end
97
+ end
98
+ end
99
+
100
+ else
101
+ scoped.readonly(nil).send(method, *args, &block)
102
+ end
103
+ end
104
+
105
+ # Forwards <tt>===</tt> explicitly to the \target because the instance method
106
+ # removal above doesn't catch it. Loads the \target if needed.
107
+ def ===(other)
108
+ other === load_target
109
+ end
110
+
111
+ def to_ary
112
+ load_target.dup
113
+ end
114
+ alias_method :to_a, :to_ary
115
+
116
+ def <<(*records)
117
+ proxy_association.concat(records) && self
118
+ end
119
+ alias_method :push, :<<
120
+
121
+ def clear
122
+ delete_all
123
+ self
124
+ end
125
+
126
+ def reload
127
+ proxy_association.reload
128
+ self
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,62 @@
1
+ module ActiveRecord
2
+ # = Active Record Has And Belongs To Many Association
3
+ module Associations
4
+ class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
5
+ attr_reader :join_table
6
+
7
+ def initialize(owner, reflection)
8
+ @join_table = Arel::Table.new(reflection.options[:join_table])
9
+ super
10
+ end
11
+
12
+ def insert_record(record, validate = true, raise = false)
13
+ if record.new_record?
14
+ if raise
15
+ record.save!(:validate => validate)
16
+ else
17
+ return unless record.save(:validate => validate)
18
+ end
19
+ end
20
+
21
+ if options[:insert_sql]
22
+ owner.connection.insert(interpolate(options[:insert_sql], record))
23
+ else
24
+ stmt = join_table.compile_insert(
25
+ join_table[reflection.foreign_key] => owner.id,
26
+ join_table[reflection.association_foreign_key] => record.id
27
+ )
28
+
29
+ owner.connection.insert stmt
30
+ end
31
+
32
+ record
33
+ end
34
+
35
+ # ActiveRecord::Relation#delete_all needs to support joins before we can use a
36
+ # SQL-only implementation.
37
+ alias delete_all_on_destroy delete_all
38
+
39
+ private
40
+
41
+ def count_records
42
+ load_target.size
43
+ end
44
+
45
+ def delete_records(records, method)
46
+ if sql = options[:delete_sql]
47
+ records.each { |record| owner.connection.delete(interpolate(sql, record)) }
48
+ else
49
+ relation = join_table
50
+ stmt = relation.where(relation[reflection.foreign_key].eq(owner.id).
51
+ and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact))
52
+ ).compile_delete
53
+ owner.connection.delete stmt
54
+ end
55
+ end
56
+
57
+ def invertible_for?(record)
58
+ false
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,108 @@
1
+ module ActiveRecord
2
+ # = Active Record Has Many Association
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.
8
+ class HasManyAssociation < CollectionAssociation #:nodoc:
9
+
10
+ def insert_record(record, validate = true, raise = false)
11
+ set_owner_attributes(record)
12
+
13
+ if raise
14
+ record.save!(:validate => validate)
15
+ else
16
+ record.save(:validate => validate)
17
+ end
18
+ end
19
+
20
+ private
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.
35
+ def count_records
36
+ count = if has_cached_counter?
37
+ owner.send(:read_attribute, cached_counter_attribute_name)
38
+ elsif options[:counter_sql] || options[:finder_sql]
39
+ reflection.klass.count_by_sql(custom_counter_sql)
40
+ else
41
+ scoped.count
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
+ [options[:limit], count].compact.min
50
+ end
51
+
52
+ def has_cached_counter?(reflection = reflection)
53
+ owner.attribute_present?(cached_counter_attribute_name(reflection))
54
+ end
55
+
56
+ def cached_counter_attribute_name(reflection = reflection)
57
+ "#{reflection.name}_count"
58
+ end
59
+
60
+ def update_counter(difference, reflection = reflection)
61
+ if has_cached_counter?(reflection)
62
+ counter = cached_counter_attribute_name(reflection)
63
+ owner.class.update_counters(owner.id, counter => difference)
64
+ owner[counter] += difference
65
+ owner.changed_attributes.delete(counter) # eww
66
+ end
67
+ end
68
+
69
+ # This shit is nasty. We need to avoid the following situation:
70
+ #
71
+ # * An associated record is deleted via record.destroy
72
+ # * Hence the callbacks run, and they find a belongs_to on the record with a
73
+ # :counter_cache options which points back at our owner. So they update the
74
+ # counter cache.
75
+ # * In which case, we must make sure to *not* update the counter cache, or else
76
+ # it will be decremented twice.
77
+ #
78
+ # Hence this method.
79
+ def inverse_updates_counter_cache?(reflection = reflection)
80
+ counter_name = cached_counter_attribute_name(reflection)
81
+ reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
82
+ inverse_reflection.counter_cache_column == counter_name
83
+ }
84
+ end
85
+
86
+ # Deletes the records according to the <tt>:dependent</tt> option.
87
+ def delete_records(records, method)
88
+ if method == :destroy
89
+ records.each { |r| r.destroy }
90
+ update_counter(-records.length) unless inverse_updates_counter_cache?
91
+ else
92
+ keys = records.map { |r| r[reflection.association_primary_key] }
93
+ scope = scoped.where(reflection.association_primary_key => keys)
94
+
95
+ if method == :delete_all
96
+ update_counter(-scope.delete_all)
97
+ else
98
+ update_counter(-scope.update_all(reflection.foreign_key => nil))
99
+ end
100
+ end
101
+ end
102
+
103
+ def foreign_key_present?
104
+ owner.attribute_present?(reflection.association_primary_key)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,180 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
3
+ module ActiveRecord
4
+ # = Active Record Has Many Through Association
5
+ module Associations
6
+ class HasManyThroughAssociation < HasManyAssociation #:nodoc:
7
+ include ThroughAssociation
8
+
9
+ def initialize(owner, reflection)
10
+ super
11
+
12
+ @through_records = {}
13
+ @through_association = nil
14
+ end
15
+
16
+ # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
17
+ # loaded and calling collection.size if it has. If it's more likely than not that the collection does
18
+ # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
19
+ # SELECT query if you use #length.
20
+ def size
21
+ if has_cached_counter?
22
+ owner.send(:read_attribute, cached_counter_attribute_name)
23
+ elsif loaded?
24
+ target.size
25
+ else
26
+ count
27
+ end
28
+ end
29
+
30
+ def concat(*records)
31
+ unless owner.new_record?
32
+ records.flatten.each do |record|
33
+ raise_on_type_mismatch(record)
34
+ record.save! if record.new_record?
35
+ end
36
+ end
37
+
38
+ super
39
+ end
40
+
41
+ def insert_record(record, validate = true, raise = false)
42
+ ensure_not_nested
43
+
44
+ if record.new_record?
45
+ if raise
46
+ record.save!(:validate => validate)
47
+ else
48
+ return unless record.save(:validate => validate)
49
+ end
50
+ end
51
+
52
+ save_through_record(record)
53
+ update_counter(1)
54
+ record
55
+ end
56
+
57
+ # ActiveRecord::Relation#delete_all needs to support joins before we can use a
58
+ # SQL-only implementation.
59
+ alias delete_all_on_destroy delete_all
60
+
61
+ private
62
+
63
+ def through_association
64
+ @through_association ||= owner.association(through_reflection.name)
65
+ end
66
+
67
+ # We temporarily cache through record that has been build, because if we build a
68
+ # through record in build_record and then subsequently call insert_record, then we
69
+ # want to use the exact same object.
70
+ #
71
+ # However, after insert_record has been called, we clear the cache entry because
72
+ # we want it to be possible to have multiple instances of the same record in an
73
+ # association
74
+ def build_through_record(record)
75
+ @through_records[record.object_id] ||= begin
76
+ through_record = through_association.build(construct_join_attributes(record))
77
+ through_record.send("#{source_reflection.name}=", record)
78
+ through_record
79
+ end
80
+ end
81
+
82
+ def save_through_record(record)
83
+ build_through_record(record).save!
84
+ ensure
85
+ @through_records.delete(record.object_id)
86
+ end
87
+
88
+ def build_record(attributes, options = {})
89
+ ensure_not_nested
90
+
91
+ record = super(attributes, options)
92
+
93
+ inverse = source_reflection.inverse_of
94
+ if inverse
95
+ if inverse.macro == :has_many
96
+ record.send(inverse.name) << build_through_record(record)
97
+ elsif inverse.macro == :has_one
98
+ record.send("#{inverse.name}=", build_through_record(record))
99
+ end
100
+ end
101
+
102
+ record
103
+ end
104
+
105
+ def target_reflection_has_associated_record?
106
+ if through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?
107
+ false
108
+ else
109
+ true
110
+ end
111
+ end
112
+
113
+ def update_through_counter?(method)
114
+ case method
115
+ when :destroy
116
+ !inverse_updates_counter_cache?(through_reflection)
117
+ when :nullify
118
+ false
119
+ else
120
+ true
121
+ end
122
+ end
123
+
124
+ def delete_records(records, method)
125
+ ensure_not_nested
126
+
127
+ scope = through_association.scoped.where(construct_join_attributes(*records))
128
+
129
+ case method
130
+ when :destroy
131
+ count = scope.destroy_all.length
132
+ when :nullify
133
+ count = scope.update_all(source_reflection.foreign_key => nil)
134
+ else
135
+ count = scope.delete_all
136
+ end
137
+
138
+ delete_through_records(records)
139
+
140
+ if through_reflection.macro == :has_many && update_through_counter?(method)
141
+ update_counter(-count, through_reflection)
142
+ end
143
+
144
+ update_counter(-count)
145
+ end
146
+
147
+ def through_records_for(record)
148
+ attributes = construct_join_attributes(record)
149
+ candidates = Array.wrap(through_association.target)
150
+ candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
151
+ end
152
+
153
+ def delete_through_records(records)
154
+ records.each do |record|
155
+ through_records = through_records_for(record)
156
+
157
+ if through_reflection.macro == :has_many
158
+ through_records.each { |r| through_association.target.delete(r) }
159
+ else
160
+ if through_records.include?(through_association.target)
161
+ through_association.target = nil
162
+ end
163
+ end
164
+
165
+ @through_records.delete(record.object_id)
166
+ end
167
+ end
168
+
169
+ def find_target
170
+ return [] unless target_reflection_has_associated_record?
171
+ scoped.all
172
+ end
173
+
174
+ # NOTE - not sure that we can actually cope with inverses here
175
+ def invertible_for?(record)
176
+ false
177
+ end
178
+ end
179
+ end
180
+ end