activerecord 5.2.3

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 (244) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +937 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +217 -0
  5. data/examples/performance.rb +185 -0
  6. data/examples/simple.rb +15 -0
  7. data/lib/active_record.rb +188 -0
  8. data/lib/active_record/aggregations.rb +283 -0
  9. data/lib/active_record/association_relation.rb +40 -0
  10. data/lib/active_record/associations.rb +1860 -0
  11. data/lib/active_record/associations/alias_tracker.rb +81 -0
  12. data/lib/active_record/associations/association.rb +299 -0
  13. data/lib/active_record/associations/association_scope.rb +168 -0
  14. data/lib/active_record/associations/belongs_to_association.rb +130 -0
  15. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +40 -0
  16. data/lib/active_record/associations/builder/association.rb +140 -0
  17. data/lib/active_record/associations/builder/belongs_to.rb +163 -0
  18. data/lib/active_record/associations/builder/collection_association.rb +82 -0
  19. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +135 -0
  20. data/lib/active_record/associations/builder/has_many.rb +17 -0
  21. data/lib/active_record/associations/builder/has_one.rb +30 -0
  22. data/lib/active_record/associations/builder/singular_association.rb +42 -0
  23. data/lib/active_record/associations/collection_association.rb +513 -0
  24. data/lib/active_record/associations/collection_proxy.rb +1131 -0
  25. data/lib/active_record/associations/foreign_association.rb +13 -0
  26. data/lib/active_record/associations/has_many_association.rb +144 -0
  27. data/lib/active_record/associations/has_many_through_association.rb +227 -0
  28. data/lib/active_record/associations/has_one_association.rb +120 -0
  29. data/lib/active_record/associations/has_one_through_association.rb +45 -0
  30. data/lib/active_record/associations/join_dependency.rb +262 -0
  31. data/lib/active_record/associations/join_dependency/join_association.rb +60 -0
  32. data/lib/active_record/associations/join_dependency/join_base.rb +23 -0
  33. data/lib/active_record/associations/join_dependency/join_part.rb +71 -0
  34. data/lib/active_record/associations/preloader.rb +193 -0
  35. data/lib/active_record/associations/preloader/association.rb +131 -0
  36. data/lib/active_record/associations/preloader/through_association.rb +107 -0
  37. data/lib/active_record/associations/singular_association.rb +73 -0
  38. data/lib/active_record/associations/through_association.rb +121 -0
  39. data/lib/active_record/attribute_assignment.rb +88 -0
  40. data/lib/active_record/attribute_decorators.rb +90 -0
  41. data/lib/active_record/attribute_methods.rb +492 -0
  42. data/lib/active_record/attribute_methods/before_type_cast.rb +78 -0
  43. data/lib/active_record/attribute_methods/dirty.rb +150 -0
  44. data/lib/active_record/attribute_methods/primary_key.rb +143 -0
  45. data/lib/active_record/attribute_methods/query.rb +42 -0
  46. data/lib/active_record/attribute_methods/read.rb +85 -0
  47. data/lib/active_record/attribute_methods/serialization.rb +90 -0
  48. data/lib/active_record/attribute_methods/time_zone_conversion.rb +91 -0
  49. data/lib/active_record/attribute_methods/write.rb +68 -0
  50. data/lib/active_record/attributes.rb +266 -0
  51. data/lib/active_record/autosave_association.rb +498 -0
  52. data/lib/active_record/base.rb +329 -0
  53. data/lib/active_record/callbacks.rb +353 -0
  54. data/lib/active_record/coders/json.rb +15 -0
  55. data/lib/active_record/coders/yaml_column.rb +50 -0
  56. data/lib/active_record/collection_cache_key.rb +53 -0
  57. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +1068 -0
  58. data/lib/active_record/connection_adapters/abstract/database_limits.rb +72 -0
  59. data/lib/active_record/connection_adapters/abstract/database_statements.rb +540 -0
  60. data/lib/active_record/connection_adapters/abstract/query_cache.rb +145 -0
  61. data/lib/active_record/connection_adapters/abstract/quoting.rb +200 -0
  62. data/lib/active_record/connection_adapters/abstract/savepoints.rb +23 -0
  63. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +146 -0
  64. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +685 -0
  65. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +95 -0
  66. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +1396 -0
  67. data/lib/active_record/connection_adapters/abstract/transaction.rb +283 -0
  68. data/lib/active_record/connection_adapters/abstract_adapter.rb +628 -0
  69. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +887 -0
  70. data/lib/active_record/connection_adapters/column.rb +91 -0
  71. data/lib/active_record/connection_adapters/connection_specification.rb +287 -0
  72. data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +33 -0
  73. data/lib/active_record/connection_adapters/mysql/column.rb +27 -0
  74. data/lib/active_record/connection_adapters/mysql/database_statements.rb +140 -0
  75. data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +72 -0
  76. data/lib/active_record/connection_adapters/mysql/quoting.rb +44 -0
  77. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +73 -0
  78. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +87 -0
  79. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +80 -0
  80. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +148 -0
  81. data/lib/active_record/connection_adapters/mysql/type_metadata.rb +35 -0
  82. data/lib/active_record/connection_adapters/mysql2_adapter.rb +129 -0
  83. data/lib/active_record/connection_adapters/postgresql/column.rb +44 -0
  84. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +163 -0
  85. data/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb +44 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid.rb +34 -0
  87. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +92 -0
  88. data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +56 -0
  89. data/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +15 -0
  90. data/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +17 -0
  91. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +50 -0
  92. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +23 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +23 -0
  94. data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +15 -0
  95. data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +21 -0
  96. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +71 -0
  97. data/lib/active_record/connection_adapters/postgresql/oid/inet.rb +15 -0
  98. data/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +15 -0
  99. data/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +45 -0
  100. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +41 -0
  101. data/lib/active_record/connection_adapters/postgresql/oid/oid.rb +15 -0
  102. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +65 -0
  103. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +97 -0
  104. data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +18 -0
  105. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +111 -0
  106. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +23 -0
  107. data/lib/active_record/connection_adapters/postgresql/oid/vector.rb +28 -0
  108. data/lib/active_record/connection_adapters/postgresql/oid/xml.rb +30 -0
  109. data/lib/active_record/connection_adapters/postgresql/quoting.rb +168 -0
  110. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +43 -0
  111. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +65 -0
  112. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +206 -0
  113. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +50 -0
  114. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +774 -0
  115. data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +39 -0
  116. data/lib/active_record/connection_adapters/postgresql/utils.rb +81 -0
  117. data/lib/active_record/connection_adapters/postgresql_adapter.rb +863 -0
  118. data/lib/active_record/connection_adapters/schema_cache.rb +118 -0
  119. data/lib/active_record/connection_adapters/sql_type_metadata.rb +34 -0
  120. data/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb +21 -0
  121. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +67 -0
  122. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +17 -0
  123. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +19 -0
  124. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +18 -0
  125. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +106 -0
  126. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +573 -0
  127. data/lib/active_record/connection_adapters/statement_pool.rb +61 -0
  128. data/lib/active_record/connection_handling.rb +145 -0
  129. data/lib/active_record/core.rb +559 -0
  130. data/lib/active_record/counter_cache.rb +218 -0
  131. data/lib/active_record/define_callbacks.rb +22 -0
  132. data/lib/active_record/dynamic_matchers.rb +122 -0
  133. data/lib/active_record/enum.rb +244 -0
  134. data/lib/active_record/errors.rb +380 -0
  135. data/lib/active_record/explain.rb +50 -0
  136. data/lib/active_record/explain_registry.rb +32 -0
  137. data/lib/active_record/explain_subscriber.rb +34 -0
  138. data/lib/active_record/fixture_set/file.rb +82 -0
  139. data/lib/active_record/fixtures.rb +1065 -0
  140. data/lib/active_record/gem_version.rb +17 -0
  141. data/lib/active_record/inheritance.rb +283 -0
  142. data/lib/active_record/integration.rb +155 -0
  143. data/lib/active_record/internal_metadata.rb +45 -0
  144. data/lib/active_record/legacy_yaml_adapter.rb +48 -0
  145. data/lib/active_record/locale/en.yml +48 -0
  146. data/lib/active_record/locking/optimistic.rb +198 -0
  147. data/lib/active_record/locking/pessimistic.rb +89 -0
  148. data/lib/active_record/log_subscriber.rb +137 -0
  149. data/lib/active_record/migration.rb +1378 -0
  150. data/lib/active_record/migration/command_recorder.rb +240 -0
  151. data/lib/active_record/migration/compatibility.rb +217 -0
  152. data/lib/active_record/migration/join_table.rb +17 -0
  153. data/lib/active_record/model_schema.rb +521 -0
  154. data/lib/active_record/nested_attributes.rb +600 -0
  155. data/lib/active_record/no_touching.rb +58 -0
  156. data/lib/active_record/null_relation.rb +68 -0
  157. data/lib/active_record/persistence.rb +763 -0
  158. data/lib/active_record/query_cache.rb +45 -0
  159. data/lib/active_record/querying.rb +70 -0
  160. data/lib/active_record/railtie.rb +226 -0
  161. data/lib/active_record/railties/console_sandbox.rb +7 -0
  162. data/lib/active_record/railties/controller_runtime.rb +56 -0
  163. data/lib/active_record/railties/databases.rake +377 -0
  164. data/lib/active_record/readonly_attributes.rb +24 -0
  165. data/lib/active_record/reflection.rb +1044 -0
  166. data/lib/active_record/relation.rb +629 -0
  167. data/lib/active_record/relation/batches.rb +287 -0
  168. data/lib/active_record/relation/batches/batch_enumerator.rb +69 -0
  169. data/lib/active_record/relation/calculations.rb +417 -0
  170. data/lib/active_record/relation/delegation.rb +147 -0
  171. data/lib/active_record/relation/finder_methods.rb +565 -0
  172. data/lib/active_record/relation/from_clause.rb +26 -0
  173. data/lib/active_record/relation/merger.rb +193 -0
  174. data/lib/active_record/relation/predicate_builder.rb +152 -0
  175. data/lib/active_record/relation/predicate_builder/array_handler.rb +48 -0
  176. data/lib/active_record/relation/predicate_builder/association_query_value.rb +46 -0
  177. data/lib/active_record/relation/predicate_builder/base_handler.rb +19 -0
  178. data/lib/active_record/relation/predicate_builder/basic_object_handler.rb +20 -0
  179. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +56 -0
  180. data/lib/active_record/relation/predicate_builder/range_handler.rb +42 -0
  181. data/lib/active_record/relation/predicate_builder/relation_handler.rb +19 -0
  182. data/lib/active_record/relation/query_attribute.rb +45 -0
  183. data/lib/active_record/relation/query_methods.rb +1231 -0
  184. data/lib/active_record/relation/record_fetch_warning.rb +51 -0
  185. data/lib/active_record/relation/spawn_methods.rb +77 -0
  186. data/lib/active_record/relation/where_clause.rb +186 -0
  187. data/lib/active_record/relation/where_clause_factory.rb +34 -0
  188. data/lib/active_record/result.rb +149 -0
  189. data/lib/active_record/runtime_registry.rb +24 -0
  190. data/lib/active_record/sanitization.rb +222 -0
  191. data/lib/active_record/schema.rb +70 -0
  192. data/lib/active_record/schema_dumper.rb +255 -0
  193. data/lib/active_record/schema_migration.rb +56 -0
  194. data/lib/active_record/scoping.rb +106 -0
  195. data/lib/active_record/scoping/default.rb +152 -0
  196. data/lib/active_record/scoping/named.rb +213 -0
  197. data/lib/active_record/secure_token.rb +40 -0
  198. data/lib/active_record/serialization.rb +22 -0
  199. data/lib/active_record/statement_cache.rb +121 -0
  200. data/lib/active_record/store.rb +211 -0
  201. data/lib/active_record/suppressor.rb +61 -0
  202. data/lib/active_record/table_metadata.rb +82 -0
  203. data/lib/active_record/tasks/database_tasks.rb +337 -0
  204. data/lib/active_record/tasks/mysql_database_tasks.rb +115 -0
  205. data/lib/active_record/tasks/postgresql_database_tasks.rb +143 -0
  206. data/lib/active_record/tasks/sqlite_database_tasks.rb +83 -0
  207. data/lib/active_record/timestamp.rb +153 -0
  208. data/lib/active_record/touch_later.rb +64 -0
  209. data/lib/active_record/transactions.rb +502 -0
  210. data/lib/active_record/translation.rb +24 -0
  211. data/lib/active_record/type.rb +79 -0
  212. data/lib/active_record/type/adapter_specific_registry.rb +136 -0
  213. data/lib/active_record/type/date.rb +9 -0
  214. data/lib/active_record/type/date_time.rb +9 -0
  215. data/lib/active_record/type/decimal_without_scale.rb +15 -0
  216. data/lib/active_record/type/hash_lookup_type_map.rb +25 -0
  217. data/lib/active_record/type/internal/timezone.rb +17 -0
  218. data/lib/active_record/type/json.rb +30 -0
  219. data/lib/active_record/type/serialized.rb +71 -0
  220. data/lib/active_record/type/text.rb +11 -0
  221. data/lib/active_record/type/time.rb +21 -0
  222. data/lib/active_record/type/type_map.rb +62 -0
  223. data/lib/active_record/type/unsigned_integer.rb +17 -0
  224. data/lib/active_record/type_caster.rb +9 -0
  225. data/lib/active_record/type_caster/connection.rb +33 -0
  226. data/lib/active_record/type_caster/map.rb +23 -0
  227. data/lib/active_record/validations.rb +93 -0
  228. data/lib/active_record/validations/absence.rb +25 -0
  229. data/lib/active_record/validations/associated.rb +60 -0
  230. data/lib/active_record/validations/length.rb +26 -0
  231. data/lib/active_record/validations/presence.rb +68 -0
  232. data/lib/active_record/validations/uniqueness.rb +238 -0
  233. data/lib/active_record/version.rb +10 -0
  234. data/lib/rails/generators/active_record.rb +19 -0
  235. data/lib/rails/generators/active_record/application_record/application_record_generator.rb +27 -0
  236. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +5 -0
  237. data/lib/rails/generators/active_record/migration.rb +35 -0
  238. data/lib/rails/generators/active_record/migration/migration_generator.rb +78 -0
  239. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +24 -0
  240. data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +46 -0
  241. data/lib/rails/generators/active_record/model/model_generator.rb +48 -0
  242. data/lib/rails/generators/active_record/model/templates/model.rb.tt +13 -0
  243. data/lib/rails/generators/active_record/model/templates/module.rb.tt +7 -0
  244. metadata +333 -0
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/associations"
4
+
5
+ module ActiveRecord::Associations::Builder # :nodoc:
6
+ class CollectionAssociation < Association #:nodoc:
7
+ CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
8
+
9
+ def self.valid_options(options)
10
+ super + [:table_name, :before_add,
11
+ :after_add, :before_remove, :after_remove, :extend]
12
+ end
13
+
14
+ def self.define_callbacks(model, reflection)
15
+ super
16
+ name = reflection.name
17
+ options = reflection.options
18
+ CALLBACKS.each { |callback_name|
19
+ define_callback(model, callback_name, name, options)
20
+ }
21
+ end
22
+
23
+ def self.define_extensions(model, name)
24
+ if block_given?
25
+ extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
26
+ extension = Module.new(&Proc.new)
27
+ model.parent.const_set(extension_module_name, extension)
28
+ end
29
+ end
30
+
31
+ def self.define_callback(model, callback_name, name, options)
32
+ full_callback_name = "#{callback_name}_for_#{name}"
33
+
34
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
35
+ model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
36
+ callbacks = Array(options[callback_name.to_sym]).map do |callback|
37
+ case callback
38
+ when Symbol
39
+ ->(method, owner, record) { owner.send(callback, record) }
40
+ when Proc
41
+ ->(method, owner, record) { callback.call(owner, record) }
42
+ else
43
+ ->(method, owner, record) { callback.send(method, owner, record) }
44
+ end
45
+ end
46
+ model.send "#{full_callback_name}=", callbacks
47
+ end
48
+
49
+ # Defines the setter and getter methods for the collection_singular_ids.
50
+ def self.define_readers(mixin, name)
51
+ super
52
+
53
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
54
+ def #{name.to_s.singularize}_ids
55
+ association(:#{name}).ids_reader
56
+ end
57
+ CODE
58
+ end
59
+
60
+ def self.define_writers(mixin, name)
61
+ super
62
+
63
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
64
+ def #{name.to_s.singularize}_ids=(ids)
65
+ association(:#{name}).ids_writer(ids)
66
+ end
67
+ CODE
68
+ end
69
+
70
+ def self.wrap_scope(scope, mod)
71
+ if scope
72
+ if scope.arity > 0
73
+ proc { |owner| instance_exec(owner, &scope).extending(mod) }
74
+ else
75
+ proc { instance_exec(&scope).extending(mod) }
76
+ end
77
+ else
78
+ proc { extending(mod) }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord::Associations::Builder # :nodoc:
4
+ class HasAndBelongsToMany # :nodoc:
5
+ class JoinTableResolver # :nodoc:
6
+ KnownTable = Struct.new :join_table
7
+
8
+ class KnownClass # :nodoc:
9
+ def initialize(lhs_class, rhs_class_name)
10
+ @lhs_class = lhs_class
11
+ @rhs_class_name = rhs_class_name
12
+ @join_table = nil
13
+ end
14
+
15
+ def join_table
16
+ @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
17
+ end
18
+
19
+ private
20
+
21
+ def klass
22
+ @lhs_class.send(:compute_type, @rhs_class_name)
23
+ end
24
+ end
25
+
26
+ def self.build(lhs_class, name, options)
27
+ if options[:join_table]
28
+ KnownTable.new options[:join_table].to_s
29
+ else
30
+ class_name = options.fetch(:class_name) {
31
+ name.to_s.camelize.singularize
32
+ }
33
+ KnownClass.new lhs_class, class_name.to_s
34
+ end
35
+ end
36
+ end
37
+
38
+ attr_reader :lhs_model, :association_name, :options
39
+
40
+ def initialize(association_name, lhs_model, options)
41
+ @association_name = association_name
42
+ @lhs_model = lhs_model
43
+ @options = options
44
+ end
45
+
46
+ def through_model
47
+ habtm = JoinTableResolver.build lhs_model, association_name, options
48
+
49
+ join_model = Class.new(ActiveRecord::Base) {
50
+ class << self
51
+ attr_accessor :left_model
52
+ attr_accessor :name
53
+ attr_accessor :table_name_resolver
54
+ attr_accessor :left_reflection
55
+ attr_accessor :right_reflection
56
+ end
57
+
58
+ def self.table_name
59
+ table_name_resolver.join_table
60
+ end
61
+
62
+ def self.compute_type(class_name)
63
+ left_model.compute_type class_name
64
+ end
65
+
66
+ def self.add_left_association(name, options)
67
+ belongs_to name, required: false, **options
68
+ self.left_reflection = _reflect_on_association(name)
69
+ end
70
+
71
+ def self.add_right_association(name, options)
72
+ rhs_name = name.to_s.singularize.to_sym
73
+ belongs_to rhs_name, required: false, **options
74
+ self.right_reflection = _reflect_on_association(rhs_name)
75
+ end
76
+
77
+ def self.retrieve_connection
78
+ left_model.retrieve_connection
79
+ end
80
+
81
+ private
82
+
83
+ def self.suppress_composite_primary_key(pk)
84
+ pk unless pk.is_a?(Array)
85
+ end
86
+ }
87
+
88
+ join_model.name = "HABTM_#{association_name.to_s.camelize}"
89
+ join_model.table_name_resolver = habtm
90
+ join_model.left_model = lhs_model
91
+
92
+ join_model.add_left_association :left_side, anonymous_class: lhs_model
93
+ join_model.add_right_association association_name, belongs_to_options(options)
94
+ join_model
95
+ end
96
+
97
+ def middle_reflection(join_model)
98
+ middle_name = [lhs_model.name.downcase.pluralize,
99
+ association_name].join("_".freeze).gsub("::".freeze, "_".freeze).to_sym
100
+ middle_options = middle_options join_model
101
+
102
+ HasMany.create_reflection(lhs_model,
103
+ middle_name,
104
+ nil,
105
+ middle_options)
106
+ end
107
+
108
+ private
109
+
110
+ def middle_options(join_model)
111
+ middle_options = {}
112
+ middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}"
113
+ middle_options[:source] = join_model.left_reflection.name
114
+ if options.key? :foreign_key
115
+ middle_options[:foreign_key] = options[:foreign_key]
116
+ end
117
+ middle_options
118
+ end
119
+
120
+ def belongs_to_options(options)
121
+ rhs_options = {}
122
+
123
+ if options.key? :class_name
124
+ rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key
125
+ rhs_options[:class_name] = options[:class_name]
126
+ end
127
+
128
+ if options.key? :association_foreign_key
129
+ rhs_options[:foreign_key] = options[:association_foreign_key]
130
+ end
131
+
132
+ rhs_options
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord::Associations::Builder # :nodoc:
4
+ class HasMany < CollectionAssociation #:nodoc:
5
+ def self.macro
6
+ :has_many
7
+ end
8
+
9
+ def self.valid_options(options)
10
+ super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors]
11
+ end
12
+
13
+ def self.valid_dependent_options
14
+ [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord::Associations::Builder # :nodoc:
4
+ class HasOne < SingularAssociation #:nodoc:
5
+ def self.macro
6
+ :has_one
7
+ end
8
+
9
+ def self.valid_options(options)
10
+ valid = super + [:as]
11
+ valid += [:through, :source, :source_type] if options[:through]
12
+ valid
13
+ end
14
+
15
+ def self.valid_dependent_options
16
+ [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
17
+ end
18
+
19
+ def self.add_destroy_callbacks(model, reflection)
20
+ super unless reflection.options[:through]
21
+ end
22
+
23
+ def self.define_validations(model, reflection)
24
+ super
25
+ if reflection.options[:required]
26
+ model.validates_presence_of reflection.name, message: :required
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class is inherited by the has_one and belongs_to association classes
4
+
5
+ module ActiveRecord::Associations::Builder # :nodoc:
6
+ class SingularAssociation < Association #:nodoc:
7
+ def self.valid_options(options)
8
+ super + [:foreign_type, :dependent, :primary_key, :inverse_of, :required]
9
+ end
10
+
11
+ def self.define_accessors(model, reflection)
12
+ super
13
+ mixin = model.generated_association_methods
14
+ name = reflection.name
15
+
16
+ define_constructors(mixin, name) if reflection.constructable?
17
+
18
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
19
+ def reload_#{name}
20
+ association(:#{name}).force_reload_reader
21
+ end
22
+ CODE
23
+ end
24
+
25
+ # Defines the (build|create)_association methods for belongs_to or has_one association
26
+ def self.define_constructors(mixin, name)
27
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
28
+ def build_#{name}(*args, &block)
29
+ association(:#{name}).build(*args, &block)
30
+ end
31
+
32
+ def create_#{name}(*args, &block)
33
+ association(:#{name}).create(*args, &block)
34
+ end
35
+
36
+ def create_#{name}!(*args, &block)
37
+ association(:#{name}).create!(*args, &block)
38
+ end
39
+ CODE
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,513 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ # = Active Record Association Collection
6
+ #
7
+ # CollectionAssociation is an abstract class that provides common stuff to
8
+ # ease the implementation of association proxies that represent
9
+ # collections. See the class hierarchy in Association.
10
+ #
11
+ # CollectionAssociation:
12
+ # HasManyAssociation => has_many
13
+ # HasManyThroughAssociation + ThroughAssociation => has_many :through
14
+ #
15
+ # The CollectionAssociation class provides common methods to the collections
16
+ # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with
17
+ # the +:through association+ option.
18
+ #
19
+ # You need to be careful with assumptions regarding the target: The proxy
20
+ # does not fetch records from the database until it needs them, but new
21
+ # ones created with +build+ are added to the target. So, the target may be
22
+ # non-empty and still lack children waiting to be read from the database.
23
+ # If you look directly to the database you cannot assume that's the entire
24
+ # collection because new records may have been added to the target, etc.
25
+ #
26
+ # If you need to work on all current children, new and existing records,
27
+ # +load_target+ and the +loaded+ flag are your friends.
28
+ class CollectionAssociation < Association #:nodoc:
29
+ # Implements the reader method, e.g. foo.items for Foo.has_many :items
30
+ def reader
31
+ if stale_target?
32
+ reload
33
+ end
34
+
35
+ @proxy ||= CollectionProxy.create(klass, self)
36
+ @proxy.reset_scope
37
+ end
38
+
39
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
40
+ def writer(records)
41
+ replace(records)
42
+ end
43
+
44
+ # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
45
+ def ids_reader
46
+ if loaded?
47
+ target.pluck(reflection.association_primary_key)
48
+ elsif !target.empty?
49
+ load_target.pluck(reflection.association_primary_key)
50
+ else
51
+ @association_ids ||= scope.pluck(reflection.association_primary_key)
52
+ end
53
+ end
54
+
55
+ # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
56
+ def ids_writer(ids)
57
+ primary_key = reflection.association_primary_key
58
+ pk_type = klass.type_for_attribute(primary_key)
59
+ ids = Array(ids).reject(&:blank?)
60
+ ids.map! { |i| pk_type.cast(i) }
61
+
62
+ records = klass.where(primary_key => ids).index_by do |r|
63
+ r.public_send(primary_key)
64
+ end.values_at(*ids).compact
65
+
66
+ if records.size != ids.size
67
+ found_ids = records.map { |record| record.public_send(primary_key) }
68
+ not_found_ids = ids - found_ids
69
+ klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, primary_key, not_found_ids)
70
+ else
71
+ replace(records)
72
+ end
73
+ end
74
+
75
+ def reset
76
+ super
77
+ @target = []
78
+ @association_ids = nil
79
+ end
80
+
81
+ def find(*args)
82
+ if options[:inverse_of] && loaded?
83
+ args_flatten = args.flatten
84
+ model = scope.klass
85
+
86
+ if args_flatten.blank?
87
+ error_message = "Couldn't find #{model.name} without an ID"
88
+ raise RecordNotFound.new(error_message, model.name, model.primary_key, args)
89
+ end
90
+
91
+ result = find_by_scan(*args)
92
+
93
+ result_size = Array(result).size
94
+ if !result || result_size != args_flatten.size
95
+ scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
96
+ else
97
+ result
98
+ end
99
+ else
100
+ scope.find(*args)
101
+ end
102
+ end
103
+
104
+ def build(attributes = {}, &block)
105
+ if attributes.is_a?(Array)
106
+ attributes.collect { |attr| build(attr, &block) }
107
+ else
108
+ add_to_target(build_record(attributes, &block))
109
+ end
110
+ end
111
+
112
+ # Add +records+ to this association. Since +<<+ flattens its argument list
113
+ # and inserts each record, +push+ and +concat+ behave identically.
114
+ def concat(*records)
115
+ records = records.flatten
116
+ if owner.new_record?
117
+ load_target
118
+ concat_records(records)
119
+ else
120
+ transaction { concat_records(records) }
121
+ end
122
+ end
123
+
124
+ # Starts a transaction in the association class's database connection.
125
+ #
126
+ # class Author < ActiveRecord::Base
127
+ # has_many :books
128
+ # end
129
+ #
130
+ # Author.first.books.transaction do
131
+ # # same effect as calling Book.transaction
132
+ # end
133
+ def transaction(*args)
134
+ reflection.klass.transaction(*args) do
135
+ yield
136
+ end
137
+ end
138
+
139
+ # Removes all records from the association without calling callbacks
140
+ # on the associated records. It honors the +:dependent+ option. However
141
+ # if the +:dependent+ value is +:destroy+ then in that case the +:delete_all+
142
+ # deletion strategy for the association is applied.
143
+ #
144
+ # You can force a particular deletion strategy by passing a parameter.
145
+ #
146
+ # Example:
147
+ #
148
+ # @author.books.delete_all(:nullify)
149
+ # @author.books.delete_all(:delete_all)
150
+ #
151
+ # See delete for more info.
152
+ def delete_all(dependent = nil)
153
+ if dependent && ![:nullify, :delete_all].include?(dependent)
154
+ raise ArgumentError, "Valid values are :nullify or :delete_all"
155
+ end
156
+
157
+ dependent = if dependent
158
+ dependent
159
+ elsif options[:dependent] == :destroy
160
+ :delete_all
161
+ else
162
+ options[:dependent]
163
+ end
164
+
165
+ delete_or_nullify_all_records(dependent).tap do
166
+ reset
167
+ loaded!
168
+ end
169
+ end
170
+
171
+ # Destroy all the records from this association.
172
+ #
173
+ # See destroy for more info.
174
+ def destroy_all
175
+ destroy(load_target).tap do
176
+ reset
177
+ loaded!
178
+ end
179
+ end
180
+
181
+ # Removes +records+ from this association calling +before_remove+ and
182
+ # +after_remove+ callbacks.
183
+ #
184
+ # This method is abstract in the sense that +delete_records+ has to be
185
+ # provided by descendants. Note this method does not imply the records
186
+ # are actually removed from the database, that depends precisely on
187
+ # +delete_records+. They are in any case removed from the collection.
188
+ def delete(*records)
189
+ delete_or_destroy(records, options[:dependent])
190
+ end
191
+
192
+ # Deletes the +records+ and removes them from this association calling
193
+ # +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
194
+ #
195
+ # Note that this method removes records from the database ignoring the
196
+ # +:dependent+ option.
197
+ def destroy(*records)
198
+ delete_or_destroy(records, :destroy)
199
+ end
200
+
201
+ # Returns the size of the collection by executing a SELECT COUNT(*)
202
+ # query if the collection hasn't been loaded, and calling
203
+ # <tt>collection.size</tt> if it has.
204
+ #
205
+ # If the collection has been already loaded +size+ and +length+ are
206
+ # equivalent. If not and you are going to need the records anyway
207
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
208
+ #
209
+ # This method is abstract in the sense that it relies on
210
+ # +count_records+, which is a method descendants have to provide.
211
+ def size
212
+ if !find_target? || loaded?
213
+ target.size
214
+ elsif !association_scope.group_values.empty?
215
+ load_target.size
216
+ elsif !association_scope.distinct_value && target.is_a?(Array)
217
+ unsaved_records = target.select(&:new_record?)
218
+ unsaved_records.size + count_records
219
+ else
220
+ count_records
221
+ end
222
+ end
223
+
224
+ # Returns true if the collection is empty.
225
+ #
226
+ # If the collection has been loaded
227
+ # it is equivalent to <tt>collection.size.zero?</tt>. If the
228
+ # collection has not been loaded, it is equivalent to
229
+ # <tt>collection.exists?</tt>. If the collection has not already been
230
+ # loaded and you are going to fetch the records anyway it is better to
231
+ # check <tt>collection.length.zero?</tt>.
232
+ def empty?
233
+ if loaded?
234
+ size.zero?
235
+ else
236
+ @target.blank? && !scope.exists?
237
+ end
238
+ end
239
+
240
+ # Replace this collection with +other_array+. This will perform a diff
241
+ # and delete/add only records that have changed.
242
+ def replace(other_array)
243
+ other_array.each { |val| raise_on_type_mismatch!(val) }
244
+ original_target = load_target.dup
245
+
246
+ if owner.new_record?
247
+ replace_records(other_array, original_target)
248
+ else
249
+ replace_common_records_in_memory(other_array, original_target)
250
+ if other_array != original_target
251
+ transaction { replace_records(other_array, original_target) }
252
+ else
253
+ other_array
254
+ end
255
+ end
256
+ end
257
+
258
+ def include?(record)
259
+ if record.is_a?(reflection.klass)
260
+ if record.new_record?
261
+ include_in_memory?(record)
262
+ else
263
+ loaded? ? target.include?(record) : scope.exists?(record.id)
264
+ end
265
+ else
266
+ false
267
+ end
268
+ end
269
+
270
+ def load_target
271
+ if find_target?
272
+ @target = merge_target_lists(find_target, target)
273
+ end
274
+
275
+ loaded!
276
+ target
277
+ end
278
+
279
+ def add_to_target(record, skip_callbacks = false, &block)
280
+ if association_scope.distinct_value
281
+ index = @target.index(record)
282
+ end
283
+ replace_on_target(record, index, skip_callbacks, &block)
284
+ end
285
+
286
+ def scope
287
+ scope = super
288
+ scope.none! if null_scope?
289
+ scope
290
+ end
291
+
292
+ def null_scope?
293
+ owner.new_record? && !foreign_key_present?
294
+ end
295
+
296
+ def find_from_target?
297
+ loaded? ||
298
+ owner.new_record? ||
299
+ target.any? { |record| record.new_record? || record.changed? }
300
+ end
301
+
302
+ private
303
+
304
+ def find_target
305
+ scope = self.scope
306
+ return scope.to_a if skip_statement_cache?(scope)
307
+
308
+ conn = klass.connection
309
+ sc = reflection.association_scope_cache(conn, owner) do |params|
310
+ as = AssociationScope.create { params.bind }
311
+ target_scope.merge!(as.scope(self))
312
+ end
313
+
314
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
315
+ sc.execute(binds, conn) do |record|
316
+ set_inverse_instance(record)
317
+ end
318
+ end
319
+
320
+ # We have some records loaded from the database (persisted) and some that are
321
+ # in-memory (memory). The same record may be represented in the persisted array
322
+ # and in the memory array.
323
+ #
324
+ # So the task of this method is to merge them according to the following rules:
325
+ #
326
+ # * The final array must not have duplicates
327
+ # * The order of the persisted array is to be preserved
328
+ # * Any changes made to attributes on objects in the memory array are to be preserved
329
+ # * Otherwise, attributes should have the value found in the database
330
+ def merge_target_lists(persisted, memory)
331
+ return persisted if memory.empty?
332
+ return memory if persisted.empty?
333
+
334
+ persisted.map! do |record|
335
+ if mem_record = memory.delete(record)
336
+
337
+ ((record.attribute_names & mem_record.attribute_names) - mem_record.changed_attribute_names_to_save).each do |name|
338
+ mem_record[name] = record[name]
339
+ end
340
+
341
+ mem_record
342
+ else
343
+ record
344
+ end
345
+ end
346
+
347
+ persisted + memory
348
+ end
349
+
350
+ def _create_record(attributes, raise = false, &block)
351
+ unless owner.persisted?
352
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
353
+ end
354
+
355
+ if attributes.is_a?(Array)
356
+ attributes.collect { |attr| _create_record(attr, raise, &block) }
357
+ else
358
+ record = build_record(attributes, &block)
359
+ transaction do
360
+ result = nil
361
+ add_to_target(record) do
362
+ result = insert_record(record, true, raise) {
363
+ @_was_loaded = loaded?
364
+ }
365
+ end
366
+ raise ActiveRecord::Rollback unless result
367
+ end
368
+ record
369
+ end
370
+ end
371
+
372
+ # Do the relevant stuff to insert the given record into the association collection.
373
+ def insert_record(record, validate = true, raise = false, &block)
374
+ if raise
375
+ record.save!(validate: validate, &block)
376
+ else
377
+ record.save(validate: validate, &block)
378
+ end
379
+ end
380
+
381
+ def delete_or_destroy(records, method)
382
+ return if records.empty?
383
+ records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) }
384
+ records = records.flatten
385
+ records.each { |record| raise_on_type_mismatch!(record) }
386
+ existing_records = records.reject(&:new_record?)
387
+
388
+ if existing_records.empty?
389
+ remove_records(existing_records, records, method)
390
+ else
391
+ transaction { remove_records(existing_records, records, method) }
392
+ end
393
+ end
394
+
395
+ def remove_records(existing_records, records, method)
396
+ records.each { |record| callback(:before_remove, record) }
397
+
398
+ delete_records(existing_records, method) if existing_records.any?
399
+ records.each { |record| target.delete(record) }
400
+ @association_ids = nil
401
+
402
+ records.each { |record| callback(:after_remove, record) }
403
+ end
404
+
405
+ # Delete the given records from the association,
406
+ # using one of the methods +:destroy+, +:delete_all+
407
+ # or +:nullify+ (or +nil+, in which case a default is used).
408
+ def delete_records(records, method)
409
+ raise NotImplementedError
410
+ end
411
+
412
+ def replace_records(new_target, original_target)
413
+ delete(difference(target, new_target))
414
+
415
+ unless concat(difference(new_target, target))
416
+ @target = original_target
417
+ raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
418
+ "new records could not be saved."
419
+ end
420
+
421
+ target
422
+ end
423
+
424
+ def replace_common_records_in_memory(new_target, original_target)
425
+ common_records = intersection(new_target, original_target)
426
+ common_records.each do |record|
427
+ skip_callbacks = true
428
+ replace_on_target(record, @target.index(record), skip_callbacks)
429
+ end
430
+ end
431
+
432
+ def concat_records(records, raise = false)
433
+ result = true
434
+
435
+ records.each do |record|
436
+ raise_on_type_mismatch!(record)
437
+ add_to_target(record) do
438
+ unless owner.new_record?
439
+ result &&= insert_record(record, true, raise) {
440
+ @_was_loaded = loaded?
441
+ }
442
+ end
443
+ end
444
+ end
445
+
446
+ raise ActiveRecord::Rollback unless result
447
+
448
+ records
449
+ end
450
+
451
+ def replace_on_target(record, index, skip_callbacks)
452
+ callback(:before_add, record) unless skip_callbacks
453
+
454
+ set_inverse_instance(record)
455
+
456
+ @_was_loaded = true
457
+
458
+ yield(record) if block_given?
459
+
460
+ if index
461
+ target[index] = record
462
+ elsif @_was_loaded || !loaded?
463
+ @association_ids = nil
464
+ target << record
465
+ end
466
+
467
+ callback(:after_add, record) unless skip_callbacks
468
+
469
+ record
470
+ ensure
471
+ @_was_loaded = nil
472
+ end
473
+
474
+ def callback(method, record)
475
+ callbacks_for(method).each do |callback|
476
+ callback.call(method, owner, record)
477
+ end
478
+ end
479
+
480
+ def callbacks_for(callback_name)
481
+ full_callback_name = "#{callback_name}_for_#{reflection.name}"
482
+ owner.class.send(full_callback_name)
483
+ end
484
+
485
+ def include_in_memory?(record)
486
+ if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
487
+ assoc = owner.association(reflection.through_reflection.name)
488
+ assoc.reader.any? { |source|
489
+ target_reflection = source.send(reflection.source_reflection.name)
490
+ target_reflection.respond_to?(:include?) ? target_reflection.include?(record) : target_reflection == record
491
+ } || target.include?(record)
492
+ else
493
+ target.include?(record)
494
+ end
495
+ end
496
+
497
+ # If the :inverse_of option has been
498
+ # specified, then #find scans the entire collection.
499
+ def find_by_scan(*args)
500
+ expects_array = args.first.kind_of?(Array)
501
+ ids = args.flatten.compact.map(&:to_s).uniq
502
+
503
+ if ids.size == 1
504
+ id = ids.first
505
+ record = load_target.detect { |r| id == r.id.to_s }
506
+ expects_array ? [ record ] : record
507
+ else
508
+ load_target.select { |r| ids.include?(r.id.to_s) }
509
+ end
510
+ end
511
+ end
512
+ end
513
+ end