activerecord 8.0.2 → 8.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +459 -413
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/association_relation.rb +1 -1
  5. data/lib/active_record/associations/association.rb +1 -1
  6. data/lib/active_record/associations/belongs_to_association.rb +9 -1
  7. data/lib/active_record/associations/builder/association.rb +16 -5
  8. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  9. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  10. data/lib/active_record/associations/builder/has_one.rb +1 -1
  11. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  12. data/lib/active_record/associations/collection_association.rb +3 -3
  13. data/lib/active_record/associations/collection_proxy.rb +22 -4
  14. data/lib/active_record/associations/deprecation.rb +88 -0
  15. data/lib/active_record/associations/errors.rb +3 -0
  16. data/lib/active_record/associations/join_dependency.rb +2 -0
  17. data/lib/active_record/associations/preloader/branch.rb +1 -0
  18. data/lib/active_record/associations.rb +159 -21
  19. data/lib/active_record/attribute_methods/query.rb +34 -0
  20. data/lib/active_record/attribute_methods/serialization.rb +17 -4
  21. data/lib/active_record/attributes.rb +38 -24
  22. data/lib/active_record/base.rb +0 -1
  23. data/lib/active_record/coders/json.rb +14 -5
  24. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +2 -4
  25. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +15 -0
  26. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
  27. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +384 -49
  28. data/lib/active_record/connection_adapters/abstract/database_statements.rb +26 -30
  29. data/lib/active_record/connection_adapters/abstract/query_cache.rb +19 -1
  30. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  31. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  32. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +26 -34
  33. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  34. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +89 -23
  35. data/lib/active_record/connection_adapters/abstract/transaction.rb +16 -3
  36. data/lib/active_record/connection_adapters/abstract_adapter.rb +67 -13
  37. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -11
  38. data/lib/active_record/connection_adapters/column.rb +17 -4
  39. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  40. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  41. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  42. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +26 -4
  43. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +27 -22
  44. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -0
  45. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +18 -16
  46. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  47. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  48. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  49. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -1
  50. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +8 -21
  51. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +65 -30
  52. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +74 -38
  53. data/lib/active_record/connection_adapters/postgresql_adapter.rb +12 -7
  54. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  55. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -27
  56. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  57. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
  58. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +56 -32
  59. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  60. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  61. data/lib/active_record/connection_adapters.rb +1 -0
  62. data/lib/active_record/connection_handling.rb +1 -1
  63. data/lib/active_record/core.rb +13 -10
  64. data/lib/active_record/counter_cache.rb +33 -8
  65. data/lib/active_record/database_configurations/database_config.rb +5 -1
  66. data/lib/active_record/database_configurations/hash_config.rb +56 -9
  67. data/lib/active_record/database_configurations/url_config.rb +13 -3
  68. data/lib/active_record/database_configurations.rb +7 -3
  69. data/lib/active_record/delegated_type.rb +2 -2
  70. data/lib/active_record/dynamic_matchers.rb +54 -69
  71. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  72. data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
  73. data/lib/active_record/encryption/encryptor.rb +27 -25
  74. data/lib/active_record/encryption/scheme.rb +1 -1
  75. data/lib/active_record/enum.rb +37 -20
  76. data/lib/active_record/errors.rb +20 -4
  77. data/lib/active_record/explain_registry.rb +0 -1
  78. data/lib/active_record/filter_attribute_handler.rb +73 -0
  79. data/lib/active_record/fixture_set/table_row.rb +19 -2
  80. data/lib/active_record/fixtures.rb +2 -2
  81. data/lib/active_record/gem_version.rb +3 -3
  82. data/lib/active_record/inheritance.rb +1 -1
  83. data/lib/active_record/insert_all.rb +12 -7
  84. data/lib/active_record/locking/optimistic.rb +7 -0
  85. data/lib/active_record/locking/pessimistic.rb +5 -0
  86. data/lib/active_record/log_subscriber.rb +1 -5
  87. data/lib/active_record/middleware/shard_selector.rb +34 -17
  88. data/lib/active_record/migration/command_recorder.rb +14 -1
  89. data/lib/active_record/migration/compatibility.rb +34 -24
  90. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  91. data/lib/active_record/migration.rb +31 -21
  92. data/lib/active_record/model_schema.rb +10 -7
  93. data/lib/active_record/nested_attributes.rb +2 -0
  94. data/lib/active_record/persistence.rb +34 -3
  95. data/lib/active_record/query_cache.rb +22 -15
  96. data/lib/active_record/query_logs.rb +7 -7
  97. data/lib/active_record/querying.rb +4 -4
  98. data/lib/active_record/railtie.rb +34 -5
  99. data/lib/active_record/railties/databases.rake +23 -19
  100. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  101. data/lib/active_record/railties/job_runtime.rb +10 -11
  102. data/lib/active_record/reflection.rb +42 -3
  103. data/lib/active_record/relation/batches.rb +26 -12
  104. data/lib/active_record/relation/calculations.rb +35 -25
  105. data/lib/active_record/relation/delegation.rb +0 -1
  106. data/lib/active_record/relation/finder_methods.rb +41 -24
  107. data/lib/active_record/relation/merger.rb +2 -2
  108. data/lib/active_record/relation/predicate_builder.rb +2 -2
  109. data/lib/active_record/relation/query_attribute.rb +3 -1
  110. data/lib/active_record/relation/query_methods.rb +43 -33
  111. data/lib/active_record/relation/spawn_methods.rb +6 -6
  112. data/lib/active_record/relation/where_clause.rb +7 -10
  113. data/lib/active_record/relation.rb +37 -15
  114. data/lib/active_record/result.rb +44 -21
  115. data/lib/active_record/sanitization.rb +2 -0
  116. data/lib/active_record/schema_dumper.rb +12 -10
  117. data/lib/active_record/scoping.rb +0 -1
  118. data/lib/active_record/secure_token.rb +3 -3
  119. data/lib/active_record/signed_id.rb +46 -18
  120. data/lib/active_record/statement_cache.rb +13 -9
  121. data/lib/active_record/store.rb +44 -19
  122. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  123. data/lib/active_record/tasks/database_tasks.rb +24 -35
  124. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  125. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  126. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  127. data/lib/active_record/test_databases.rb +11 -3
  128. data/lib/active_record/test_fixtures.rb +27 -2
  129. data/lib/active_record/testing/query_assertions.rb +8 -2
  130. data/lib/active_record/timestamp.rb +4 -2
  131. data/lib/active_record/transaction.rb +2 -5
  132. data/lib/active_record/transactions.rb +34 -10
  133. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  134. data/lib/active_record/type/internal/timezone.rb +7 -0
  135. data/lib/active_record/type/json.rb +15 -2
  136. data/lib/active_record/type/serialized.rb +11 -4
  137. data/lib/active_record/type/type_map.rb +1 -1
  138. data/lib/active_record/type_caster/connection.rb +2 -1
  139. data/lib/active_record/validations/associated.rb +1 -1
  140. data/lib/active_record.rb +68 -5
  141. data/lib/arel/alias_predication.rb +2 -0
  142. data/lib/arel/crud.rb +8 -11
  143. data/lib/arel/delete_manager.rb +5 -0
  144. data/lib/arel/nodes/count.rb +2 -2
  145. data/lib/arel/nodes/delete_statement.rb +4 -2
  146. data/lib/arel/nodes/function.rb +4 -10
  147. data/lib/arel/nodes/named_function.rb +2 -2
  148. data/lib/arel/nodes/node.rb +1 -1
  149. data/lib/arel/nodes/update_statement.rb +4 -2
  150. data/lib/arel/nodes.rb +0 -2
  151. data/lib/arel/select_manager.rb +13 -4
  152. data/lib/arel/update_manager.rb +5 -0
  153. data/lib/arel/visitors/dot.rb +2 -3
  154. data/lib/arel/visitors/postgresql.rb +55 -0
  155. data/lib/arel/visitors/sqlite.rb +55 -8
  156. data/lib/arel/visitors/to_sql.rb +5 -21
  157. data/lib/arel.rb +3 -1
  158. metadata +15 -11
  159. data/lib/active_record/normalization.rb +0 -163
@@ -11,41 +11,43 @@ module ActiveRecord
11
11
  # It interacts with a KeyProvider for getting the keys, and delegate to
12
12
  # ActiveRecord::Encryption::Cipher the actual encryption algorithm.
13
13
  class Encryptor
14
- # The compressor to use for compressing the payload
14
+ # The compressor to use for compressing the payload.
15
15
  attr_reader :compressor
16
16
 
17
- # === Options
17
+ # ==== Options
18
18
  #
19
- # * <tt>:compress</tt> - Boolean indicating whether records should be compressed before encryption.
20
- # Defaults to +true+.
21
- # * <tt>:compressor</tt> - The compressor to use.
22
- # 1. If compressor is provided, it will be used.
23
- # 2. If not, it will use ActiveRecord::Encryption.config.compressor which default value is +Zlib+.
24
- # If you want to use a custom compressor, it must respond to +deflate+ and +inflate+.
19
+ # [+:compress+]
20
+ # Boolean indicating whether records should be compressed before
21
+ # encryption. Defaults to +true+.
22
+ #
23
+ # [+:compressor+]
24
+ # The compressor to use. It must respond to +deflate+ and +inflate+.
25
+ # If not provided, will default to +ActiveRecord::Encryption.config.compressor+,
26
+ # which itself defaults to +Zlib+.
25
27
  def initialize(compress: true, compressor: nil)
26
28
  @compress = compress
27
29
  @compressor = compressor || ActiveRecord::Encryption.config.compressor
28
30
  end
29
31
 
30
- # Encrypts +clean_text+ and returns the encrypted result
32
+ # Encrypts +clean_text+ and returns the encrypted result.
31
33
  #
32
34
  # Internally, it will:
33
35
  #
34
- # 1. Create a new ActiveRecord::Encryption::Message
35
- # 2. Compress and encrypt +clean_text+ as the message payload
36
- # 3. Serialize it with +ActiveRecord::Encryption.message_serializer+ (+ActiveRecord::Encryption::SafeMarshal+
37
- # by default)
38
- # 4. Encode the result with Base 64
36
+ # 1. Create a new ActiveRecord::Encryption::Message.
37
+ # 2. Compress and encrypt +clean_text+ as the message payload.
38
+ # 3. Serialize it with +ActiveRecord::Encryption.message_serializer+
39
+ # (+ActiveRecord::Encryption::SafeMarshal+ by default).
40
+ # 4. Encode the result with Base64.
39
41
  #
40
- # === Options
42
+ # ==== Options
41
43
  #
42
- # [:key_provider]
44
+ # [+:key_provider+]
43
45
  # Key provider to use for the encryption operation. It will default to
44
46
  # +ActiveRecord::Encryption.key_provider+ when not provided.
45
47
  #
46
- # [:cipher_options]
48
+ # [+:cipher_options+]
47
49
  # Cipher-specific options that will be passed to the Cipher configured in
48
- # +ActiveRecord::Encryption.cipher+
50
+ # +ActiveRecord::Encryption.cipher+.
49
51
  def encrypt(clear_text, key_provider: default_key_provider, cipher_options: {})
50
52
  clear_text = force_encoding_if_needed(clear_text) if cipher_options[:deterministic]
51
53
 
@@ -53,17 +55,17 @@ module ActiveRecord
53
55
  serialize_message build_encrypted_message(clear_text, key_provider: key_provider, cipher_options: cipher_options)
54
56
  end
55
57
 
56
- # Decrypts an +encrypted_text+ and returns the result as clean text
58
+ # Decrypts an +encrypted_text+ and returns the result as clean text.
57
59
  #
58
- # === Options
60
+ # ==== Options
59
61
  #
60
- # [:key_provider]
62
+ # [+:key_provider+]
61
63
  # Key provider to use for the encryption operation. It will default to
62
- # +ActiveRecord::Encryption.key_provider+ when not provided
64
+ # +ActiveRecord::Encryption.key_provider+ when not provided.
63
65
  #
64
- # [:cipher_options]
66
+ # [+:cipher_options+]
65
67
  # Cipher-specific options that will be passed to the Cipher configured in
66
- # +ActiveRecord::Encryption.cipher+
68
+ # +ActiveRecord::Encryption.cipher+.
67
69
  def decrypt(encrypted_text, key_provider: default_key_provider, cipher_options: {})
68
70
  message = deserialize_message(encrypted_text)
69
71
  keys = key_provider.decryption_keys(message)
@@ -73,7 +75,7 @@ module ActiveRecord
73
75
  raise Errors::Decryption
74
76
  end
75
77
 
76
- # Returns whether the text is encrypted or not
78
+ # Returns whether the text is encrypted or not.
77
79
  def encrypted?(text)
78
80
  deserialize_message(text)
79
81
  true
@@ -59,7 +59,7 @@ module ActiveRecord
59
59
  end
60
60
 
61
61
  def merge(other_scheme)
62
- self.class.new(**to_h.merge(other_scheme.to_h))
62
+ self.class.new(**to_h, **other_scheme.to_h)
63
63
  end
64
64
 
65
65
  def to_h
@@ -119,7 +119,18 @@ module ActiveRecord
119
119
  # enum :status, [ :active, :archived ], instance_methods: false
120
120
  # end
121
121
  #
122
- # If you want the enum value to be validated before saving, use the option +:validate+:
122
+ # By default, an +ArgumentError+ will be raised when assigning an invalid value:
123
+ #
124
+ # class Conversation < ActiveRecord::Base
125
+ # enum :status, [ :active, :archived ]
126
+ # end
127
+ #
128
+ # conversation = Conversation.new
129
+ #
130
+ # conversation.status = :unknown # 'unknown' is not a valid status (ArgumentError)
131
+ #
132
+ # If, instead, you want the enum value to be validated before saving, use the
133
+ # +:validate+ option:
123
134
  #
124
135
  # class Conversation < ActiveRecord::Base
125
136
  # enum :status, [ :active, :archived ], validate: true
@@ -136,7 +147,7 @@ module ActiveRecord
136
147
  # conversation.status = :active
137
148
  # conversation.valid? # => true
138
149
  #
139
- # It is also possible to pass additional validation options:
150
+ # You may also pass additional validation options:
140
151
  #
141
152
  # class Conversation < ActiveRecord::Base
142
153
  # enum :status, [ :active, :archived ], validate: { allow_nil: true }
@@ -152,16 +163,6 @@ module ActiveRecord
152
163
  #
153
164
  # conversation.status = :active
154
165
  # conversation.valid? # => true
155
- #
156
- # Otherwise +ArgumentError+ will raise:
157
- #
158
- # class Conversation < ActiveRecord::Base
159
- # enum :status, [ :active, :archived ]
160
- # end
161
- #
162
- # conversation = Conversation.new
163
- #
164
- # conversation.status = :unknown # 'unknown' is not a valid status (ArgumentError)
165
166
  module Enum
166
167
  def self.extended(base) # :nodoc:
167
168
  base.class_attribute(:defined_enums, instance_writer: false, default: {})
@@ -220,7 +221,7 @@ module ActiveRecord
220
221
 
221
222
  private
222
223
  def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods: true, validate: false, **options)
223
- assert_valid_enum_definition_values(values)
224
+ values = assert_valid_enum_definition_values(values)
224
225
  assert_valid_enum_options(options)
225
226
 
226
227
  # statuses = { }
@@ -341,6 +342,20 @@ module ActiveRecord
341
342
  if values.keys.any?(&:blank?)
342
343
  raise ArgumentError, "Enum values #{values} must not contain a blank name."
343
344
  end
345
+
346
+ values = values.transform_values do |value|
347
+ value.is_a?(Symbol) ? value.name : value
348
+ end
349
+
350
+ values.each_value do |value|
351
+ case value
352
+ when String, Integer, true, false, nil
353
+ # noop
354
+ else
355
+ raise ArgumentError, "Enum values #{values} must be only booleans, integers, symbols or strings, got: #{value.class}"
356
+ end
357
+ end
358
+
344
359
  when Array
345
360
  if values.empty?
346
361
  raise ArgumentError, "Enum values #{values} must not be empty."
@@ -356,6 +371,8 @@ module ActiveRecord
356
371
  else
357
372
  raise ArgumentError, "Enum values #{values} must be either a non-empty hash or an array."
358
373
  end
374
+
375
+ values
359
376
  end
360
377
 
361
378
  def assert_valid_enum_options(options)
@@ -367,25 +384,25 @@ module ActiveRecord
367
384
 
368
385
  ENUM_CONFLICT_MESSAGE = \
369
386
  "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
370
- "this will generate a %{type} method \"%{method}\", which is already defined " \
387
+ "this will generate %{type} method \"%{method}\", which is already defined " \
371
388
  "by %{source}."
372
389
  private_constant :ENUM_CONFLICT_MESSAGE
373
390
 
374
391
  def detect_enum_conflict!(enum_name, method_name, klass_method = false)
375
392
  if klass_method && dangerous_class_method?(method_name)
376
- raise_conflict_error(enum_name, method_name, type: "class")
393
+ raise_conflict_error(enum_name, method_name, "a class")
377
394
  elsif klass_method && method_defined_within?(method_name, Relation)
378
- raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name)
395
+ raise_conflict_error(enum_name, method_name, "a class", source: Relation.name)
379
396
  elsif klass_method && method_name.to_sym == :id
380
- raise_conflict_error(enum_name, method_name)
397
+ raise_conflict_error(enum_name, method_name, "an instance")
381
398
  elsif !klass_method && dangerous_attribute_method?(method_name)
382
- raise_conflict_error(enum_name, method_name)
399
+ raise_conflict_error(enum_name, method_name, "an instance")
383
400
  elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
384
- raise_conflict_error(enum_name, method_name, source: "another enum")
401
+ raise_conflict_error(enum_name, method_name, "an instance", source: "another enum")
385
402
  end
386
403
  end
387
404
 
388
- def raise_conflict_error(enum_name, method_name, type: "instance", source: "Active Record")
405
+ def raise_conflict_error(enum_name, method_name, type, source: "Active Record")
389
406
  raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
390
407
  enum: enum_name,
391
408
  klass: name,
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/deprecation"
4
3
 
5
4
  module ActiveRecord
6
5
  include ActiveSupport::Deprecation::DeprecatedConstantAccessor
@@ -293,6 +292,14 @@ module ActiveRecord
293
292
  class NotNullViolation < StatementInvalid
294
293
  end
295
294
 
295
+ # Raised when a record cannot be inserted or updated because it would violate a check constraint.
296
+ class CheckViolation < StatementInvalid
297
+ end
298
+
299
+ # Raised when a record cannot be inserted or updated because it would violate an exclusion constraint.
300
+ class ExclusionViolation < StatementInvalid
301
+ end
302
+
296
303
  # Raised when a record cannot be inserted or updated because a value too long for a column type.
297
304
  class ValueTooLong < StatementInvalid
298
305
  end
@@ -339,15 +346,15 @@ module ActiveRecord
339
346
  class << self
340
347
  def db_error(db_name)
341
348
  NoDatabaseError.new(<<~MSG)
342
- We could not find your database: #{db_name}. Available database configurations can be found in config/database.yml.
349
+ Database not found: #{db_name}. Available database configurations can be found in config/database.yml.
343
350
 
344
351
  To resolve this error:
345
352
 
346
- - Did you not create the database, or did you delete it? To create the database, run:
353
+ - Create the database by running:
347
354
 
348
355
  bin/rails db:create
349
356
 
350
- - Has the database name changed? Verify that config/database.yml contains the correct database name.
357
+ - Verify that config/database.yml contains the correct database name.
351
358
  MSG
352
359
  end
353
360
  end
@@ -490,6 +497,7 @@ module ActiveRecord
490
497
  # end
491
498
  #
492
499
  # relation = Task.all
500
+ # relation.load
493
501
  # relation.loaded? # => true
494
502
  #
495
503
  # # Methods which try to mutate a loaded relation fail.
@@ -552,6 +560,11 @@ module ActiveRecord
552
560
  class Deadlocked < TransactionRollbackError
553
561
  end
554
562
 
563
+ # MissingRequiredOrderError is raised when a relation requires ordering but
564
+ # lacks any +order+ values in scope or any model order columns to use.
565
+ class MissingRequiredOrderError < ActiveRecordError
566
+ end
567
+
555
568
  # IrreversibleOrderError is raised when a relation's order is too complex for
556
569
  # +reverse_order+ to automatically reverse.
557
570
  class IrreversibleOrderError < ActiveRecordError
@@ -609,6 +622,9 @@ module ActiveRecord
609
622
  # the database version cannot be determined.
610
623
  class DatabaseVersionError < ActiveRecordError
611
624
  end
625
+
626
+ class DeprecatedAssociationError < ActiveRecordError
627
+ end
612
628
  end
613
629
 
614
630
  require "active_record/associations/errors"
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/module/delegation"
4
3
 
5
4
  module ActiveRecord
6
5
  # This is a thread locals registry for EXPLAIN. For example
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ class FilterAttributeHandler # :nodoc:
5
+ class << self
6
+ def on_sensitive_attribute_declared(&block)
7
+ @sensitive_attribute_declaration_listeners ||= Concurrent::Array.new
8
+ @sensitive_attribute_declaration_listeners << block
9
+ end
10
+
11
+ def sensitive_attribute_was_declared(klass, list)
12
+ @sensitive_attribute_declaration_listeners&.each do |block|
13
+ block.call(klass, list)
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(app)
19
+ @app = app
20
+ @attributes_by_class = Concurrent::Map.new
21
+ @collecting = true
22
+ end
23
+
24
+ def enable
25
+ install_collecting_hook
26
+
27
+ apply_collected_attributes
28
+ @collecting = false
29
+ end
30
+
31
+ private
32
+ attr_reader :app
33
+
34
+ def install_collecting_hook
35
+ self.class.on_sensitive_attribute_declared do |klass, list|
36
+ attribute_was_declared(klass, list)
37
+ end
38
+ end
39
+
40
+ def attribute_was_declared(klass, list)
41
+ if collecting?
42
+ collect_for_later(klass, list)
43
+ else
44
+ apply_filter(klass, list)
45
+ end
46
+ end
47
+
48
+ def apply_collected_attributes
49
+ @attributes_by_class.each do |klass, list|
50
+ apply_filter(klass, list)
51
+ end
52
+ end
53
+
54
+ def collecting?
55
+ @collecting
56
+ end
57
+
58
+ def collect_for_later(klass, list)
59
+ @attributes_by_class[klass] ||= Concurrent::Array.new
60
+ @attributes_by_class[klass] += list
61
+ end
62
+
63
+ def apply_filter(klass, list)
64
+ list.each do |attribute|
65
+ next if klass.abstract_class? || klass == Base
66
+
67
+ klass_name = klass.name ? klass.model_name.element : nil
68
+ filter = [klass_name, attribute.to_s].compact.join(".")
69
+ app.config.filter_parameters << filter unless app.config.filter_parameters.include?(filter)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -193,8 +193,25 @@ module ActiveRecord
193
193
 
194
194
  targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
195
195
  joins = targets.map do |target|
196
- join = { lhs_key => @row[model_metadata.primary_key_name],
197
- rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
196
+ join = {}
197
+
198
+ if rhs_key.is_a?(Array)
199
+ composite_key = ActiveRecord::FixtureSet.composite_identify(target, rhs_key)
200
+ composite_key.each do |column, value|
201
+ join[column] = value
202
+ end
203
+ else
204
+ join[rhs_key] = ActiveRecord::FixtureSet.identify(target, column_type)
205
+ end
206
+
207
+ if lhs_key.is_a?(Array)
208
+ lhs_key.zip(model_metadata.primary_key_name).each do |fkey, pkey|
209
+ join[fkey] = @row[pkey]
210
+ end
211
+ else
212
+ join[lhs_key] = @row[model_metadata.primary_key_name]
213
+ end
214
+
198
215
  association.timestamp_column_names.each do |col|
199
216
  join[col] = @now
200
217
  end
@@ -242,10 +242,10 @@ module ActiveRecord
242
242
  # and one for the humans. Why don't we generate the primary key instead?
243
243
  # Hashing each fixture's label yields a consistent ID:
244
244
  #
245
- # george: # generated id: 503576764
245
+ # george: # generated id: 380982691
246
246
  # name: George the Monkey
247
247
  #
248
- # reginald: # generated id: 324201669
248
+ # reginald: # generated id: 41001176
249
249
  # name: Reginald the Pirate
250
250
  #
251
251
  # Active Record looks at the fixture's model class, discovers the correct
@@ -8,9 +8,9 @@ module ActiveRecord
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 8
11
- MINOR = 0
12
- TINY = 2
13
- PRE = nil
11
+ MINOR = 1
12
+ TINY = 0
13
+ PRE = "beta1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -97,7 +97,7 @@ module ActiveRecord
97
97
  # Returns the first class in the inheritance hierarchy that descends from either an
98
98
  # abstract class or from <tt>ActiveRecord::Base</tt>.
99
99
  #
100
- # Consider the following behaviour:
100
+ # Consider the following behavior:
101
101
  #
102
102
  # class ApplicationRecord < ActiveRecord::Base
103
103
  # self.abstract_class = true
@@ -11,7 +11,7 @@ module ActiveRecord
11
11
  def execute(relation, ...)
12
12
  relation.model.with_connection do |c|
13
13
  new(relation, c, ...).execute
14
- end
14
+ end.tap { relation.reset }
15
15
  end
16
16
  end
17
17
 
@@ -225,7 +225,7 @@ module ActiveRecord
225
225
  class Builder # :nodoc:
226
226
  attr_reader :model
227
227
 
228
- delegate :skip_duplicates?, :update_duplicates?, :keys, :keys_including_timestamps, :record_timestamps?, to: :insert_all
228
+ delegate :skip_duplicates?, :update_duplicates?, :keys, :keys_including_timestamps, :record_timestamps?, :primary_keys, to: :insert_all
229
229
 
230
230
  def initialize(insert_all)
231
231
  @insert_all, @model, @connection = insert_all, insert_all.model, insert_all.connection
@@ -236,11 +236,16 @@ module ActiveRecord
236
236
  end
237
237
 
238
238
  def values_list
239
- types = extract_types_from_columns_on(model.table_name, keys: keys_including_timestamps)
239
+ types = extract_types_for(keys_including_timestamps)
240
240
 
241
241
  values_list = insert_all.map_key_with_value do |key, value|
242
- next value if Arel::Nodes::SqlLiteral === value
243
- ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
242
+ if Arel::Nodes::SqlLiteral === value
243
+ value
244
+ elsif primary_keys.include?(key) && value.nil?
245
+ connection.default_insert_value(model.columns_hash[key])
246
+ else
247
+ ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
248
+ end
244
249
  end
245
250
 
246
251
  connection.visitor.compile(Arel::Nodes::ValuesList.new(values_list))
@@ -303,8 +308,8 @@ module ActiveRecord
303
308
  format_columns(insert_all.keys_including_timestamps)
304
309
  end
305
310
 
306
- def extract_types_from_columns_on(table_name, keys:)
307
- columns = @model.schema_cache.columns_hash(table_name)
311
+ def extract_types_for(keys)
312
+ columns = @model.columns_hash
308
313
 
309
314
  unknown_column = (keys - columns.keys).first
310
315
  raise UnknownAttributeError.new(model.new, unknown_column) if unknown_column
@@ -101,6 +101,13 @@ module ActiveRecord
101
101
  attribute_names = attribute_names.dup if attribute_names.frozen?
102
102
  attribute_names << locking_column
103
103
 
104
+ if self[locking_column].nil?
105
+ raise(<<-MSG.squish)
106
+ For optimistic locking, locking_column ('#{locking_column}') can't be nil.
107
+ Are you missing a default value or validation on '#{locking_column}'?
108
+ MSG
109
+ end
110
+
104
111
  self[locking_column] += 1
105
112
 
106
113
  affected_rows = self.class._update_record(
@@ -67,6 +67,10 @@ module ActiveRecord
67
67
  # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
68
68
  # the locked record.
69
69
  def lock!(lock = true)
70
+ if self.class.current_preventing_writes
71
+ raise ActiveRecord::ReadOnlyError, "Lock query attempted while in readonly mode"
72
+ end
73
+
70
74
  if persisted?
71
75
  if has_changes_to_save?
72
76
  raise(<<-MSG.squish)
@@ -79,6 +83,7 @@ module ActiveRecord
79
83
 
80
84
  reload(lock: lock)
81
85
  end
86
+
82
87
  self
83
88
  end
84
89
 
@@ -127,11 +127,7 @@ module ActiveRecord
127
127
  end
128
128
 
129
129
  def query_source_location
130
- Thread.each_caller_location do |location|
131
- frame = backtrace_cleaner.clean_frame(location)
132
- return frame if frame
133
- end
134
- nil
130
+ backtrace_cleaner.first_clean_frame
135
131
  end
136
132
 
137
133
  def filter(name, value)
@@ -9,25 +9,40 @@ module ActiveRecord
9
9
  # shard to switch to and allows for applications to write custom strategies
10
10
  # for swapping if needed.
11
11
  #
12
- # The ShardSelector takes a set of options (currently only +lock+ is supported)
13
- # that can be used by the middleware to alter behavior. +lock+ is
14
- # true by default and will prohibit the request from switching shards once
15
- # inside the block. If +lock+ is false, then shard swapping will be allowed.
16
- # For tenant based sharding, +lock+ should always be true to prevent application
17
- # code from mistakenly switching between tenants.
12
+ # == Setup
18
13
  #
19
- # Options can be set in the config:
14
+ # Applications must provide a resolver that will provide application-specific logic for
15
+ # selecting the appropriate shard. Setting +config.active_record.shard_resolver+ will cause
16
+ # Rails to add ShardSelector to the default middleware stack.
20
17
  #
21
- # config.active_record.shard_selector = { lock: true }
18
+ # The resolver, along with any configuration options, can be set in the application
19
+ # configuration using an initializer like so:
22
20
  #
23
- # Applications must also provide the code for the resolver as it depends on application
24
- # specific models. An example resolver would look like this:
21
+ # Rails.application.configure do
22
+ # config.active_record.shard_selector = { lock: false, class_name: "AnimalsRecord" }
23
+ # config.active_record.shard_resolver = ->(request) {
24
+ # subdomain = request.subdomain
25
+ # tenant = Tenant.find_by_subdomain!(subdomain)
26
+ # tenant.shard
27
+ # }
28
+ # end
29
+ #
30
+ # == Configuration
31
+ #
32
+ # The behavior of ShardSelector can be altered through some configuration options.
33
+ #
34
+ # [+lock:+]
35
+ # +lock+ is true by default and will prohibit the request from switching shards once inside
36
+ # the block. If +lock+ is false, then shard switching will be allowed. For tenant based
37
+ # sharding, +lock+ should always be true to prevent application code from mistakenly switching
38
+ # between tenants.
39
+ #
40
+ # [+class_name:+]
41
+ # +class_name+ is the name of the abstract connection class to switch. By
42
+ # default, the ShardSelector will use ActiveRecord::Base, but if the
43
+ # application has multiple databases, then this option should be set to
44
+ # the name of the sharded database's abstract connection class.
25
45
  #
26
- # config.active_record.shard_resolver = ->(request) {
27
- # subdomain = request.subdomain
28
- # tenant = Tenant.find_by_subdomain!(subdomain)
29
- # tenant.shard
30
- # }
31
46
  class ShardSelector
32
47
  def initialize(app, resolver, options = {})
33
48
  @app = app
@@ -53,8 +68,10 @@ module ActiveRecord
53
68
  end
54
69
 
55
70
  def set_shard(shard, &block)
56
- ActiveRecord::Base.connected_to(shard: shard.to_sym) do
57
- ActiveRecord::Base.prohibit_shard_swapping(options.fetch(:lock, true), &block)
71
+ klass = options[:class_name]&.constantize || ActiveRecord::Base
72
+
73
+ klass.connected_to(shard: shard.to_sym) do
74
+ klass.prohibit_shard_swapping(options.fetch(:lock, true), &block)
58
75
  end
59
76
  end
60
77
  end
@@ -44,6 +44,8 @@ module ActiveRecord
44
44
  # * rename_enum_value (must supply a +:from+ and +:to+ option)
45
45
  # * rename_index
46
46
  # * rename_table
47
+ # * enable_index
48
+ # * disable_index
47
49
  class CommandRecorder
48
50
  ReversibleAndIrreversibleMethods = [
49
51
  :create_table, :create_join_table, :rename_table, :add_column, :remove_column,
@@ -58,7 +60,8 @@ module ActiveRecord
58
60
  :add_unique_constraint, :remove_unique_constraint,
59
61
  :create_enum, :drop_enum, :rename_enum, :add_enum_value, :rename_enum_value,
60
62
  :create_schema, :drop_schema,
61
- :create_virtual_table, :drop_virtual_table
63
+ :create_virtual_table, :drop_virtual_table,
64
+ :enable_index, :disable_index
62
65
  ]
63
66
  include JoinTable
64
67
 
@@ -183,6 +186,16 @@ module ActiveRecord
183
186
 
184
187
  include StraightReversions
185
188
 
189
+ def invert_enable_index(args)
190
+ table_name, index_name = args
191
+ [:disable_index, [table_name, index_name]]
192
+ end
193
+
194
+ def invert_disable_index(args)
195
+ table_name, index_name = args
196
+ [:enable_index, [table_name, index_name]]
197
+ end
198
+
186
199
  def invert_transaction(args, &block)
187
200
  sub_recorder = CommandRecorder.new(delegate)
188
201
  sub_recorder.revert(&block)