activerecord 7.0.0.alpha1 → 7.0.0.rc3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +516 -10
- data/lib/active_record/associations/association.rb +2 -8
- data/lib/active_record/associations/builder/collection_association.rb +9 -2
- data/lib/active_record/associations/collection_association.rb +10 -2
- data/lib/active_record/associations/preloader/association.rb +68 -48
- data/lib/active_record/associations/preloader/batch.rb +3 -6
- data/lib/active_record/associations/preloader/through_association.rb +19 -9
- data/lib/active_record/associations/preloader.rb +14 -24
- data/lib/active_record/associations/through_association.rb +2 -2
- data/lib/active_record/associations.rb +16 -3
- data/lib/active_record/asynchronous_queries_tracker.rb +3 -0
- data/lib/active_record/attribute_methods/dirty.rb +9 -1
- data/lib/active_record/attribute_methods.rb +7 -5
- data/lib/active_record/autosave_association.rb +3 -3
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +6 -26
- data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +2 -2
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +18 -5
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +0 -17
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/quoting.rb +33 -70
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +4 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +9 -2
- data/lib/active_record/connection_adapters/abstract/transaction.rb +12 -19
- data/lib/active_record/connection_adapters/abstract_adapter.rb +37 -8
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/column.rb +4 -0
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +2 -2
- data/lib/active_record/connection_adapters/mysql/quoting.rb +23 -24
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +1 -1
- data/lib/active_record/connection_adapters/pool_config.rb +7 -5
- data/lib/active_record/connection_adapters/postgresql/column.rb +17 -1
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +44 -44
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +21 -1
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +18 -1
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +25 -0
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +15 -4
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +48 -5
- data/lib/active_record/connection_adapters/schema_cache.rb +3 -1
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +2 -2
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +15 -16
- data/lib/active_record/connection_handling.rb +31 -19
- data/lib/active_record/core.rb +13 -24
- data/lib/active_record/database_configurations/connection_url_resolver.rb +2 -1
- data/lib/active_record/database_configurations/database_config.rb +0 -9
- data/lib/active_record/database_configurations/hash_config.rb +40 -8
- data/lib/active_record/database_configurations.rb +2 -27
- data/lib/active_record/delegated_type.rb +19 -0
- data/lib/active_record/encryption/encrypted_attribute_type.rb +1 -1
- data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +1 -2
- data/lib/active_record/encryption/message_serializer.rb +11 -1
- data/lib/active_record/encryption/scheme.rb +1 -1
- data/lib/active_record/enum.rb +8 -1
- data/lib/active_record/errors.rb +1 -1
- data/lib/active_record/explain_registry.rb +11 -6
- data/lib/active_record/fixture_set/table_row.rb +1 -1
- data/lib/active_record/fixtures.rb +1 -9
- data/lib/active_record/future_result.rb +2 -2
- data/lib/active_record/gem_version.rb +1 -1
- data/lib/active_record/insert_all.rb +52 -15
- data/lib/active_record/integration.rb +3 -2
- data/lib/active_record/legacy_yaml_adapter.rb +2 -39
- data/lib/active_record/locking/pessimistic.rb +9 -3
- data/lib/active_record/log_subscriber.rb +8 -1
- data/lib/active_record/middleware/shard_selector.rb +60 -0
- data/lib/active_record/model_schema.rb +1 -28
- data/lib/active_record/nested_attributes.rb +11 -10
- data/lib/active_record/no_touching.rb +1 -1
- data/lib/active_record/persistence.rb +99 -21
- data/lib/active_record/query_logs.rb +18 -83
- data/lib/active_record/railtie.rb +11 -1
- data/lib/active_record/railties/databases.rake +4 -91
- data/lib/active_record/reflection.rb +22 -6
- data/lib/active_record/relation/calculations.rb +1 -10
- data/lib/active_record/relation/finder_methods.rb +0 -13
- data/lib/active_record/relation/query_methods.rb +5 -14
- data/lib/active_record/relation/record_fetch_warning.rb +5 -7
- data/lib/active_record/relation/where_clause.rb +2 -15
- data/lib/active_record/relation.rb +11 -13
- data/lib/active_record/result.rb +0 -5
- data/lib/active_record/runtime_registry.rb +10 -12
- data/lib/active_record/schema_dumper.rb +7 -0
- data/lib/active_record/scoping.rb +34 -22
- data/lib/active_record/suppressor.rb +11 -15
- data/lib/active_record/tasks/database_tasks.rb +18 -44
- data/lib/active_record/tasks/postgresql_database_tasks.rb +5 -1
- data/lib/active_record/validations/uniqueness.rb +1 -1
- data/lib/active_record.rb +41 -33
- data/lib/arel/crud.rb +12 -2
- data/lib/arel/delete_manager.rb +16 -0
- data/lib/arel/filter_predications.rb +9 -0
- data/lib/arel/nodes/delete_statement.rb +5 -1
- data/lib/arel/nodes/filter.rb +10 -0
- data/lib/arel/nodes/function.rb +1 -0
- data/lib/arel/nodes/update_statement.rb +5 -1
- data/lib/arel/nodes.rb +1 -0
- data/lib/arel/predications.rb +10 -2
- data/lib/arel/update_manager.rb +16 -0
- data/lib/arel/visitors/mysql.rb +2 -1
- data/lib/arel/visitors/to_sql.rb +15 -0
- data/lib/arel.rb +1 -0
- metadata +17 -13
@@ -32,11 +32,6 @@ module ActiveRecord
|
|
32
32
|
@configuration_hash = configuration_hash.symbolize_keys.freeze
|
33
33
|
end
|
34
34
|
|
35
|
-
def config
|
36
|
-
ActiveSupport::Deprecation.warn("DatabaseConfig#config will be removed in 7.0.0 in favor of DatabaseConfigurations#configuration_hash which returns a hash with symbol keys")
|
37
|
-
configuration_hash.stringify_keys
|
38
|
-
end
|
39
|
-
|
40
35
|
# Determines whether a database configuration is for a replica / readonly
|
41
36
|
# connection. If the +replica+ key is present in the config, +replica?+ will
|
42
37
|
# return +true+.
|
@@ -109,14 +104,51 @@ module ActiveRecord
|
|
109
104
|
configuration_hash[:schema_cache_path]
|
110
105
|
end
|
111
106
|
|
112
|
-
|
113
|
-
|
114
|
-
|
107
|
+
def default_schema_cache_path
|
108
|
+
"db/schema_cache.yml"
|
109
|
+
end
|
110
|
+
|
111
|
+
def lazy_schema_cache_path
|
112
|
+
schema_cache_path || default_schema_cache_path
|
113
|
+
end
|
114
|
+
|
115
|
+
def primary? # :nodoc:
|
116
|
+
Base.configurations.primary?(name)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Determines whether to dump the schema/structure files and the
|
120
|
+
# filename that should be used.
|
121
|
+
#
|
122
|
+
# If +configuration_hash[:schema_dump]+ is set to +false+ or +nil+
|
123
|
+
# the schema will not be dumped.
|
124
|
+
#
|
125
|
+
# If the config option is set that will be used. Otherwise Rails
|
126
|
+
# will generate the filename from the database config name.
|
127
|
+
def schema_dump(format = ActiveRecord.schema_format)
|
128
|
+
if configuration_hash.key?(:schema_dump)
|
129
|
+
if config = configuration_hash[:schema_dump]
|
130
|
+
config
|
131
|
+
end
|
132
|
+
elsif primary?
|
133
|
+
schema_file_type(format)
|
134
|
+
else
|
135
|
+
"#{name}_#{schema_file_type(format)}"
|
136
|
+
end
|
115
137
|
end
|
116
138
|
|
117
139
|
def database_tasks? # :nodoc:
|
118
140
|
!replica? && !!configuration_hash.fetch(:database_tasks, true)
|
119
141
|
end
|
142
|
+
|
143
|
+
private
|
144
|
+
def schema_file_type(format)
|
145
|
+
case format
|
146
|
+
when :ruby
|
147
|
+
"schema.rb"
|
148
|
+
when :sql
|
149
|
+
"structure.sql"
|
150
|
+
end
|
151
|
+
end
|
120
152
|
end
|
121
153
|
end
|
122
154
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "uri"
|
3
4
|
require "active_record/database_configurations/database_config"
|
4
5
|
require "active_record/database_configurations/hash_config"
|
5
6
|
require "active_record/database_configurations/url_config"
|
@@ -41,12 +42,7 @@ module ActiveRecord
|
|
41
42
|
# hidden by +database_tasks: false+ in the returned list. Most of the time we're only
|
42
43
|
# iterating over the primary connections (i.e. migrations don't need to run for the
|
43
44
|
# write and read connection). Defaults to +false+.
|
44
|
-
def configs_for(env_name: nil,
|
45
|
-
if spec_name
|
46
|
-
name = spec_name
|
47
|
-
ActiveSupport::Deprecation.warn("The kwarg `spec_name` is deprecated in favor of `name`. `spec_name` will be removed in Rails 7.0")
|
48
|
-
end
|
49
|
-
|
45
|
+
def configs_for(env_name: nil, name: nil, include_replicas: false, include_hidden: false)
|
50
46
|
if include_replicas
|
51
47
|
include_hidden = include_replicas
|
52
48
|
ActiveSupport::Deprecation.warn("The kwarg `include_replicas` is deprecated in favor of `include_hidden`. When `include_hidden` is passed, configurations with `replica: true` or `database_tasks: false` will be returned. `include_replicas` will be removed in Rails 7.1.")
|
@@ -70,19 +66,6 @@ module ActiveRecord
|
|
70
66
|
end
|
71
67
|
end
|
72
68
|
|
73
|
-
# Returns the config hash that corresponds with the environment
|
74
|
-
#
|
75
|
-
# If the application has multiple databases +default_hash+ will
|
76
|
-
# return the first config hash for the environment.
|
77
|
-
#
|
78
|
-
# { database: "my_db", adapter: "mysql2" }
|
79
|
-
def default_hash(env = default_env)
|
80
|
-
default = find_db_config(env)
|
81
|
-
default.configuration_hash if default
|
82
|
-
end
|
83
|
-
alias :[] :default_hash
|
84
|
-
deprecate "[]": "Use configs_for", default_hash: "Use configs_for"
|
85
|
-
|
86
69
|
# Returns a single DatabaseConfig object based on the requested environment.
|
87
70
|
#
|
88
71
|
# If the application has multiple databases +find_db_config+ will return
|
@@ -109,14 +92,6 @@ module ActiveRecord
|
|
109
92
|
first_config && name == first_config.name
|
110
93
|
end
|
111
94
|
|
112
|
-
# Returns the DatabaseConfigurations object as a Hash.
|
113
|
-
def to_h
|
114
|
-
configurations.inject({}) do |memo, db_config|
|
115
|
-
memo.merge(db_config.env_name => db_config.configuration_hash.stringify_keys)
|
116
|
-
end
|
117
|
-
end
|
118
|
-
deprecate to_h: "You can use `ActiveRecord::Base.configurations.configs_for(env_name: 'env', name: 'primary').configuration_hash` to get the configuration hashes."
|
119
|
-
|
120
95
|
# Checks if the application's configurations are empty.
|
121
96
|
#
|
122
97
|
# Aliased to blank?
|
@@ -137,6 +137,21 @@ module ActiveRecord
|
|
137
137
|
# end
|
138
138
|
#
|
139
139
|
# Now you can list a bunch of entries, call +Entry#title+, and polymorphism will provide you with the answer.
|
140
|
+
#
|
141
|
+
# == Nested Attributes
|
142
|
+
#
|
143
|
+
# Enabling nested attributes on a delegated_type association allows you to
|
144
|
+
# create the entry and message in one go:
|
145
|
+
#
|
146
|
+
# class Entry < ApplicationRecord
|
147
|
+
# delegated_type :entryable, types: %w[ Message Comment ]
|
148
|
+
# accepts_nested_attributes_for :entryable
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# params = { entry: { entryable_type: 'Message', entryable_attributes: { subject: 'Smiling' } } }
|
152
|
+
# entry = Entry.create(params[:entry])
|
153
|
+
# entry.entryable.id # => 2
|
154
|
+
# entry.entryable.subject # => 'Smiling'
|
140
155
|
module DelegatedType
|
141
156
|
# Defines this as a class that'll delegate its type for the passed +role+ to the class references in +types+.
|
142
157
|
# That'll create a polymorphic +belongs_to+ relationship to that +role+, and it'll add all the delegated
|
@@ -207,6 +222,10 @@ module ActiveRecord
|
|
207
222
|
public_send("#{role}_class").model_name.singular.inquiry
|
208
223
|
end
|
209
224
|
|
225
|
+
define_method "build_#{role}" do |*params|
|
226
|
+
public_send("#{role}=", public_send("#{role}_class").new(*params))
|
227
|
+
end
|
228
|
+
|
210
229
|
types.each do |type|
|
211
230
|
scope_name = type.tableize.tr("/", "_")
|
212
231
|
singular = scope_name.singularize
|
@@ -17,7 +17,7 @@ module ActiveRecord
|
|
17
17
|
|
18
18
|
# === Options
|
19
19
|
#
|
20
|
-
# * <tt>:scheme</tt> -
|
20
|
+
# * <tt>:scheme</tt> - A +Scheme+ with the encryption properties for this attribute.
|
21
21
|
# * <tt>:cast_type</tt> - A type that will be used to serialize (before encrypting) and deserialize
|
22
22
|
# (after decrypting). +ActiveModel::Type::String+ by default.
|
23
23
|
def initialize(scheme:, cast_type: ActiveModel::Type::String.new, previous_type: false)
|
@@ -12,7 +12,7 @@ module ActiveRecord
|
|
12
12
|
super(record, attribute, value)
|
13
13
|
|
14
14
|
klass = record.class
|
15
|
-
|
15
|
+
klass.deterministic_encrypted_attributes&.each do |attribute_name|
|
16
16
|
encrypted_type = klass.type_for_attribute(attribute_name)
|
17
17
|
[ encrypted_type, *encrypted_type.previous_types ].each do |type|
|
18
18
|
encrypted_value = type.serialize(value)
|
@@ -21,7 +21,6 @@ module ActiveRecord
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
24
|
-
end
|
25
24
|
end
|
26
25
|
end
|
27
26
|
end
|
@@ -33,10 +33,20 @@ module ActiveRecord
|
|
33
33
|
|
34
34
|
private
|
35
35
|
def parse_message(data, level)
|
36
|
-
|
36
|
+
validate_message_data_format(data, level)
|
37
37
|
ActiveRecord::Encryption::Message.new(payload: decode_if_needed(data["p"]), headers: parse_properties(data["h"], level))
|
38
38
|
end
|
39
39
|
|
40
|
+
def validate_message_data_format(data, level)
|
41
|
+
if level > 2
|
42
|
+
raise ActiveRecord::Encryption::Errors::Decryption, "More than one level of hash nesting in headers is not supported"
|
43
|
+
end
|
44
|
+
|
45
|
+
unless data.is_a?(Hash) && data.has_key?("p")
|
46
|
+
raise ActiveRecord::Encryption::Errors::Decryption, "Invalid data format: hash without payload"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
40
50
|
def parse_properties(headers, level)
|
41
51
|
ActiveRecord::Encryption::Properties.new.tap do |properties|
|
42
52
|
headers&.each do |key, value|
|
@@ -82,7 +82,7 @@ module ActiveRecord
|
|
82
82
|
|
83
83
|
def validate_credential(key, error_message = "is not configured")
|
84
84
|
unless ActiveRecord::Encryption.config.public_send(key).present?
|
85
|
-
raise Errors::Configuration, "#{key} #{error_message}. Please configure it via credential"\
|
85
|
+
raise Errors::Configuration, "#{key} #{error_message}. Please configure it via credential "\
|
86
86
|
"active_record_encryption.#{key} or by setting config.active_record.encryption.#{key}"
|
87
87
|
end
|
88
88
|
end
|
data/lib/active_record/enum.rb
CHANGED
@@ -57,13 +57,20 @@ module ActiveRecord
|
|
57
57
|
# conversation = Conversation.new
|
58
58
|
# conversation.status # => "active"
|
59
59
|
#
|
60
|
-
#
|
60
|
+
# It's possible to explicitly map the relation between attribute and
|
61
61
|
# database integer with a hash:
|
62
62
|
#
|
63
63
|
# class Conversation < ActiveRecord::Base
|
64
64
|
# enum :status, active: 0, archived: 1
|
65
65
|
# end
|
66
66
|
#
|
67
|
+
# Finally it's also possible to use a string column to persist the enumerated value.
|
68
|
+
# Note that this will likely lead to slower database queries:
|
69
|
+
#
|
70
|
+
# class Conversation < ActiveRecord::Base
|
71
|
+
# enum :status, active: "active", archived: "archived"
|
72
|
+
# end
|
73
|
+
#
|
67
74
|
# Note that when an array is used, the implicit mapping from the values to database
|
68
75
|
# integers is derived from the order the values appear in the array. In the example,
|
69
76
|
# <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
|
data/lib/active_record/errors.rb
CHANGED
@@ -326,7 +326,7 @@ module ActiveRecord
|
|
326
326
|
# # The system must fail on Friday so that our support department
|
327
327
|
# # won't be out of job. We silently rollback this transaction
|
328
328
|
# # without telling the user.
|
329
|
-
# raise ActiveRecord::Rollback
|
329
|
+
# raise ActiveRecord::Rollback
|
330
330
|
# end
|
331
331
|
# end
|
332
332
|
# # ActiveRecord::Rollback is the only exception that won't be passed on
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_support/
|
3
|
+
require "active_support/core_ext/module/delegation"
|
4
4
|
|
5
5
|
module ActiveRecord
|
6
6
|
# This is a thread locals registry for EXPLAIN. For example
|
@@ -8,13 +8,18 @@ module ActiveRecord
|
|
8
8
|
# ActiveRecord::ExplainRegistry.queries
|
9
9
|
#
|
10
10
|
# returns the collected queries local to the current thread.
|
11
|
-
#
|
12
|
-
# See the documentation of ActiveSupport::PerThreadRegistry
|
13
|
-
# for further details.
|
14
11
|
class ExplainRegistry # :nodoc:
|
15
|
-
|
12
|
+
class << self
|
13
|
+
delegate :reset, :collect, :collect=, :collect?, :queries, to: :instance
|
14
|
+
|
15
|
+
private
|
16
|
+
def instance
|
17
|
+
ActiveSupport::IsolatedExecutionState[:active_record_explain_registry] ||= new
|
18
|
+
end
|
19
|
+
end
|
16
20
|
|
17
|
-
attr_accessor :
|
21
|
+
attr_accessor :collect
|
22
|
+
attr_reader :queries
|
18
23
|
|
19
24
|
def initialize
|
20
25
|
reset
|
@@ -126,7 +126,7 @@ module ActiveRecord
|
|
126
126
|
end
|
127
127
|
|
128
128
|
def resolve_enums
|
129
|
-
|
129
|
+
reflection_class.defined_enums.each do |name, values|
|
130
130
|
if @row.include?(name)
|
131
131
|
@row[name] = values.fetch(@row[name], @row[name])
|
132
132
|
end
|
@@ -407,7 +407,7 @@ module ActiveRecord
|
|
407
407
|
# defaults:
|
408
408
|
#
|
409
409
|
# DEFAULTS: &DEFAULTS
|
410
|
-
# created_on: <%= 3.weeks.ago.
|
410
|
+
# created_on: <%= 3.weeks.ago.to_formatted_s(:db) %>
|
411
411
|
#
|
412
412
|
# first:
|
413
413
|
# name: Smurf
|
@@ -585,14 +585,6 @@ module ActiveRecord
|
|
585
585
|
end
|
586
586
|
end
|
587
587
|
|
588
|
-
def signed_global_id(fixture_set_name, label, column_type: :integer, **options)
|
589
|
-
identifier = identify(label, column_type)
|
590
|
-
model_name = default_fixture_model_name(fixture_set_name)
|
591
|
-
uri = URI::GID.build([GlobalID.app, model_name, identifier, {}])
|
592
|
-
|
593
|
-
SignedGlobalID.new(uri, **options)
|
594
|
-
end
|
595
|
-
|
596
588
|
# Superclass for the evaluation contexts used by ERB fixtures.
|
597
589
|
def context_class
|
598
590
|
@context_class ||= Class.new
|
@@ -102,12 +102,12 @@ module ActiveRecord
|
|
102
102
|
|
103
103
|
def execute_or_wait
|
104
104
|
if pending?
|
105
|
-
start =
|
105
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
106
106
|
@mutex.synchronize do
|
107
107
|
if pending?
|
108
108
|
execute_query(@pool.connection)
|
109
109
|
else
|
110
|
-
@lock_wait = (
|
110
|
+
@lock_wait = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start)
|
111
111
|
end
|
112
112
|
end
|
113
113
|
else
|
@@ -5,21 +5,19 @@ require "active_support/core_ext/enumerable"
|
|
5
5
|
module ActiveRecord
|
6
6
|
class InsertAll # :nodoc:
|
7
7
|
attr_reader :model, :connection, :inserts, :keys
|
8
|
-
attr_reader :on_duplicate, :returning, :unique_by, :update_sql
|
8
|
+
attr_reader :on_duplicate, :update_only, :returning, :unique_by, :update_sql
|
9
9
|
|
10
|
-
def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil)
|
10
|
+
def initialize(model, inserts, on_duplicate:, update_only: nil, returning: nil, unique_by: nil, record_timestamps: nil)
|
11
11
|
raise ArgumentError, "Empty list of attributes passed" if inserts.blank?
|
12
12
|
|
13
13
|
@model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s)
|
14
|
-
@on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
|
14
|
+
@on_duplicate, @update_only, @returning, @unique_by = on_duplicate, update_only, returning, unique_by
|
15
|
+
@record_timestamps = record_timestamps.nil? ? model.record_timestamps : record_timestamps
|
15
16
|
|
16
|
-
disallow_raw_sql!(returning)
|
17
17
|
disallow_raw_sql!(on_duplicate)
|
18
|
+
disallow_raw_sql!(returning)
|
18
19
|
|
19
|
-
|
20
|
-
@update_sql = on_duplicate
|
21
|
-
@on_duplicate = :update
|
22
|
-
end
|
20
|
+
configure_on_duplicate_update_logic
|
23
21
|
|
24
22
|
if model.scope_attributes?
|
25
23
|
@scope_attributes = model.scope_attributes
|
@@ -44,7 +42,7 @@ module ActiveRecord
|
|
44
42
|
end
|
45
43
|
|
46
44
|
def updatable_columns
|
47
|
-
keys - readonly_columns - unique_by_columns
|
45
|
+
@updatable_columns ||= keys - readonly_columns - unique_by_columns
|
48
46
|
end
|
49
47
|
|
50
48
|
def primary_keys
|
@@ -64,18 +62,50 @@ module ActiveRecord
|
|
64
62
|
inserts.map do |attributes|
|
65
63
|
attributes = attributes.stringify_keys
|
66
64
|
attributes.merge!(scope_attributes) if scope_attributes
|
65
|
+
attributes.reverse_merge!(timestamps_for_create) if record_timestamps?
|
67
66
|
|
68
67
|
verify_attributes(attributes)
|
69
68
|
|
70
|
-
|
69
|
+
keys_including_timestamps.map do |key|
|
71
70
|
yield key, attributes[key]
|
72
71
|
end
|
73
72
|
end
|
74
73
|
end
|
75
74
|
|
75
|
+
def record_timestamps?
|
76
|
+
@record_timestamps
|
77
|
+
end
|
78
|
+
|
79
|
+
# TODO: Consider remaining this method, as it only conditionally extends keys, not always
|
80
|
+
def keys_including_timestamps
|
81
|
+
@keys_including_timestamps ||= if record_timestamps?
|
82
|
+
keys + model.all_timestamp_attributes_in_model
|
83
|
+
else
|
84
|
+
keys
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
76
88
|
private
|
77
89
|
attr_reader :scope_attributes
|
78
90
|
|
91
|
+
def configure_on_duplicate_update_logic
|
92
|
+
if custom_update_sql_provided? && update_only.present?
|
93
|
+
raise ArgumentError, "You can't set :update_only and provide custom update SQL via :on_duplicate at the same time"
|
94
|
+
end
|
95
|
+
|
96
|
+
if update_only.present?
|
97
|
+
@updatable_columns = Array(update_only)
|
98
|
+
@on_duplicate = :update
|
99
|
+
elsif custom_update_sql_provided?
|
100
|
+
@update_sql = on_duplicate
|
101
|
+
@on_duplicate = :update
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def custom_update_sql_provided?
|
106
|
+
@custom_update_sql_provided ||= Arel.arel_node?(on_duplicate)
|
107
|
+
end
|
108
|
+
|
79
109
|
def find_unique_index_for(unique_by)
|
80
110
|
if !connection.supports_insert_conflict_target?
|
81
111
|
return if unique_by.nil?
|
@@ -134,7 +164,7 @@ module ActiveRecord
|
|
134
164
|
|
135
165
|
|
136
166
|
def verify_attributes(attributes)
|
137
|
-
if
|
167
|
+
if keys_including_timestamps != attributes.keys.to_set
|
138
168
|
raise ArgumentError, "All objects being inserted must have the same keys"
|
139
169
|
end
|
140
170
|
end
|
@@ -148,10 +178,14 @@ module ActiveRecord
|
|
148
178
|
"by wrapping them in Arel.sql()."
|
149
179
|
end
|
150
180
|
|
181
|
+
def timestamps_for_create
|
182
|
+
model.all_timestamp_attributes_in_model.index_with(connection.high_precision_current_timestamp)
|
183
|
+
end
|
184
|
+
|
151
185
|
class Builder # :nodoc:
|
152
186
|
attr_reader :model
|
153
187
|
|
154
|
-
delegate :skip_duplicates?, :update_duplicates?, :keys, to: :insert_all
|
188
|
+
delegate :skip_duplicates?, :update_duplicates?, :keys, :keys_including_timestamps, :record_timestamps?, to: :insert_all
|
155
189
|
|
156
190
|
def initialize(insert_all)
|
157
191
|
@insert_all, @model, @connection = insert_all, insert_all.model, insert_all.connection
|
@@ -162,9 +196,10 @@ module ActiveRecord
|
|
162
196
|
end
|
163
197
|
|
164
198
|
def values_list
|
165
|
-
types = extract_types_from_columns_on(model.table_name, keys:
|
199
|
+
types = extract_types_from_columns_on(model.table_name, keys: keys_including_timestamps)
|
166
200
|
|
167
201
|
values_list = insert_all.map_key_with_value do |key, value|
|
202
|
+
next value if Arel::Nodes::SqlLiteral === value
|
168
203
|
connection.with_yaml_fallback(types[key].serialize(value))
|
169
204
|
end
|
170
205
|
|
@@ -196,6 +231,8 @@ module ActiveRecord
|
|
196
231
|
end
|
197
232
|
|
198
233
|
def touch_model_timestamps_unless(&block)
|
234
|
+
return "" unless update_duplicates? && record_timestamps?
|
235
|
+
|
199
236
|
model.timestamp_attributes_for_update_in_model.filter_map do |column_name|
|
200
237
|
if touch_timestamp_attribute?(column_name)
|
201
238
|
"#{column_name}=(CASE WHEN (#{updatable_columns.map(&block).join(" AND ")}) THEN #{model.quoted_table_name}.#{column_name} ELSE #{connection.high_precision_current_timestamp} END),"
|
@@ -213,11 +250,11 @@ module ActiveRecord
|
|
213
250
|
attr_reader :connection, :insert_all
|
214
251
|
|
215
252
|
def touch_timestamp_attribute?(column_name)
|
216
|
-
|
253
|
+
insert_all.updatable_columns.exclude?(column_name)
|
217
254
|
end
|
218
255
|
|
219
256
|
def columns_list
|
220
|
-
format_columns(insert_all.
|
257
|
+
format_columns(insert_all.keys_including_timestamps)
|
221
258
|
end
|
222
259
|
|
223
260
|
def extract_types_from_columns_on(table_name, keys:)
|
@@ -79,7 +79,7 @@ module ActiveRecord
|
|
79
79
|
timestamp = max_updated_column_timestamp
|
80
80
|
|
81
81
|
if timestamp
|
82
|
-
timestamp = timestamp.utc.
|
82
|
+
timestamp = timestamp.utc.to_formatted_s(cache_timestamp_format)
|
83
83
|
"#{model_name.cache_key}/#{id}-#{timestamp}"
|
84
84
|
else
|
85
85
|
"#{model_name.cache_key}/#{id}"
|
@@ -101,8 +101,9 @@ module ActiveRecord
|
|
101
101
|
timestamp = updated_at_before_type_cast
|
102
102
|
if can_use_fast_cache_version?(timestamp)
|
103
103
|
raw_timestamp_to_cache_version(timestamp)
|
104
|
+
|
104
105
|
elsif timestamp = updated_at
|
105
|
-
timestamp.utc.
|
106
|
+
timestamp.utc.to_formatted_s(cache_timestamp_format)
|
106
107
|
end
|
107
108
|
elsif self.class.has_attribute?("updated_at")
|
108
109
|
raise ActiveModel::MissingAttributeError, "missing attribute: updated_at"
|
@@ -2,50 +2,13 @@
|
|
2
2
|
|
3
3
|
module ActiveRecord
|
4
4
|
module LegacyYamlAdapter # :nodoc:
|
5
|
-
def self.convert(
|
5
|
+
def self.convert(coder)
|
6
6
|
return coder unless coder.is_a?(Psych::Coder)
|
7
7
|
|
8
8
|
case coder["active_record_yaml_version"]
|
9
9
|
when 1, 2 then coder
|
10
10
|
else
|
11
|
-
|
12
|
-
YAML loading from legacy format older than Rails 5.0 is deprecated
|
13
|
-
and will be removed in Rails 7.0.
|
14
|
-
MSG
|
15
|
-
if coder["attributes"].is_a?(ActiveModel::AttributeSet)
|
16
|
-
Rails420.convert(klass, coder)
|
17
|
-
else
|
18
|
-
Rails41.convert(klass, coder)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
module Rails420 # :nodoc:
|
24
|
-
def self.convert(klass, coder)
|
25
|
-
attribute_set = coder["attributes"]
|
26
|
-
|
27
|
-
klass.attribute_names.each do |attr_name|
|
28
|
-
attribute = attribute_set[attr_name]
|
29
|
-
if attribute.type.is_a?(Delegator)
|
30
|
-
type_from_klass = klass.type_for_attribute(attr_name)
|
31
|
-
attribute_set[attr_name] = attribute.with_type(type_from_klass)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
coder
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
module Rails41 # :nodoc:
|
40
|
-
def self.convert(klass, coder)
|
41
|
-
attributes = klass.attributes_builder
|
42
|
-
.build_from_database(coder["attributes"])
|
43
|
-
new_record = coder["attributes"][klass.primary_key].blank?
|
44
|
-
|
45
|
-
{
|
46
|
-
"attributes" => attributes,
|
47
|
-
"new_record" => new_record,
|
48
|
-
}
|
11
|
+
raise("Active Record doesn't know how to load YAML with this format.")
|
49
12
|
end
|
50
13
|
end
|
51
14
|
end
|
@@ -81,9 +81,15 @@ module ActiveRecord
|
|
81
81
|
|
82
82
|
# Wraps the passed block in a transaction, locking the object
|
83
83
|
# before yielding. You can pass the SQL locking clause
|
84
|
-
# as argument (see <tt
|
85
|
-
|
86
|
-
|
84
|
+
# as an optional argument (see <tt>#lock!</tt>).
|
85
|
+
#
|
86
|
+
# You can also pass options like <tt>requires_new:</tt>, <tt>isolation:</tt>,
|
87
|
+
# and <tt>joinable:</tt> to the wrapping transaction (see
|
88
|
+
# <tt>ActiveRecord::ConnectionAdapters::DatabaseStatements#transaction</tt>).
|
89
|
+
def with_lock(*args)
|
90
|
+
transaction_opts = args.extract_options!
|
91
|
+
lock = args.present? ? args.first : true
|
92
|
+
transaction(**transaction_opts) do
|
87
93
|
lock!(lock)
|
88
94
|
yield
|
89
95
|
end
|
@@ -51,7 +51,10 @@ module ActiveRecord
|
|
51
51
|
|
52
52
|
binds = []
|
53
53
|
payload[:binds].each_with_index do |attr, i|
|
54
|
-
|
54
|
+
attribute_name = attr.respond_to?(:name) ? attr.name : attr[i].name
|
55
|
+
filtered_params = filter(attribute_name, casted_params[i])
|
56
|
+
|
57
|
+
binds << render_bind(attr, filtered_params)
|
55
58
|
end
|
56
59
|
binds = binds.inspect
|
57
60
|
binds.prepend(" ")
|
@@ -135,6 +138,10 @@ module ActiveRecord
|
|
135
138
|
def extract_query_source_location(locations)
|
136
139
|
backtrace_cleaner.clean(locations.lazy).first
|
137
140
|
end
|
141
|
+
|
142
|
+
def filter(name, value)
|
143
|
+
ActiveRecord::Base.inspection_filter.filter_param(name, value)
|
144
|
+
end
|
138
145
|
end
|
139
146
|
end
|
140
147
|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Middleware
|
5
|
+
# The ShardSelector Middleware provides a framework for automatically
|
6
|
+
# swapping shards. Rails provides a basic framework to determine which
|
7
|
+
# shard to switch to and allows for applications to write custom strategies
|
8
|
+
# for swapping if needed.
|
9
|
+
#
|
10
|
+
# The ShardSelector takes a set of options (currently only `lock` is supported)
|
11
|
+
# that can be used by the middleware to alter behavior. `lock` is
|
12
|
+
# true by default and will prohibit the request from switching shards once
|
13
|
+
# inside the block. If `lock` is false, then shard swapping will be allowed.
|
14
|
+
# For tenant based sharding, `lock` should always be true to prevent application
|
15
|
+
# code from mistakenly switching between tenants.
|
16
|
+
#
|
17
|
+
# Options can be set in the config:
|
18
|
+
#
|
19
|
+
# config.active_record.shard_selector = { lock: true }
|
20
|
+
#
|
21
|
+
# Applications must also provide the code for the resolver as it depends on application
|
22
|
+
# specific models. An example resolver would look like this:
|
23
|
+
#
|
24
|
+
# config.active_record.shard_resolver = ->(request) {
|
25
|
+
# subdomain = request.subdomain
|
26
|
+
# tenant = Tenant.find_by_subdomain!(subdomain)
|
27
|
+
# tenant.shard
|
28
|
+
# }
|
29
|
+
class ShardSelector
|
30
|
+
def initialize(app, resolver, options = {})
|
31
|
+
@app = app
|
32
|
+
@resolver = resolver
|
33
|
+
@options = options
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :resolver, :options
|
37
|
+
|
38
|
+
def call(env)
|
39
|
+
request = ActionDispatch::Request.new(env)
|
40
|
+
|
41
|
+
shard = selected_shard(request)
|
42
|
+
|
43
|
+
set_shard(shard) do
|
44
|
+
@app.call(env)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def selected_shard(request)
|
50
|
+
resolver.call(request)
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_shard(shard, &block)
|
54
|
+
ActiveRecord::Base.connected_to(shard: shard.to_sym) do
|
55
|
+
ActiveRecord::Base.prohibit_shard_swapping(options.fetch(:lock, true), &block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|