activerecord 4.0.4 → 4.1.16

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1632 -1797
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/examples/performance.rb +30 -18
  6. data/examples/simple.rb +4 -4
  7. data/lib/active_record/aggregations.rb +2 -1
  8. data/lib/active_record/association_relation.rb +4 -0
  9. data/lib/active_record/associations/alias_tracker.rb +49 -29
  10. data/lib/active_record/associations/association.rb +9 -17
  11. data/lib/active_record/associations/association_scope.rb +59 -49
  12. data/lib/active_record/associations/belongs_to_association.rb +34 -25
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +6 -1
  14. data/lib/active_record/associations/builder/association.rb +84 -54
  15. data/lib/active_record/associations/builder/belongs_to.rb +90 -58
  16. data/lib/active_record/associations/builder/collection_association.rb +47 -45
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +119 -25
  18. data/lib/active_record/associations/builder/has_many.rb +3 -3
  19. data/lib/active_record/associations/builder/has_one.rb +5 -7
  20. data/lib/active_record/associations/builder/singular_association.rb +6 -7
  21. data/lib/active_record/associations/collection_association.rb +121 -111
  22. data/lib/active_record/associations/collection_proxy.rb +73 -18
  23. data/lib/active_record/associations/has_many_association.rb +14 -11
  24. data/lib/active_record/associations/has_many_through_association.rb +33 -6
  25. data/lib/active_record/associations/has_one_association.rb +1 -1
  26. data/lib/active_record/associations/join_dependency/join_association.rb +46 -104
  27. data/lib/active_record/associations/join_dependency/join_base.rb +6 -8
  28. data/lib/active_record/associations/join_dependency/join_part.rb +18 -37
  29. data/lib/active_record/associations/join_dependency.rb +208 -168
  30. data/lib/active_record/associations/preloader/association.rb +69 -27
  31. data/lib/active_record/associations/preloader/collection_association.rb +2 -2
  32. data/lib/active_record/associations/preloader/has_many_through.rb +1 -1
  33. data/lib/active_record/associations/preloader/singular_association.rb +3 -3
  34. data/lib/active_record/associations/preloader/through_association.rb +58 -26
  35. data/lib/active_record/associations/preloader.rb +63 -49
  36. data/lib/active_record/associations/singular_association.rb +6 -5
  37. data/lib/active_record/associations/through_association.rb +30 -9
  38. data/lib/active_record/associations.rb +116 -42
  39. data/lib/active_record/attribute_assignment.rb +6 -3
  40. data/lib/active_record/attribute_methods/before_type_cast.rb +2 -1
  41. data/lib/active_record/attribute_methods/dirty.rb +35 -26
  42. data/lib/active_record/attribute_methods/primary_key.rb +8 -1
  43. data/lib/active_record/attribute_methods/read.rb +56 -29
  44. data/lib/active_record/attribute_methods/serialization.rb +44 -12
  45. data/lib/active_record/attribute_methods/time_zone_conversion.rb +13 -1
  46. data/lib/active_record/attribute_methods/write.rb +59 -26
  47. data/lib/active_record/attribute_methods.rb +82 -43
  48. data/lib/active_record/autosave_association.rb +209 -194
  49. data/lib/active_record/base.rb +6 -2
  50. data/lib/active_record/callbacks.rb +2 -2
  51. data/lib/active_record/coders/json.rb +13 -0
  52. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +5 -10
  53. data/lib/active_record/connection_adapters/abstract/database_statements.rb +14 -24
  54. data/lib/active_record/connection_adapters/abstract/query_cache.rb +13 -13
  55. data/lib/active_record/connection_adapters/abstract/quoting.rb +6 -3
  56. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  57. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +90 -0
  58. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +9 -8
  59. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +45 -70
  60. data/lib/active_record/connection_adapters/abstract/transaction.rb +1 -0
  61. data/lib/active_record/connection_adapters/abstract_adapter.rb +28 -96
  62. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +74 -66
  63. data/lib/active_record/connection_adapters/column.rb +1 -35
  64. data/lib/active_record/connection_adapters/connection_specification.rb +231 -43
  65. data/lib/active_record/connection_adapters/mysql2_adapter.rb +10 -5
  66. data/lib/active_record/connection_adapters/mysql_adapter.rb +24 -17
  67. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +22 -15
  68. data/lib/active_record/connection_adapters/postgresql/cast.rb +12 -4
  69. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +18 -44
  70. data/lib/active_record/connection_adapters/postgresql/oid.rb +38 -14
  71. data/lib/active_record/connection_adapters/postgresql/quoting.rb +37 -12
  72. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +20 -11
  73. data/lib/active_record/connection_adapters/postgresql_adapter.rb +98 -52
  74. data/lib/active_record/connection_adapters/schema_cache.rb +8 -29
  75. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -60
  76. data/lib/active_record/connection_handling.rb +39 -5
  77. data/lib/active_record/core.rb +38 -54
  78. data/lib/active_record/counter_cache.rb +9 -10
  79. data/lib/active_record/dynamic_matchers.rb +6 -2
  80. data/lib/active_record/enum.rb +199 -0
  81. data/lib/active_record/errors.rb +22 -5
  82. data/lib/active_record/fixture_set/file.rb +2 -1
  83. data/lib/active_record/fixtures.rb +173 -76
  84. data/lib/active_record/gem_version.rb +15 -0
  85. data/lib/active_record/inheritance.rb +23 -9
  86. data/lib/active_record/integration.rb +54 -1
  87. data/lib/active_record/locking/optimistic.rb +7 -2
  88. data/lib/active_record/locking/pessimistic.rb +1 -1
  89. data/lib/active_record/log_subscriber.rb +6 -13
  90. data/lib/active_record/migration/command_recorder.rb +8 -2
  91. data/lib/active_record/migration.rb +91 -56
  92. data/lib/active_record/model_schema.rb +7 -14
  93. data/lib/active_record/nested_attributes.rb +25 -13
  94. data/lib/active_record/no_touching.rb +52 -0
  95. data/lib/active_record/null_relation.rb +26 -6
  96. data/lib/active_record/persistence.rb +23 -29
  97. data/lib/active_record/querying.rb +15 -12
  98. data/lib/active_record/railtie.rb +12 -61
  99. data/lib/active_record/railties/databases.rake +37 -56
  100. data/lib/active_record/readonly_attributes.rb +0 -6
  101. data/lib/active_record/reflection.rb +230 -79
  102. data/lib/active_record/relation/batches.rb +74 -24
  103. data/lib/active_record/relation/calculations.rb +52 -48
  104. data/lib/active_record/relation/delegation.rb +54 -39
  105. data/lib/active_record/relation/finder_methods.rb +210 -67
  106. data/lib/active_record/relation/merger.rb +15 -12
  107. data/lib/active_record/relation/predicate_builder/array_handler.rb +29 -0
  108. data/lib/active_record/relation/predicate_builder/relation_handler.rb +17 -0
  109. data/lib/active_record/relation/predicate_builder.rb +81 -40
  110. data/lib/active_record/relation/query_methods.rb +185 -108
  111. data/lib/active_record/relation/spawn_methods.rb +8 -5
  112. data/lib/active_record/relation.rb +79 -84
  113. data/lib/active_record/result.rb +45 -6
  114. data/lib/active_record/runtime_registry.rb +5 -0
  115. data/lib/active_record/sanitization.rb +4 -4
  116. data/lib/active_record/schema_dumper.rb +18 -6
  117. data/lib/active_record/schema_migration.rb +31 -18
  118. data/lib/active_record/scoping/default.rb +5 -18
  119. data/lib/active_record/scoping/named.rb +14 -29
  120. data/lib/active_record/scoping.rb +5 -0
  121. data/lib/active_record/store.rb +67 -18
  122. data/lib/active_record/tasks/database_tasks.rb +66 -26
  123. data/lib/active_record/tasks/mysql_database_tasks.rb +16 -10
  124. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  125. data/lib/active_record/tasks/sqlite_database_tasks.rb +5 -1
  126. data/lib/active_record/timestamp.rb +6 -6
  127. data/lib/active_record/transactions.rb +10 -12
  128. data/lib/active_record/validations/presence.rb +1 -1
  129. data/lib/active_record/validations/uniqueness.rb +19 -9
  130. data/lib/active_record/version.rb +4 -7
  131. data/lib/active_record.rb +5 -7
  132. data/lib/rails/generators/active_record/migration/migration_generator.rb +4 -0
  133. data/lib/rails/generators/active_record/migration.rb +18 -0
  134. data/lib/rails/generators/active_record/model/model_generator.rb +4 -0
  135. data/lib/rails/generators/active_record.rb +2 -8
  136. metadata +18 -30
  137. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -65
  138. data/lib/active_record/associations/join_helper.rb +0 -45
  139. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  140. data/lib/active_record/tasks/firebird_database_tasks.rb +0 -56
  141. data/lib/active_record/tasks/oracle_database_tasks.rb +0 -45
  142. data/lib/active_record/tasks/sqlserver_database_tasks.rb +0 -48
  143. data/lib/active_record/test_case.rb +0 -96
@@ -1,50 +1,72 @@
1
+ require 'active_support/core_ext/module/attribute_accessors'
2
+
3
+ # This is the parent Association class which defines the variables
4
+ # used by all associations.
5
+ #
6
+ # The hierarchy is defined as follows:
7
+ # Association
8
+ # - SingularAssociation
9
+ # - BelongsToAssociation
10
+ # - HasOneAssociation
11
+ # - CollectionAssociation
12
+ # - HasManyAssociation
13
+
1
14
  module ActiveRecord::Associations::Builder
2
15
  class Association #:nodoc:
3
16
  class << self
17
+ attr_accessor :extensions
18
+ # TODO: This class accessor is needed to make activerecord-deprecated_finders work.
19
+ # We can move it to a constant in 5.0.
4
20
  attr_accessor :valid_options
5
21
  end
22
+ self.extensions = []
23
+
24
+ self.valid_options = [:class_name, :anonymous_class, :foreign_key, :validate]
6
25
 
7
- self.valid_options = [:class_name, :foreign_key, :validate]
26
+ attr_reader :name, :scope, :options
8
27
 
9
- attr_reader :model, :name, :scope, :options, :reflection
28
+ def self.build(model, name, scope, options, &block)
29
+ if model.dangerous_attribute_method?(name)
30
+ raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
31
+ "this will conflict with a method #{name} already defined by Active Record. " \
32
+ "Please choose a different association name."
33
+ end
10
34
 
11
- def self.build(*args, &block)
12
- new(*args, &block).build
35
+ builder = create_builder model, name, scope, options, &block
36
+ reflection = builder.build(model)
37
+ define_accessors model, reflection
38
+ define_callbacks model, reflection
39
+ builder.define_extensions model
40
+ reflection
13
41
  end
14
42
 
15
- def initialize(model, name, scope, options)
43
+ def self.create_builder(model, name, scope, options, &block)
16
44
  raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
17
45
 
18
- @model = model
19
- @name = name
46
+ new(model, name, scope, options, &block)
47
+ end
20
48
 
49
+ def initialize(model, name, scope, options)
50
+ # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders.
21
51
  if scope.is_a?(Hash)
22
- @scope = nil
23
- @options = scope
24
- else
25
- @scope = scope
26
- @options = options
52
+ options = scope
53
+ scope = nil
27
54
  end
28
55
 
29
- if @scope && @scope.arity == 0
30
- prev_scope = @scope
31
- @scope = proc { instance_exec(&prev_scope) }
32
- end
33
- end
56
+ # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders.
57
+ @name = name
58
+ @scope = scope
59
+ @options = options
34
60
 
35
- def mixin
36
- @model.generated_feature_methods
37
- end
61
+ validate_options
38
62
 
39
- include Module.new { def build; end }
63
+ if scope && scope.arity == 0
64
+ @scope = proc { instance_exec(&scope) }
65
+ end
66
+ end
40
67
 
41
- def build
42
- validate_options
43
- define_accessors
44
- configure_dependency if options[:dependent]
45
- @reflection = model.create_reflection(macro, name, scope, options, model)
46
- super # provides an extension point
47
- @reflection
68
+ def build(model)
69
+ ActiveRecord::Reflection.create(macro, name, scope, options, model)
48
70
  end
49
71
 
50
72
  def macro
@@ -52,19 +74,37 @@ module ActiveRecord::Associations::Builder
52
74
  end
53
75
 
54
76
  def valid_options
55
- Association.valid_options
77
+ Association.valid_options + Association.extensions.flat_map(&:valid_options)
56
78
  end
57
79
 
58
80
  def validate_options
59
81
  options.assert_valid_keys(valid_options)
60
82
  end
61
83
 
62
- def define_accessors
63
- define_readers
64
- define_writers
84
+ def define_extensions(model)
65
85
  end
66
86
 
67
- def define_readers
87
+ def self.define_callbacks(model, reflection)
88
+ add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent]
89
+ Association.extensions.each do |extension|
90
+ extension.build model, reflection
91
+ end
92
+ end
93
+
94
+ # Defines the setter and getter methods for the association
95
+ # class Post < ActiveRecord::Base
96
+ # has_many :comments
97
+ # end
98
+ #
99
+ # Post.first.comments and Post.first.comments= methods are defined by this method...
100
+ def self.define_accessors(model, reflection)
101
+ mixin = model.generated_association_methods
102
+ name = reflection.name
103
+ define_readers(mixin, name)
104
+ define_writers(mixin, name)
105
+ end
106
+
107
+ def self.define_readers(mixin, name)
68
108
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
69
109
  def #{name}(*args)
70
110
  association(:#{name}).reader(*args)
@@ -72,7 +112,7 @@ module ActiveRecord::Associations::Builder
72
112
  CODE
73
113
  end
74
114
 
75
- def define_writers
115
+ def self.define_writers(mixin, name)
76
116
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
77
117
  def #{name}=(value)
78
118
  association(:#{name}).writer(value)
@@ -80,29 +120,19 @@ module ActiveRecord::Associations::Builder
80
120
  CODE
81
121
  end
82
122
 
83
- def configure_dependency
84
- unless valid_dependent_options.include? options[:dependent]
85
- raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}"
86
- end
87
-
88
- if options[:dependent] == :restrict
89
- ActiveSupport::Deprecation.warn(
90
- "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \
91
- "provides the same functionality."
92
- )
93
- end
123
+ def self.valid_dependent_options
124
+ raise NotImplementedError
125
+ end
94
126
 
95
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
96
- def #{macro}_dependent_for_#{name}
97
- association(:#{name}).handle_dependency
98
- end
99
- CODE
127
+ private
100
128
 
101
- model.before_destroy "#{macro}_dependent_for_#{name}"
102
- end
129
+ def self.add_before_destroy_callbacks(model, reflection)
130
+ unless valid_dependent_options.include? reflection.options[:dependent]
131
+ raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}"
132
+ end
103
133
 
104
- def valid_dependent_options
105
- raise NotImplementedError
134
+ name = reflection.name
135
+ model.before_destroy lambda { |o| o.association(name).handle_dependency }
106
136
  end
107
137
  end
108
138
  end
@@ -8,99 +8,131 @@ module ActiveRecord::Associations::Builder
8
8
  super + [:foreign_type, :polymorphic, :touch, :counter_cache]
9
9
  end
10
10
 
11
- def constructable?
12
- !options[:polymorphic]
11
+ def self.valid_dependent_options
12
+ [:destroy, :delete]
13
13
  end
14
14
 
15
- def build
16
- reflection = super
17
- add_counter_cache_callbacks(reflection) if options[:counter_cache]
18
- add_touch_callbacks(reflection) if options[:touch]
19
- reflection
15
+ def self.define_callbacks(model, reflection)
16
+ super
17
+ add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
18
+ add_touch_callbacks(model, reflection) if reflection.options[:touch]
20
19
  end
21
20
 
22
- def add_counter_cache_callbacks(reflection)
23
- cache_column = reflection.counter_cache_column
24
- foreign_key = reflection.foreign_key
21
+ def self.define_accessors(mixin, reflection)
22
+ super
23
+ add_counter_cache_methods mixin
24
+ end
25
+
26
+ private
27
+
28
+ def self.add_counter_cache_methods(mixin)
29
+ return if mixin.method_defined? :belongs_to_counter_cache_after_create
25
30
 
26
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
27
- def belongs_to_counter_cache_after_create_for_#{name}
28
- if record = #{name}
29
- record.class.increment_counter(:#{cache_column}, record.id)
31
+ mixin.class_eval do
32
+ def belongs_to_counter_cache_after_create(reflection)
33
+ if record = send(reflection.name)
34
+ cache_column = reflection.counter_cache_column
35
+ record.class.increment_counter(cache_column, record.id)
30
36
  @_after_create_counter_called = true
31
37
  end
32
38
  end
33
39
 
34
- def belongs_to_counter_cache_before_destroy_for_#{name}
35
- unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == #{foreign_key.to_sym.inspect}
36
- record = #{name}
40
+ def belongs_to_counter_cache_before_destroy(reflection)
41
+ foreign_key = reflection.foreign_key.to_sym
42
+ unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
43
+ record = send reflection.name
37
44
  if record && !self.destroyed?
38
- record.class.decrement_counter(:#{cache_column}, record.id)
45
+ cache_column = reflection.counter_cache_column
46
+ record.class.decrement_counter(cache_column, record.id)
39
47
  end
40
48
  end
41
49
  end
42
50
 
43
- def belongs_to_counter_cache_after_update_for_#{name}
51
+ def belongs_to_counter_cache_after_update(reflection)
52
+ foreign_key = reflection.foreign_key
53
+ cache_column = reflection.counter_cache_column
54
+
44
55
  if (@_after_create_counter_called ||= false)
45
56
  @_after_create_counter_called = false
46
- elsif self.#{foreign_key}_changed? && !new_record? && defined?(#{name.to_s.camelize})
47
- model = #{name.to_s.camelize}
48
- foreign_key_was = self.#{foreign_key}_was
49
- foreign_key = self.#{foreign_key}
57
+ elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable?
58
+ model = reflection.klass
59
+ foreign_key_was = attribute_was foreign_key
60
+ foreign_key = attribute foreign_key
50
61
 
51
62
  if foreign_key && model.respond_to?(:increment_counter)
52
- model.increment_counter(:#{cache_column}, foreign_key)
63
+ model.increment_counter(cache_column, foreign_key)
53
64
  end
54
65
  if foreign_key_was && model.respond_to?(:decrement_counter)
55
- model.decrement_counter(:#{cache_column}, foreign_key_was)
66
+ model.decrement_counter(cache_column, foreign_key_was)
56
67
  end
57
68
  end
58
69
  end
59
- CODE
70
+ end
71
+ end
72
+
73
+ def self.add_counter_cache_callbacks(model, reflection)
74
+ cache_column = reflection.counter_cache_column
75
+
76
+ model.after_create lambda { |record|
77
+ record.belongs_to_counter_cache_after_create(reflection)
78
+ }
60
79
 
61
- model.after_create "belongs_to_counter_cache_after_create_for_#{name}"
62
- model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}"
63
- model.after_update "belongs_to_counter_cache_after_update_for_#{name}"
80
+ model.before_destroy lambda { |record|
81
+ record.belongs_to_counter_cache_before_destroy(reflection)
82
+ }
83
+
84
+ model.after_update lambda { |record|
85
+ record.belongs_to_counter_cache_after_update(reflection)
86
+ }
64
87
 
65
88
  klass = reflection.class_name.safe_constantize
66
89
  klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)
67
90
  end
68
91
 
69
- def add_touch_callbacks(reflection)
70
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
71
- def belongs_to_touch_after_save_or_destroy_for_#{name}
72
- foreign_key_field = #{reflection.foreign_key.inspect}
73
- old_foreign_id = changed_attributes[foreign_key_field]
74
-
75
- if old_foreign_id
76
- association = association(:#{name})
77
- reflection = association.reflection
78
- if reflection.polymorphic?
79
- klass = send("#{reflection.foreign_type}_was").constantize
80
- else
81
- klass = association.klass
82
- end
83
- old_record = klass.find_by(klass.primary_key => old_foreign_id)
92
+ def self.touch_record(o, foreign_key, name, touch) # :nodoc:
93
+ old_foreign_id = o.changed_attributes[foreign_key]
84
94
 
85
- if old_record
86
- old_record.touch #{options[:touch].inspect if options[:touch] != true}
87
- end
88
- end
95
+ if old_foreign_id
96
+ association = o.association(name)
97
+ reflection = association.reflection
98
+ if reflection.polymorphic?
99
+ klass = o.public_send("#{reflection.foreign_type}_was").constantize
100
+ else
101
+ klass = association.klass
102
+ end
103
+ old_record = klass.find_by(klass.primary_key => old_foreign_id)
89
104
 
90
- record = #{name}
91
- if record && record.persisted?
92
- record.touch #{options[:touch].inspect if options[:touch] != true}
105
+ if old_record
106
+ if touch != true
107
+ old_record.touch touch
108
+ else
109
+ old_record.touch
93
110
  end
94
111
  end
95
- CODE
96
-
97
- model.after_save "belongs_to_touch_after_save_or_destroy_for_#{name}"
98
- model.after_touch "belongs_to_touch_after_save_or_destroy_for_#{name}"
99
- model.after_destroy "belongs_to_touch_after_save_or_destroy_for_#{name}"
112
+ end
113
+
114
+ record = o.send name
115
+ if record && record.persisted?
116
+ if touch != true
117
+ record.touch touch
118
+ else
119
+ record.touch
120
+ end
121
+ end
100
122
  end
101
123
 
102
- def valid_dependent_options
103
- [:destroy, :delete]
124
+ def self.add_touch_callbacks(model, reflection)
125
+ foreign_key = reflection.foreign_key
126
+ n = reflection.name
127
+ touch = reflection.options[:touch]
128
+
129
+ callback = lambda { |record|
130
+ BelongsTo.touch_record(record, foreign_key, n, touch)
131
+ }
132
+
133
+ model.after_save callback
134
+ model.after_touch callback
135
+ model.after_destroy callback
104
136
  end
105
137
  end
106
138
  end
@@ -1,3 +1,5 @@
1
+ # This class is inherited by the has_many and has_many_and_belongs_to_many association classes
2
+
1
3
  require 'active_record/associations'
2
4
 
3
5
  module ActiveRecord::Associations::Builder
@@ -6,67 +8,57 @@ module ActiveRecord::Associations::Builder
6
8
  CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
7
9
 
8
10
  def valid_options
9
- super + [:table_name, :finder_sql, :counter_sql, :before_add,
11
+ super + [:table_name, :before_add,
10
12
  :after_add, :before_remove, :after_remove, :extend]
11
13
  end
12
14
 
13
- attr_reader :block_extension, :extension_module
14
-
15
- def initialize(*args, &extension)
16
- super(*args)
17
- @block_extension = extension
18
- end
15
+ attr_reader :block_extension
19
16
 
20
- def build
21
- show_deprecation_warnings
22
- wrap_block_extension
23
- reflection = super
24
- CALLBACKS.each { |callback_name| define_callback(callback_name) }
25
- reflection
17
+ def initialize(model, name, scope, options)
18
+ super
19
+ @mod = nil
20
+ if block_given?
21
+ @mod = Module.new(&Proc.new)
22
+ @scope = wrap_scope @scope, @mod
23
+ end
26
24
  end
27
25
 
28
- def writable?
29
- true
26
+ def self.define_callbacks(model, reflection)
27
+ super
28
+ name = reflection.name
29
+ options = reflection.options
30
+ CALLBACKS.each { |callback_name|
31
+ define_callback(model, callback_name, name, options)
32
+ }
30
33
  end
31
34
 
32
- def show_deprecation_warnings
33
- [:finder_sql, :counter_sql].each do |name|
34
- if options.include? name
35
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).")
36
- end
35
+ def define_extensions(model)
36
+ if @mod
37
+ extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
38
+ model.parent.const_set(extension_module_name, @mod)
37
39
  end
38
40
  end
39
41
 
40
- def wrap_block_extension
41
- if block_extension
42
- @extension_module = mod = Module.new(&block_extension)
43
- silence_warnings do
44
- model.parent.const_set(extension_module_name, mod)
45
- end
46
-
47
- prev_scope = @scope
42
+ def self.define_callback(model, callback_name, name, options)
43
+ full_callback_name = "#{callback_name}_for_#{name}"
48
44
 
49
- if prev_scope
50
- @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) }
45
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
46
+ model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
47
+ callbacks = Array(options[callback_name.to_sym]).map do |callback|
48
+ case callback
49
+ when Symbol
50
+ ->(method, owner, record) { owner.send(callback, record) }
51
+ when Proc
52
+ ->(method, owner, record) { callback.call(owner, record) }
51
53
  else
52
- @scope = proc { extending(mod) }
54
+ ->(method, owner, record) { callback.send(method, owner, record) }
53
55
  end
54
56
  end
57
+ model.send "#{full_callback_name}=", callbacks
55
58
  end
56
59
 
57
- def extension_module_name
58
- @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
59
- end
60
-
61
- def define_callback(callback_name)
62
- full_callback_name = "#{callback_name}_for_#{name}"
63
-
64
- # TODO : why do i need method_defined? I think its because of the inheritance chain
65
- model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name)
66
- model.send("#{full_callback_name}=", Array(options[callback_name.to_sym]))
67
- end
68
-
69
- def define_readers
60
+ # Defines the setter and getter methods for the collection_singular_ids.
61
+ def self.define_readers(mixin, name)
70
62
  super
71
63
 
72
64
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -76,7 +68,7 @@ module ActiveRecord::Associations::Builder
76
68
  CODE
77
69
  end
78
70
 
79
- def define_writers
71
+ def self.define_writers(mixin, name)
80
72
  super
81
73
 
82
74
  mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -85,5 +77,15 @@ module ActiveRecord::Associations::Builder
85
77
  end
86
78
  CODE
87
79
  end
80
+
81
+ private
82
+
83
+ def wrap_scope(scope, mod)
84
+ if scope
85
+ proc { |owner| instance_exec(owner, &scope).extending(mod) }
86
+ else
87
+ proc { extending(mod) }
88
+ end
89
+ end
88
90
  end
89
91
  end
@@ -1,39 +1,133 @@
1
1
  module ActiveRecord::Associations::Builder
2
- class HasAndBelongsToMany < CollectionAssociation #:nodoc:
3
- def macro
4
- :has_and_belongs_to_many
5
- end
2
+ class HasAndBelongsToMany # :nodoc:
3
+ class JoinTableResolver
4
+ KnownTable = Struct.new :join_table
5
+
6
+ class KnownClass
7
+ def initialize(lhs_class, rhs_class_name)
8
+ @lhs_class = lhs_class
9
+ @rhs_class_name = rhs_class_name
10
+ @join_table = nil
11
+ end
12
+
13
+ def join_table
14
+ @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
15
+ end
6
16
 
7
- def valid_options
8
- super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql]
17
+ private
18
+
19
+ def klass
20
+ @lhs_class.send(:compute_type, @rhs_class_name)
21
+ end
22
+ end
23
+
24
+ def self.build(lhs_class, name, options)
25
+ if options[:join_table]
26
+ KnownTable.new options[:join_table].to_s
27
+ else
28
+ class_name = options.fetch(:class_name) {
29
+ name.to_s.camelize.singularize
30
+ }
31
+ KnownClass.new lhs_class, class_name
32
+ end
33
+ end
9
34
  end
10
35
 
11
- def build
12
- reflection = super
13
- define_destroy_hook
14
- reflection
36
+ attr_reader :lhs_model, :association_name, :options
37
+
38
+ def initialize(association_name, lhs_model, options)
39
+ @association_name = association_name
40
+ @lhs_model = lhs_model
41
+ @options = options
15
42
  end
16
43
 
17
- def show_deprecation_warnings
18
- super
44
+ def through_model
45
+ habtm = JoinTableResolver.build lhs_model, association_name, options
19
46
 
20
- [:delete_sql, :insert_sql].each do |name|
21
- if options.include? name
22
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).")
47
+ join_model = Class.new(ActiveRecord::Base) {
48
+ class << self;
49
+ attr_accessor :class_resolver
50
+ attr_accessor :name
51
+ attr_accessor :table_name_resolver
52
+ attr_accessor :left_reflection
53
+ attr_accessor :right_reflection
23
54
  end
55
+
56
+ def self.table_name
57
+ table_name_resolver.join_table
58
+ end
59
+
60
+ def self.compute_type(class_name)
61
+ class_resolver.compute_type class_name
62
+ end
63
+
64
+ def self.add_left_association(name, options)
65
+ belongs_to name, options
66
+ self.left_reflection = _reflect_on_association(name)
67
+ end
68
+
69
+ def self.add_right_association(name, options)
70
+ rhs_name = name.to_s.singularize.to_sym
71
+ belongs_to rhs_name, options
72
+ self.right_reflection = _reflect_on_association(rhs_name)
73
+ end
74
+
75
+ def hash
76
+ object_id.hash
77
+ end
78
+
79
+ def ==(other)
80
+ equal?(other)
81
+ end
82
+ alias :eql? :==
83
+
84
+ }
85
+
86
+ join_model.name = "HABTM_#{association_name.to_s.camelize}"
87
+ join_model.table_name_resolver = habtm
88
+ join_model.class_resolver = lhs_model
89
+
90
+ join_model.add_left_association :left_side, anonymous_class: lhs_model
91
+ join_model.add_right_association association_name, belongs_to_options(options)
92
+ join_model
93
+ end
94
+
95
+ def middle_reflection(join_model)
96
+ middle_name = [lhs_model.name.downcase.pluralize,
97
+ association_name].join('_').gsub(/::/, '_').to_sym
98
+ middle_options = middle_options join_model
99
+ hm_builder = HasMany.create_builder(lhs_model,
100
+ middle_name,
101
+ nil,
102
+ middle_options)
103
+ hm_builder.build lhs_model
104
+ end
105
+
106
+ private
107
+
108
+ def middle_options(join_model)
109
+ middle_options = {}
110
+ middle_options[:anonymous_class] = join_model
111
+ middle_options[:source] = join_model.left_reflection.name
112
+ if options.key? :foreign_key
113
+ middle_options[:foreign_key] = options[:foreign_key]
24
114
  end
115
+ middle_options
25
116
  end
26
117
 
27
- def define_destroy_hook
28
- name = self.name
29
- model.send(:include, Module.new {
30
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
31
- def destroy_associations
32
- association(:#{name}).delete_all
33
- super
34
- end
35
- RUBY
36
- })
118
+ def belongs_to_options(options)
119
+ rhs_options = {}
120
+
121
+ if options.key? :class_name
122
+ rhs_options[:foreign_key] = options[:class_name].foreign_key
123
+ rhs_options[:class_name] = options[:class_name]
124
+ end
125
+
126
+ if options.key? :association_foreign_key
127
+ rhs_options[:foreign_key] = options[:association_foreign_key]
128
+ end
129
+
130
+ rhs_options
37
131
  end
38
132
  end
39
133
  end