activerecord 6.0.3.5 → 6.1.0.rc1

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 (245) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +774 -735
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/lib/active_record.rb +7 -14
  6. data/lib/active_record/aggregations.rb +1 -1
  7. data/lib/active_record/association_relation.rb +22 -14
  8. data/lib/active_record/associations.rb +114 -11
  9. data/lib/active_record/associations/alias_tracker.rb +19 -15
  10. data/lib/active_record/associations/association.rb +40 -29
  11. data/lib/active_record/associations/association_scope.rb +17 -15
  12. data/lib/active_record/associations/belongs_to_association.rb +15 -5
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +1 -1
  14. data/lib/active_record/associations/builder/association.rb +9 -3
  15. data/lib/active_record/associations/builder/belongs_to.rb +10 -7
  16. data/lib/active_record/associations/builder/collection_association.rb +5 -4
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +0 -1
  18. data/lib/active_record/associations/builder/has_many.rb +6 -2
  19. data/lib/active_record/associations/builder/has_one.rb +11 -14
  20. data/lib/active_record/associations/builder/singular_association.rb +1 -1
  21. data/lib/active_record/associations/collection_association.rb +19 -6
  22. data/lib/active_record/associations/collection_proxy.rb +13 -5
  23. data/lib/active_record/associations/foreign_association.rb +13 -0
  24. data/lib/active_record/associations/has_many_association.rb +24 -2
  25. data/lib/active_record/associations/has_many_through_association.rb +10 -4
  26. data/lib/active_record/associations/has_one_association.rb +15 -1
  27. data/lib/active_record/associations/join_dependency.rb +72 -50
  28. data/lib/active_record/associations/join_dependency/join_association.rb +36 -14
  29. data/lib/active_record/associations/join_dependency/join_part.rb +3 -3
  30. data/lib/active_record/associations/preloader.rb +11 -5
  31. data/lib/active_record/associations/preloader/association.rb +51 -25
  32. data/lib/active_record/associations/preloader/through_association.rb +2 -2
  33. data/lib/active_record/associations/singular_association.rb +1 -1
  34. data/lib/active_record/associations/through_association.rb +1 -1
  35. data/lib/active_record/attribute_assignment.rb +10 -8
  36. data/lib/active_record/attribute_methods.rb +52 -48
  37. data/lib/active_record/attribute_methods/before_type_cast.rb +13 -9
  38. data/lib/active_record/attribute_methods/dirty.rb +1 -11
  39. data/lib/active_record/attribute_methods/primary_key.rb +6 -2
  40. data/lib/active_record/attribute_methods/query.rb +3 -6
  41. data/lib/active_record/attribute_methods/read.rb +8 -11
  42. data/lib/active_record/attribute_methods/serialization.rb +4 -4
  43. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -13
  44. data/lib/active_record/attribute_methods/write.rb +12 -20
  45. data/lib/active_record/attributes.rb +27 -7
  46. data/lib/active_record/autosave_association.rb +57 -40
  47. data/lib/active_record/base.rb +2 -14
  48. data/lib/active_record/callbacks.rb +32 -22
  49. data/lib/active_record/coders/yaml_column.rb +1 -1
  50. data/lib/active_record/connection_adapters.rb +50 -0
  51. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +186 -134
  52. data/lib/active_record/connection_adapters/abstract/database_limits.rb +2 -44
  53. data/lib/active_record/connection_adapters/abstract/database_statements.rb +65 -22
  54. data/lib/active_record/connection_adapters/abstract/query_cache.rb +2 -7
  55. data/lib/active_record/connection_adapters/abstract/quoting.rb +34 -34
  56. data/lib/active_record/connection_adapters/abstract/savepoints.rb +3 -3
  57. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +153 -116
  58. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +112 -27
  59. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +3 -3
  60. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +224 -85
  61. data/lib/active_record/connection_adapters/abstract/transaction.rb +66 -24
  62. data/lib/active_record/connection_adapters/abstract_adapter.rb +36 -69
  63. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +129 -88
  64. data/lib/active_record/connection_adapters/column.rb +15 -1
  65. data/lib/active_record/connection_adapters/deduplicable.rb +29 -0
  66. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +31 -0
  67. data/lib/active_record/connection_adapters/mysql/database_statements.rb +23 -25
  68. data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +1 -1
  69. data/lib/active_record/connection_adapters/mysql/quoting.rb +1 -1
  70. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +33 -6
  71. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +8 -0
  72. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +1 -1
  73. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +11 -7
  74. data/lib/active_record/connection_adapters/mysql/type_metadata.rb +10 -1
  75. data/lib/active_record/connection_adapters/mysql2_adapter.rb +31 -12
  76. data/lib/active_record/connection_adapters/pool_config.rb +63 -0
  77. data/lib/active_record/connection_adapters/pool_manager.rb +43 -0
  78. data/lib/active_record/connection_adapters/postgresql/column.rb +24 -1
  79. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +13 -54
  80. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  81. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +3 -5
  82. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +2 -2
  83. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +2 -2
  84. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +49 -0
  85. data/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +2 -2
  86. data/lib/active_record/connection_adapters/postgresql/oid/macaddr.rb +25 -0
  87. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +2 -2
  88. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +2 -2
  89. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +24 -5
  90. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +11 -1
  91. data/lib/active_record/connection_adapters/postgresql/quoting.rb +4 -4
  92. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
  93. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +5 -1
  94. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +61 -29
  95. data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +8 -0
  96. data/lib/active_record/connection_adapters/postgresql_adapter.rb +72 -55
  97. data/lib/active_record/connection_adapters/schema_cache.rb +98 -15
  98. data/lib/active_record/connection_adapters/sql_type_metadata.rb +10 -0
  99. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +31 -6
  100. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +1 -1
  101. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +5 -1
  102. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +37 -4
  103. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +49 -50
  104. data/lib/active_record/connection_handling.rb +210 -71
  105. data/lib/active_record/core.rb +220 -55
  106. data/lib/active_record/database_configurations.rb +124 -85
  107. data/lib/active_record/database_configurations/connection_url_resolver.rb +98 -0
  108. data/lib/active_record/database_configurations/database_config.rb +52 -9
  109. data/lib/active_record/database_configurations/hash_config.rb +54 -8
  110. data/lib/active_record/database_configurations/url_config.rb +15 -40
  111. data/lib/active_record/delegated_type.rb +209 -0
  112. data/lib/active_record/destroy_association_async_job.rb +36 -0
  113. data/lib/active_record/enum.rb +27 -10
  114. data/lib/active_record/errors.rb +47 -12
  115. data/lib/active_record/explain.rb +9 -4
  116. data/lib/active_record/explain_subscriber.rb +1 -1
  117. data/lib/active_record/fixture_set/file.rb +10 -17
  118. data/lib/active_record/fixture_set/model_metadata.rb +1 -2
  119. data/lib/active_record/fixture_set/render_context.rb +1 -1
  120. data/lib/active_record/fixture_set/table_row.rb +2 -2
  121. data/lib/active_record/fixtures.rb +54 -8
  122. data/lib/active_record/gem_version.rb +3 -3
  123. data/lib/active_record/inheritance.rb +40 -18
  124. data/lib/active_record/insert_all.rb +33 -6
  125. data/lib/active_record/integration.rb +3 -5
  126. data/lib/active_record/internal_metadata.rb +15 -4
  127. data/lib/active_record/legacy_yaml_adapter.rb +7 -3
  128. data/lib/active_record/locking/optimistic.rb +22 -16
  129. data/lib/active_record/locking/pessimistic.rb +6 -2
  130. data/lib/active_record/log_subscriber.rb +26 -8
  131. data/lib/active_record/middleware/database_selector.rb +4 -1
  132. data/lib/active_record/middleware/database_selector/resolver.rb +5 -0
  133. data/lib/active_record/middleware/database_selector/resolver/session.rb +3 -0
  134. data/lib/active_record/migration.rb +113 -83
  135. data/lib/active_record/migration/command_recorder.rb +47 -27
  136. data/lib/active_record/migration/compatibility.rb +67 -17
  137. data/lib/active_record/model_schema.rb +88 -13
  138. data/lib/active_record/nested_attributes.rb +2 -3
  139. data/lib/active_record/no_touching.rb +1 -1
  140. data/lib/active_record/persistence.rb +50 -45
  141. data/lib/active_record/query_cache.rb +15 -5
  142. data/lib/active_record/querying.rb +11 -6
  143. data/lib/active_record/railtie.rb +64 -44
  144. data/lib/active_record/railties/databases.rake +253 -98
  145. data/lib/active_record/readonly_attributes.rb +4 -0
  146. data/lib/active_record/reflection.rb +70 -57
  147. data/lib/active_record/relation.rb +96 -67
  148. data/lib/active_record/relation/batches.rb +38 -31
  149. data/lib/active_record/relation/batches/batch_enumerator.rb +25 -9
  150. data/lib/active_record/relation/calculations.rb +101 -44
  151. data/lib/active_record/relation/delegation.rb +2 -1
  152. data/lib/active_record/relation/finder_methods.rb +45 -15
  153. data/lib/active_record/relation/from_clause.rb +1 -1
  154. data/lib/active_record/relation/merger.rb +27 -25
  155. data/lib/active_record/relation/predicate_builder.rb +57 -33
  156. data/lib/active_record/relation/predicate_builder/array_handler.rb +8 -9
  157. data/lib/active_record/relation/predicate_builder/association_query_value.rb +2 -2
  158. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +3 -3
  159. data/lib/active_record/relation/predicate_builder/relation_handler.rb +1 -1
  160. data/lib/active_record/relation/query_methods.rb +330 -195
  161. data/lib/active_record/relation/record_fetch_warning.rb +3 -3
  162. data/lib/active_record/relation/spawn_methods.rb +6 -5
  163. data/lib/active_record/relation/where_clause.rb +104 -57
  164. data/lib/active_record/result.rb +41 -33
  165. data/lib/active_record/runtime_registry.rb +2 -2
  166. data/lib/active_record/sanitization.rb +6 -17
  167. data/lib/active_record/schema_dumper.rb +34 -4
  168. data/lib/active_record/schema_migration.rb +0 -4
  169. data/lib/active_record/scoping/named.rb +6 -17
  170. data/lib/active_record/secure_token.rb +16 -8
  171. data/lib/active_record/serialization.rb +5 -3
  172. data/lib/active_record/signed_id.rb +116 -0
  173. data/lib/active_record/statement_cache.rb +20 -4
  174. data/lib/active_record/store.rb +2 -2
  175. data/lib/active_record/suppressor.rb +2 -2
  176. data/lib/active_record/table_metadata.rb +36 -52
  177. data/lib/active_record/tasks/database_tasks.rb +139 -113
  178. data/lib/active_record/tasks/mysql_database_tasks.rb +34 -35
  179. data/lib/active_record/tasks/postgresql_database_tasks.rb +24 -26
  180. data/lib/active_record/tasks/sqlite_database_tasks.rb +13 -9
  181. data/lib/active_record/test_databases.rb +5 -4
  182. data/lib/active_record/test_fixtures.rb +37 -16
  183. data/lib/active_record/timestamp.rb +4 -6
  184. data/lib/active_record/touch_later.rb +21 -21
  185. data/lib/active_record/transactions.rb +15 -64
  186. data/lib/active_record/type.rb +8 -1
  187. data/lib/active_record/type/serialized.rb +6 -2
  188. data/lib/active_record/type/time.rb +10 -0
  189. data/lib/active_record/type_caster/connection.rb +0 -1
  190. data/lib/active_record/type_caster/map.rb +8 -5
  191. data/lib/active_record/validations.rb +1 -0
  192. data/lib/active_record/validations/numericality.rb +35 -0
  193. data/lib/active_record/validations/uniqueness.rb +24 -4
  194. data/lib/arel.rb +5 -13
  195. data/lib/arel/attributes/attribute.rb +4 -0
  196. data/lib/arel/collectors/bind.rb +5 -0
  197. data/lib/arel/collectors/composite.rb +8 -0
  198. data/lib/arel/collectors/sql_string.rb +7 -0
  199. data/lib/arel/collectors/substitute_binds.rb +7 -0
  200. data/lib/arel/nodes.rb +3 -1
  201. data/lib/arel/nodes/binary.rb +82 -8
  202. data/lib/arel/nodes/bind_param.rb +8 -0
  203. data/lib/arel/nodes/casted.rb +21 -9
  204. data/lib/arel/nodes/equality.rb +6 -9
  205. data/lib/arel/nodes/grouping.rb +3 -0
  206. data/lib/arel/nodes/homogeneous_in.rb +72 -0
  207. data/lib/arel/nodes/in.rb +8 -1
  208. data/lib/arel/nodes/infix_operation.rb +13 -1
  209. data/lib/arel/nodes/join_source.rb +1 -1
  210. data/lib/arel/nodes/node.rb +7 -6
  211. data/lib/arel/nodes/ordering.rb +27 -0
  212. data/lib/arel/nodes/sql_literal.rb +3 -0
  213. data/lib/arel/nodes/table_alias.rb +7 -3
  214. data/lib/arel/nodes/unary.rb +0 -1
  215. data/lib/arel/predications.rb +12 -18
  216. data/lib/arel/select_manager.rb +1 -2
  217. data/lib/arel/table.rb +13 -5
  218. data/lib/arel/visitors.rb +0 -7
  219. data/lib/arel/visitors/dot.rb +14 -2
  220. data/lib/arel/visitors/mysql.rb +11 -1
  221. data/lib/arel/visitors/postgresql.rb +15 -4
  222. data/lib/arel/visitors/to_sql.rb +89 -78
  223. data/lib/rails/generators/active_record/migration.rb +6 -1
  224. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -0
  225. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +2 -0
  226. data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +3 -3
  227. data/lib/rails/generators/active_record/model/model_generator.rb +39 -2
  228. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +7 -0
  229. metadata +27 -28
  230. data/lib/active_record/advisory_lock_base.rb +0 -18
  231. data/lib/active_record/attribute_decorators.rb +0 -88
  232. data/lib/active_record/connection_adapters/connection_specification.rb +0 -296
  233. data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +0 -29
  234. data/lib/active_record/define_callbacks.rb +0 -22
  235. data/lib/active_record/railties/collection_cache_association_loading.rb +0 -34
  236. data/lib/active_record/relation/predicate_builder/base_handler.rb +0 -18
  237. data/lib/active_record/relation/where_clause_factory.rb +0 -33
  238. data/lib/arel/attributes.rb +0 -22
  239. data/lib/arel/visitors/depth_first.rb +0 -203
  240. data/lib/arel/visitors/ibm_db.rb +0 -34
  241. data/lib/arel/visitors/informix.rb +0 -62
  242. data/lib/arel/visitors/mssql.rb +0 -156
  243. data/lib/arel/visitors/oracle.rb +0 -158
  244. data/lib/arel/visitors/oracle12.rb +0 -65
  245. data/lib/arel/visitors/where_sql.rb +0 -22
@@ -23,6 +23,12 @@ module ActiveRecord
23
23
  # should not be dumped to db/schema.rb.
24
24
  cattr_accessor :fk_ignore_pattern, default: /^fk_rails_[0-9a-f]{10}$/
25
25
 
26
+ ##
27
+ # :singleton-method:
28
+ # Specify a custom regular expression matching check constraints which name
29
+ # should not be dumped to db/schema.rb.
30
+ cattr_accessor :chk_ignore_pattern, default: /^chk_rails_[0-9a-f]{10}$/
31
+
26
32
  class << self
27
33
  def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base)
28
34
  connection.create_schema_dumper(generate_options(config)).dump(stream)
@@ -72,8 +78,8 @@ module ActiveRecord
72
78
  # of editing this file, please use the migrations feature of Active Record to
73
79
  # incrementally modify your database, and then regenerate this schema definition.
74
80
  #
75
- # This file is the source Rails uses to define your schema when running `rails
76
- # db:schema:load`. When creating a new database, `rails db:schema:load` tends to
81
+ # This file is the source Rails uses to define your schema when running `bin/rails
82
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
77
83
  # be faster and is potentially less error prone than running all of your
78
84
  # migrations from scratch. Old migrations may fail to apply correctly if those
79
85
  # migrations use external dependencies or application code.
@@ -125,7 +131,10 @@ HEADER
125
131
  tbl.print ", primary_key: #{pk.inspect}" unless pk == "id"
126
132
  pkcol = columns.detect { |c| c.name == pk }
127
133
  pkcolspec = column_spec_for_primary_key(pkcol)
128
- if pkcolspec.present?
134
+ unless pkcolspec.empty?
135
+ if pkcolspec != pkcolspec.slice(:id, :default)
136
+ pkcolspec = { id: { type: pkcolspec.delete(:id), **pkcolspec }.compact }
137
+ end
129
138
  tbl.print ", #{format_colspec(pkcolspec)}"
130
139
  end
131
140
  when Array
@@ -156,6 +165,7 @@ HEADER
156
165
  end
157
166
 
158
167
  indexes_in_create(table, tbl)
168
+ check_constraints_in_create(table, tbl) if @connection.supports_check_constraints?
159
169
 
160
170
  tbl.puts " end"
161
171
  tbl.puts
@@ -209,6 +219,24 @@ HEADER
209
219
  index_parts
210
220
  end
211
221
 
222
+ def check_constraints_in_create(table, stream)
223
+ if (check_constraints = @connection.check_constraints(table)).any?
224
+ add_check_constraint_statements = check_constraints.map do |check_constraint|
225
+ parts = [
226
+ "t.check_constraint #{check_constraint.expression.inspect}"
227
+ ]
228
+
229
+ if check_constraint.export_name_on_schema_dump?
230
+ parts << "name: #{check_constraint.name.inspect}"
231
+ end
232
+
233
+ " #{parts.join(', ')}"
234
+ end
235
+
236
+ stream.puts add_check_constraint_statements.sort.join("\n")
237
+ end
238
+ end
239
+
212
240
  def foreign_keys(table, stream)
213
241
  if (foreign_keys = @connection.foreign_keys(table)).any?
214
242
  add_foreign_key_statements = foreign_keys.map do |foreign_key|
@@ -240,7 +268,9 @@ HEADER
240
268
  end
241
269
 
242
270
  def format_colspec(colspec)
243
- colspec.map { |key, value| "#{key}: #{value}" }.join(", ")
271
+ colspec.map do |key, value|
272
+ "#{key}: #{ value.is_a?(Hash) ? "{ #{format_colspec(value)} }" : value }"
273
+ end.join(", ")
244
274
  end
245
275
 
246
276
  def format_options(options)
@@ -22,10 +22,6 @@ module ActiveRecord
22
22
  "#{table_name_prefix}#{schema_migrations_table_name}#{table_name_suffix}"
23
23
  end
24
24
 
25
- def table_exists?
26
- connection.table_exists?(table_name)
27
- end
28
-
29
25
  def create_table
30
26
  unless table_exists?
31
27
  version_options = connection.internal_string_options_for_primary_key
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/array"
4
- require "active_support/core_ext/hash/except"
5
- require "active_support/core_ext/kernel/singleton_class"
6
-
7
3
  module ActiveRecord
8
4
  # = Active Record \Named \Scopes
9
5
  module Scoping
@@ -27,14 +23,6 @@ module ActiveRecord
27
23
  scope = current_scope
28
24
 
29
25
  if scope
30
- if scope._deprecated_scope_source
31
- ActiveSupport::Deprecation.warn(<<~MSG.squish)
32
- Class level methods will no longer inherit scoping from `#{scope._deprecated_scope_source}`
33
- in Rails 6.1. To continue using the scoped relation, pass it into the block directly.
34
- To instead access the full set of models, as Rails 6.1 will, use `#{name}.default_scoped`.
35
- MSG
36
- end
37
-
38
26
  if self == scope.klass
39
27
  scope.clone
40
28
  else
@@ -84,10 +72,6 @@ module ActiveRecord
84
72
  # <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, in effect,
85
73
  # represents the query <tt>Shirt.where(color: 'red')</tt>.
86
74
  #
87
- # You should always pass a callable object to the scopes defined
88
- # with #scope. This ensures that the scope is re-evaluated each
89
- # time it is called.
90
- #
91
75
  # Note that this is simply 'syntactic sugar' for defining an actual
92
76
  # class method:
93
77
  #
@@ -189,7 +173,7 @@ module ActiveRecord
189
173
 
190
174
  if body.respond_to?(:to_proc)
191
175
  singleton_class.define_method(name) do |*args|
192
- scope = all._exec_scope(name, *args, &body)
176
+ scope = all._exec_scope(*args, &body)
193
177
  scope = scope.extending(extension) if extension
194
178
  scope
195
179
  end
@@ -200,11 +184,16 @@ module ActiveRecord
200
184
  scope
201
185
  end
202
186
  end
187
+ singleton_class.send(:ruby2_keywords, name) if respond_to?(:ruby2_keywords, true)
203
188
 
204
189
  generate_relation_method(name)
205
190
  end
206
191
 
207
192
  private
193
+ def singleton_method_added(name)
194
+ generate_relation_method(name) if Kernel.respond_to?(name) && !ActiveRecord::Relation.method_defined?(name)
195
+ end
196
+
208
197
  def valid_scope_name?(name)
209
198
  if respond_to?(name, true) && logger
210
199
  logger.warn "Creating scope :#{name}. " \
@@ -2,6 +2,10 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module SecureToken
5
+ class MinimumLengthError < StandardError; end
6
+
7
+ MINIMUM_TOKEN_LENGTH = 24
8
+
5
9
  extend ActiveSupport::Concern
6
10
 
7
11
  module ClassMethods
@@ -10,30 +14,34 @@ module ActiveRecord
10
14
  # # Schema: User(token:string, auth_token:string)
11
15
  # class User < ActiveRecord::Base
12
16
  # has_secure_token
13
- # has_secure_token :auth_token
17
+ # has_secure_token :auth_token, length: 36
14
18
  # end
15
19
  #
16
20
  # user = User.new
17
21
  # user.save
18
22
  # user.token # => "pX27zsMN2ViQKta1bGfLmVJE"
19
- # user.auth_token # => "77TMHrHJFvFDwodq8w7Ev2m7"
23
+ # user.auth_token # => "tU9bLuZseefXQ4yQxQo8wjtBvsAfPc78os6R"
20
24
  # user.regenerate_token # => true
21
25
  # user.regenerate_auth_token # => true
22
26
  #
23
- # <tt>SecureRandom::base58</tt> is used to generate the 24-character unique token, so collisions are highly unlikely.
27
+ # <tt>SecureRandom::base58</tt> is used to generate at minimum a 24-character unique token, so collisions are highly unlikely.
24
28
  #
25
29
  # Note that it's still possible to generate a race condition in the database in the same way that
26
30
  # {validates_uniqueness_of}[rdoc-ref:Validations::ClassMethods#validates_uniqueness_of] can.
27
31
  # You're encouraged to add a unique index in the database to deal with this even more unlikely scenario.
28
- def has_secure_token(attribute = :token)
32
+ def has_secure_token(attribute = :token, length: MINIMUM_TOKEN_LENGTH)
33
+ if length < MINIMUM_TOKEN_LENGTH
34
+ raise MinimumLengthError, "Token requires a minimum length of #{MINIMUM_TOKEN_LENGTH} characters."
35
+ end
36
+
29
37
  # Load securerandom only when has_secure_token is used.
30
38
  require "active_support/core_ext/securerandom"
31
- define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token }
32
- before_create { send("#{attribute}=", self.class.generate_unique_secure_token) unless send("#{attribute}?") }
39
+ define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token(length: length) }
40
+ before_create { send("#{attribute}=", self.class.generate_unique_secure_token(length: length)) unless send("#{attribute}?") }
33
41
  end
34
42
 
35
- def generate_unique_secure_token
36
- SecureRandom.base58(24)
43
+ def generate_unique_secure_token(length: MINIMUM_TOKEN_LENGTH)
44
+ SecureRandom.base58(length)
37
45
  end
38
46
  end
39
47
  end
@@ -11,10 +11,12 @@ module ActiveRecord #:nodoc:
11
11
  end
12
12
 
13
13
  def serializable_hash(options = nil)
14
- options = options.try(:dup) || {}
14
+ if self.class._has_attribute?(self.class.inheritance_column)
15
+ options = options ? options.dup : {}
15
16
 
16
- options[:except] = Array(options[:except]).map(&:to_s)
17
- options[:except] |= Array(self.class.inheritance_column)
17
+ options[:except] = Array(options[:except]).map(&:to_s)
18
+ options[:except] |= Array(self.class.inheritance_column)
19
+ end
18
20
 
19
21
  super(options)
20
22
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ # = Active Record Signed Id
5
+ module SignedId
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ ##
10
+ # :singleton-method:
11
+ # Set the secret used for the signed id verifier instance when using Active Record outside of Rails.
12
+ # Within Rails, this is automatically set using the Rails application key generator.
13
+ mattr_accessor :signed_id_verifier_secret, instance_writer: false
14
+ end
15
+
16
+ module ClassMethods
17
+ # Lets you find a record based on a signed id that's safe to put into the world without risk of tampering.
18
+ # This is particularly useful for things like password reset or email verification, where you want
19
+ # the bearer of the signed id to be able to interact with the underlying record, but usually only within
20
+ # a certain time period.
21
+ #
22
+ # You set the time period that the signed id is valid for during generation, using the instance method
23
+ # +signed_id(expires_in: 15.minutes)+. If the time has elapsed before a signed find is attempted,
24
+ # the signed id will no longer be valid, and nil is returned.
25
+ #
26
+ # It's possible to further restrict the use of a signed id with a purpose. This helps when you have a
27
+ # general base model, like a User, which might have signed ids for several things, like password reset
28
+ # or email verification. The purpose that was set during generation must match the purpose set when
29
+ # finding. If there's a mismatch, nil is again returned.
30
+ #
31
+ # ==== Examples
32
+ #
33
+ # signed_id = User.first.signed_id expires_in: 15.minutes, purpose: :password_reset
34
+ #
35
+ # User.find_signed signed_id # => nil, since the purpose does not match
36
+ #
37
+ # travel 16.minutes
38
+ # User.find_signed signed_id, purpose: :password_reset # => nil, since the signed id has expired
39
+ #
40
+ # travel_back
41
+ # User.find_signed signed_id, purpose: :password_reset # => User.first
42
+ def find_signed(signed_id, purpose: nil)
43
+ raise UnknownPrimaryKey.new(self) if primary_key.nil?
44
+
45
+ if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose))
46
+ find_by primary_key => id
47
+ end
48
+ end
49
+
50
+ # Works like +find_signed+, but will raise an +ActiveSupport::MessageVerifier::InvalidSignature+
51
+ # exception if the +signed_id+ has either expired, has a purpose mismatch, is for another record,
52
+ # or has been tampered with. It will also raise an +ActiveRecord::RecordNotFound+ exception if
53
+ # the valid signed id can't find a record.
54
+ #
55
+ # === Examples
56
+ #
57
+ # User.find_signed! "bad data" # => ActiveSupport::MessageVerifier::InvalidSignature
58
+ #
59
+ # signed_id = User.first.signed_id
60
+ # User.first.destroy
61
+ # User.find_signed! signed_id # => ActiveRecord::RecordNotFound
62
+ def find_signed!(signed_id, purpose: nil)
63
+ if id = signed_id_verifier.verify(signed_id, purpose: combine_signed_id_purposes(purpose))
64
+ find(id)
65
+ end
66
+ end
67
+
68
+ # The verifier instance that all signed ids are generated and verified from. By default, it'll be initialized
69
+ # with the class-level +signed_id_verifier_secret+, which within Rails comes from the
70
+ # Rails.application.key_generator. By default, it's SHA256 for the digest and JSON for the serialization.
71
+ def signed_id_verifier
72
+ @signed_id_verifier ||= begin
73
+ secret = signed_id_verifier_secret
74
+ secret = secret.call if secret.respond_to?(:call)
75
+
76
+ if secret.nil?
77
+ raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed ids"
78
+ else
79
+ ActiveSupport::MessageVerifier.new secret, digest: "SHA256", serializer: JSON
80
+ end
81
+ end
82
+ end
83
+
84
+ # Allows you to pass in a custom verifier used for the signed ids. This also allows you to use different
85
+ # verifiers for different classes. This is also helpful if you need to rotate keys, as you can prepare
86
+ # your custom verifier for that in advance. See +ActiveSupport::MessageVerifier+ for details.
87
+ def signed_id_verifier=(verifier)
88
+ @signed_id_verifier = verifier
89
+ end
90
+
91
+ # :nodoc:
92
+ def combine_signed_id_purposes(purpose)
93
+ [ base_class.name.underscore, purpose.to_s ].compact_blank.join("/")
94
+ end
95
+ end
96
+
97
+
98
+ # Returns a signed id that's generated using a preconfigured +ActiveSupport::MessageVerifier+ instance.
99
+ # This signed id is tamper proof, so it's safe to send in an email or otherwise share with the outside world.
100
+ # It can further more be set to expire (the default is not to expire), and scoped down with a specific purpose.
101
+ # If the expiration date has been exceeded before +find_signed+ is called, the id won't find the designated
102
+ # record. If a purpose is set, this too must match.
103
+ #
104
+ # If you accidentally let a signed id out in the wild that you wish to retract sooner than its expiration date
105
+ # (or maybe you forgot to set an expiration date while meaning to!), you can use the purpose to essentially
106
+ # version the signed_id, like so:
107
+ #
108
+ # user.signed_id purpose: :v2
109
+ #
110
+ # And you then change your +find_signed+ calls to require this new purpose. Any old signed ids that were not
111
+ # created with the purpose will no longer find the record.
112
+ def signed_id(expires_in: nil, purpose: nil)
113
+ self.class.signed_id_verifier.generate id, expires_in: expires_in, purpose: self.class.combine_signed_id_purposes(purpose)
114
+ end
115
+ end
116
+ end
@@ -50,13 +50,20 @@ module ActiveRecord
50
50
 
51
51
  def sql_for(binds, connection)
52
52
  val = @values.dup
53
- casted_binds = binds.map(&:value_for_database)
54
- @indexes.each { |i| val[i] = connection.quote(casted_binds.shift) }
53
+ @indexes.each do |i|
54
+ value = binds.shift
55
+ if ActiveModel::Attribute === value
56
+ value = value.value_for_database
57
+ end
58
+ val[i] = connection.quote(value)
59
+ end
55
60
  val.join
56
61
  end
57
62
  end
58
63
 
59
64
  class PartialQueryCollector
65
+ attr_accessor :preparable
66
+
60
67
  def initialize
61
68
  @parts = []
62
69
  @binds = []
@@ -73,6 +80,15 @@ module ActiveRecord
73
80
  self
74
81
  end
75
82
 
83
+ def add_binds(binds)
84
+ @binds.concat binds
85
+ binds.size.times do |i|
86
+ @parts << ", " unless i == 0
87
+ @parts << Substitute.new
88
+ end
89
+ self
90
+ end
91
+
76
92
  def value
77
93
  [@parts, @binds]
78
94
  end
@@ -100,7 +116,7 @@ module ActiveRecord
100
116
  @bound_attributes = bound_attributes
101
117
 
102
118
  bound_attributes.each_with_index do |attr, i|
103
- if Substitute === attr.value
119
+ if ActiveModel::Attribute === attr && Substitute === attr.value
104
120
  @indexes << i
105
121
  end
106
122
  end
@@ -133,7 +149,7 @@ module ActiveRecord
133
149
 
134
150
  klass.find_by_sql(sql, bind_values, preparable: true, &block)
135
151
  rescue ::RangeError
136
- nil
152
+ []
137
153
  end
138
154
 
139
155
  def self.unsupported_value?(value)
@@ -23,7 +23,7 @@ module ActiveRecord
23
23
  # You can set custom coder to encode/decode your serialized attributes to/from different formats.
24
24
  # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
25
25
  #
26
- # NOTE: If you are using structured database data types (eg. PostgreSQL +hstore+/+json+, or MySQL 5.7+
26
+ # NOTE: If you are using structured database data types (e.g. PostgreSQL +hstore+/+json+, or MySQL 5.7+
27
27
  # +json+) there is no need for the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store].
28
28
  # Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate
29
29
  # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
@@ -251,7 +251,7 @@ module ActiveRecord
251
251
  attribute = object.send(store_attribute)
252
252
  unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
253
253
  attribute = IndifferentCoder.as_indifferent_hash(attribute)
254
- object.send :"#{store_attribute}=", attribute
254
+ object.public_send :"#{store_attribute}=", attribute
255
255
  end
256
256
  attribute
257
257
  end
@@ -40,11 +40,11 @@ module ActiveRecord
40
40
  end
41
41
  end
42
42
 
43
- def save(*, **) # :nodoc:
43
+ def save(**) # :nodoc:
44
44
  SuppressorRegistry.suppressed[self.class.name] ? true : super
45
45
  end
46
46
 
47
- def save!(*, **) # :nodoc:
47
+ def save!(**) # :nodoc:
48
48
  SuppressorRegistry.suppressed[self.class.name] ? true : super
49
49
  end
50
50
  end
@@ -2,89 +2,73 @@
2
2
 
3
3
  module ActiveRecord
4
4
  class TableMetadata # :nodoc:
5
- delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true
5
+ delegate :join_primary_key, :join_foreign_key, :join_foreign_type, to: :reflection
6
6
 
7
- def initialize(klass, arel_table, association = nil, types = klass)
7
+ def initialize(klass, arel_table, reflection = nil)
8
8
  @klass = klass
9
- @types = types
10
9
  @arel_table = arel_table
11
- @association = association
12
- end
13
-
14
- def resolve_column_aliases(hash)
15
- new_hash = hash.dup
16
- hash.each_key do |key|
17
- if key.is_a?(Symbol) && new_key = klass.attribute_aliases[key.to_s]
18
- new_hash[new_key] = new_hash.delete(key)
19
- end
20
- end
21
- new_hash
22
- end
23
-
24
- def arel_attribute(column_name)
25
- if klass
26
- klass.arel_attribute(column_name, arel_table)
27
- else
28
- arel_table[column_name]
29
- end
10
+ @reflection = reflection
30
11
  end
31
12
 
32
13
  def type(column_name)
33
- types.type_for_attribute(column_name)
14
+ arel_table.type_for_attribute(column_name)
34
15
  end
35
16
 
36
17
  def has_column?(column_name)
37
- klass && klass.columns_hash.key?(column_name.to_s)
18
+ klass&.columns_hash.key?(column_name)
38
19
  end
39
20
 
40
- def associated_with?(association_name)
41
- klass && klass._reflect_on_association(association_name)
21
+ def associated_with?(table_name)
22
+ klass&._reflect_on_association(table_name) || klass&._reflect_on_association(table_name.singularize)
42
23
  end
43
24
 
44
25
  def associated_table(table_name)
45
- association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.to_s.singularize)
26
+ reflection = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.singularize)
46
27
 
47
- if !association && table_name == arel_table.name
48
- self
49
- elsif association && !association.polymorphic?
50
- association_klass = association.klass
51
- arel_table = association_klass.arel_table.alias(table_name)
52
- TableMetadata.new(association_klass, arel_table, association)
28
+ if !reflection && table_name == arel_table.name
29
+ return self
30
+ end
31
+
32
+ reflection ||= yield table_name if block_given?
33
+
34
+ if reflection && !reflection.polymorphic?
35
+ association_klass = reflection.klass
36
+ arel_table = association_klass.arel_table
37
+ arel_table = arel_table.alias(table_name) if arel_table.name != table_name
38
+ TableMetadata.new(association_klass, arel_table, reflection)
53
39
  else
54
40
  type_caster = TypeCaster::Connection.new(klass, table_name)
55
41
  arel_table = Arel::Table.new(table_name, type_caster: type_caster)
56
- TableMetadata.new(nil, arel_table, association, type_caster)
42
+ TableMetadata.new(nil, arel_table, reflection)
57
43
  end
58
44
  end
59
45
 
60
- def associated_predicate_builder(table_name)
61
- associated_table(table_name).predicate_builder
62
- end
63
-
64
46
  def polymorphic_association?
65
- association && association.polymorphic?
47
+ reflection&.polymorphic?
66
48
  end
67
49
 
68
- def aggregated_with?(aggregation_name)
69
- klass && reflect_on_aggregation(aggregation_name)
50
+ def through_association?
51
+ reflection&.through_reflection?
70
52
  end
71
53
 
72
54
  def reflect_on_aggregation(aggregation_name)
73
- klass.reflect_on_aggregation(aggregation_name)
55
+ klass&.reflect_on_aggregation(aggregation_name)
74
56
  end
57
+ alias :aggregated_with? :reflect_on_aggregation
75
58
 
76
- protected
77
- def predicate_builder
78
- if klass
79
- predicate_builder = klass.predicate_builder.dup
80
- predicate_builder.instance_variable_set(:@table, self)
81
- predicate_builder
82
- else
83
- PredicateBuilder.new(self)
84
- end
59
+ def predicate_builder
60
+ if klass
61
+ predicate_builder = klass.predicate_builder.dup
62
+ predicate_builder.instance_variable_set(:@table, self)
63
+ predicate_builder
64
+ else
65
+ PredicateBuilder.new(self)
85
66
  end
67
+ end
68
+
69
+ attr_reader :arel_table
86
70
 
87
71
  private
88
- attr_reader :klass, :types, :arel_table, :association
72
+ attr_reader :klass, :reflection
89
73
  end
90
74
  end