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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +459 -413
- data/README.rdoc +2 -2
- data/lib/active_record/association_relation.rb +1 -1
- data/lib/active_record/associations/association.rb +1 -1
- data/lib/active_record/associations/belongs_to_association.rb +9 -1
- data/lib/active_record/associations/builder/association.rb +16 -5
- data/lib/active_record/associations/builder/belongs_to.rb +17 -4
- data/lib/active_record/associations/builder/collection_association.rb +7 -3
- data/lib/active_record/associations/builder/has_one.rb +1 -1
- data/lib/active_record/associations/builder/singular_association.rb +33 -5
- data/lib/active_record/associations/collection_association.rb +3 -3
- data/lib/active_record/associations/collection_proxy.rb +22 -4
- data/lib/active_record/associations/deprecation.rb +88 -0
- data/lib/active_record/associations/errors.rb +3 -0
- data/lib/active_record/associations/join_dependency.rb +2 -0
- data/lib/active_record/associations/preloader/branch.rb +1 -0
- data/lib/active_record/associations.rb +159 -21
- data/lib/active_record/attribute_methods/query.rb +34 -0
- data/lib/active_record/attribute_methods/serialization.rb +17 -4
- data/lib/active_record/attributes.rb +38 -24
- data/lib/active_record/base.rb +0 -1
- data/lib/active_record/coders/json.rb +14 -5
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +2 -4
- data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +15 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +384 -49
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +26 -30
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +19 -1
- data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +26 -34
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +89 -23
- data/lib/active_record/connection_adapters/abstract/transaction.rb +16 -3
- data/lib/active_record/connection_adapters/abstract_adapter.rb +67 -13
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -11
- data/lib/active_record/connection_adapters/column.rb +17 -4
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
- data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +26 -4
- data/lib/active_record/connection_adapters/mysql2/database_statements.rb +27 -22
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +18 -16
- data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
- data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +8 -21
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +65 -30
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +74 -38
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +12 -7
- data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -27
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +56 -32
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
- data/lib/active_record/connection_adapters.rb +1 -0
- data/lib/active_record/connection_handling.rb +1 -1
- data/lib/active_record/core.rb +13 -10
- data/lib/active_record/counter_cache.rb +33 -8
- data/lib/active_record/database_configurations/database_config.rb +5 -1
- data/lib/active_record/database_configurations/hash_config.rb +56 -9
- data/lib/active_record/database_configurations/url_config.rb +13 -3
- data/lib/active_record/database_configurations.rb +7 -3
- data/lib/active_record/delegated_type.rb +2 -2
- data/lib/active_record/dynamic_matchers.rb +54 -69
- data/lib/active_record/encryption/encryptable_record.rb +5 -5
- data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
- data/lib/active_record/encryption/encryptor.rb +27 -25
- data/lib/active_record/encryption/scheme.rb +1 -1
- data/lib/active_record/enum.rb +37 -20
- data/lib/active_record/errors.rb +20 -4
- data/lib/active_record/explain_registry.rb +0 -1
- data/lib/active_record/filter_attribute_handler.rb +73 -0
- data/lib/active_record/fixture_set/table_row.rb +19 -2
- data/lib/active_record/fixtures.rb +2 -2
- data/lib/active_record/gem_version.rb +3 -3
- data/lib/active_record/inheritance.rb +1 -1
- data/lib/active_record/insert_all.rb +12 -7
- data/lib/active_record/locking/optimistic.rb +7 -0
- data/lib/active_record/locking/pessimistic.rb +5 -0
- data/lib/active_record/log_subscriber.rb +1 -5
- data/lib/active_record/middleware/shard_selector.rb +34 -17
- data/lib/active_record/migration/command_recorder.rb +14 -1
- data/lib/active_record/migration/compatibility.rb +34 -24
- data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
- data/lib/active_record/migration.rb +31 -21
- data/lib/active_record/model_schema.rb +10 -7
- data/lib/active_record/nested_attributes.rb +2 -0
- data/lib/active_record/persistence.rb +34 -3
- data/lib/active_record/query_cache.rb +22 -15
- data/lib/active_record/query_logs.rb +7 -7
- data/lib/active_record/querying.rb +4 -4
- data/lib/active_record/railtie.rb +34 -5
- data/lib/active_record/railties/databases.rake +23 -19
- data/lib/active_record/railties/job_checkpoints.rb +15 -0
- data/lib/active_record/railties/job_runtime.rb +10 -11
- data/lib/active_record/reflection.rb +42 -3
- data/lib/active_record/relation/batches.rb +26 -12
- data/lib/active_record/relation/calculations.rb +35 -25
- data/lib/active_record/relation/delegation.rb +0 -1
- data/lib/active_record/relation/finder_methods.rb +41 -24
- data/lib/active_record/relation/merger.rb +2 -2
- data/lib/active_record/relation/predicate_builder.rb +2 -2
- data/lib/active_record/relation/query_attribute.rb +3 -1
- data/lib/active_record/relation/query_methods.rb +43 -33
- data/lib/active_record/relation/spawn_methods.rb +6 -6
- data/lib/active_record/relation/where_clause.rb +7 -10
- data/lib/active_record/relation.rb +37 -15
- data/lib/active_record/result.rb +44 -21
- data/lib/active_record/sanitization.rb +2 -0
- data/lib/active_record/schema_dumper.rb +12 -10
- data/lib/active_record/scoping.rb +0 -1
- data/lib/active_record/secure_token.rb +3 -3
- data/lib/active_record/signed_id.rb +46 -18
- data/lib/active_record/statement_cache.rb +13 -9
- data/lib/active_record/store.rb +44 -19
- data/lib/active_record/tasks/abstract_tasks.rb +76 -0
- data/lib/active_record/tasks/database_tasks.rb +24 -35
- data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
- data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
- data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
- data/lib/active_record/test_databases.rb +11 -3
- data/lib/active_record/test_fixtures.rb +27 -2
- data/lib/active_record/testing/query_assertions.rb +8 -2
- data/lib/active_record/timestamp.rb +4 -2
- data/lib/active_record/transaction.rb +2 -5
- data/lib/active_record/transactions.rb +34 -10
- data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
- data/lib/active_record/type/internal/timezone.rb +7 -0
- data/lib/active_record/type/json.rb +15 -2
- data/lib/active_record/type/serialized.rb +11 -4
- data/lib/active_record/type/type_map.rb +1 -1
- data/lib/active_record/type_caster/connection.rb +2 -1
- data/lib/active_record/validations/associated.rb +1 -1
- data/lib/active_record.rb +68 -5
- data/lib/arel/alias_predication.rb +2 -0
- data/lib/arel/crud.rb +8 -11
- data/lib/arel/delete_manager.rb +5 -0
- data/lib/arel/nodes/count.rb +2 -2
- data/lib/arel/nodes/delete_statement.rb +4 -2
- data/lib/arel/nodes/function.rb +4 -10
- data/lib/arel/nodes/named_function.rb +2 -2
- data/lib/arel/nodes/node.rb +1 -1
- data/lib/arel/nodes/update_statement.rb +4 -2
- data/lib/arel/nodes.rb +0 -2
- data/lib/arel/select_manager.rb +13 -4
- data/lib/arel/update_manager.rb +5 -0
- data/lib/arel/visitors/dot.rb +2 -3
- data/lib/arel/visitors/postgresql.rb +55 -0
- data/lib/arel/visitors/sqlite.rb +55 -8
- data/lib/arel/visitors/to_sql.rb +5 -21
- data/lib/arel.rb +3 -1
- metadata +15 -11
- 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
|
-
#
|
17
|
+
# ==== Options
|
18
18
|
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
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+
|
37
|
-
# by default)
|
38
|
-
# 4. Encode the result with
|
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
|
-
#
|
42
|
+
# ==== Options
|
41
43
|
#
|
42
|
-
# [
|
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
|
-
# [
|
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
|
-
#
|
60
|
+
# ==== Options
|
59
61
|
#
|
60
|
-
# [
|
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
|
-
# [
|
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
|
data/lib/active_record/enum.rb
CHANGED
@@ -119,7 +119,18 @@ module ActiveRecord
|
|
119
119
|
# enum :status, [ :active, :archived ], instance_methods: false
|
120
120
|
# end
|
121
121
|
#
|
122
|
-
#
|
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
|
-
#
|
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
|
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,
|
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,
|
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
|
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,
|
data/lib/active_record/errors.rb
CHANGED
@@ -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
|
-
|
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
|
-
-
|
353
|
+
- Create the database by running:
|
347
354
|
|
348
355
|
bin/rails db:create
|
349
356
|
|
350
|
-
-
|
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"
|
@@ -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 = {
|
197
|
-
|
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:
|
245
|
+
# george: # generated id: 380982691
|
246
246
|
# name: George the Monkey
|
247
247
|
#
|
248
|
-
# reginald: # generated id:
|
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
|
@@ -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
|
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 =
|
239
|
+
types = extract_types_for(keys_including_timestamps)
|
240
240
|
|
241
241
|
values_list = insert_all.map_key_with_value do |key, value|
|
242
|
-
|
243
|
-
|
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
|
307
|
-
columns = @model.
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
18
|
+
# The resolver, along with any configuration options, can be set in the application
|
19
|
+
# configuration using an initializer like so:
|
22
20
|
#
|
23
|
-
#
|
24
|
-
#
|
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
|
57
|
-
|
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)
|