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.

Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +516 -10
  3. data/lib/active_record/associations/association.rb +2 -8
  4. data/lib/active_record/associations/builder/collection_association.rb +9 -2
  5. data/lib/active_record/associations/collection_association.rb +10 -2
  6. data/lib/active_record/associations/preloader/association.rb +68 -48
  7. data/lib/active_record/associations/preloader/batch.rb +3 -6
  8. data/lib/active_record/associations/preloader/through_association.rb +19 -9
  9. data/lib/active_record/associations/preloader.rb +14 -24
  10. data/lib/active_record/associations/through_association.rb +2 -2
  11. data/lib/active_record/associations.rb +16 -3
  12. data/lib/active_record/asynchronous_queries_tracker.rb +3 -0
  13. data/lib/active_record/attribute_methods/dirty.rb +9 -1
  14. data/lib/active_record/attribute_methods.rb +7 -5
  15. data/lib/active_record/autosave_association.rb +3 -3
  16. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +6 -26
  17. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +2 -2
  18. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +18 -5
  19. data/lib/active_record/connection_adapters/abstract/database_limits.rb +0 -17
  20. data/lib/active_record/connection_adapters/abstract/database_statements.rb +1 -1
  21. data/lib/active_record/connection_adapters/abstract/quoting.rb +33 -70
  22. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +4 -0
  23. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +9 -2
  24. data/lib/active_record/connection_adapters/abstract/transaction.rb +12 -19
  25. data/lib/active_record/connection_adapters/abstract_adapter.rb +37 -8
  26. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +1 -0
  27. data/lib/active_record/connection_adapters/column.rb +4 -0
  28. data/lib/active_record/connection_adapters/mysql/database_statements.rb +2 -2
  29. data/lib/active_record/connection_adapters/mysql/quoting.rb +23 -24
  30. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +1 -1
  31. data/lib/active_record/connection_adapters/pool_config.rb +7 -5
  32. data/lib/active_record/connection_adapters/postgresql/column.rb +17 -1
  33. data/lib/active_record/connection_adapters/postgresql/quoting.rb +44 -44
  34. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +21 -1
  35. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +18 -1
  36. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +25 -0
  37. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +15 -4
  38. data/lib/active_record/connection_adapters/postgresql_adapter.rb +48 -5
  39. data/lib/active_record/connection_adapters/schema_cache.rb +3 -1
  40. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +2 -2
  41. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +15 -16
  42. data/lib/active_record/connection_handling.rb +31 -19
  43. data/lib/active_record/core.rb +13 -24
  44. data/lib/active_record/database_configurations/connection_url_resolver.rb +2 -1
  45. data/lib/active_record/database_configurations/database_config.rb +0 -9
  46. data/lib/active_record/database_configurations/hash_config.rb +40 -8
  47. data/lib/active_record/database_configurations.rb +2 -27
  48. data/lib/active_record/delegated_type.rb +19 -0
  49. data/lib/active_record/encryption/encrypted_attribute_type.rb +1 -1
  50. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +1 -2
  51. data/lib/active_record/encryption/message_serializer.rb +11 -1
  52. data/lib/active_record/encryption/scheme.rb +1 -1
  53. data/lib/active_record/enum.rb +8 -1
  54. data/lib/active_record/errors.rb +1 -1
  55. data/lib/active_record/explain_registry.rb +11 -6
  56. data/lib/active_record/fixture_set/table_row.rb +1 -1
  57. data/lib/active_record/fixtures.rb +1 -9
  58. data/lib/active_record/future_result.rb +2 -2
  59. data/lib/active_record/gem_version.rb +1 -1
  60. data/lib/active_record/insert_all.rb +52 -15
  61. data/lib/active_record/integration.rb +3 -2
  62. data/lib/active_record/legacy_yaml_adapter.rb +2 -39
  63. data/lib/active_record/locking/pessimistic.rb +9 -3
  64. data/lib/active_record/log_subscriber.rb +8 -1
  65. data/lib/active_record/middleware/shard_selector.rb +60 -0
  66. data/lib/active_record/model_schema.rb +1 -28
  67. data/lib/active_record/nested_attributes.rb +11 -10
  68. data/lib/active_record/no_touching.rb +1 -1
  69. data/lib/active_record/persistence.rb +99 -21
  70. data/lib/active_record/query_logs.rb +18 -83
  71. data/lib/active_record/railtie.rb +11 -1
  72. data/lib/active_record/railties/databases.rake +4 -91
  73. data/lib/active_record/reflection.rb +22 -6
  74. data/lib/active_record/relation/calculations.rb +1 -10
  75. data/lib/active_record/relation/finder_methods.rb +0 -13
  76. data/lib/active_record/relation/query_methods.rb +5 -14
  77. data/lib/active_record/relation/record_fetch_warning.rb +5 -7
  78. data/lib/active_record/relation/where_clause.rb +2 -15
  79. data/lib/active_record/relation.rb +11 -13
  80. data/lib/active_record/result.rb +0 -5
  81. data/lib/active_record/runtime_registry.rb +10 -12
  82. data/lib/active_record/schema_dumper.rb +7 -0
  83. data/lib/active_record/scoping.rb +34 -22
  84. data/lib/active_record/suppressor.rb +11 -15
  85. data/lib/active_record/tasks/database_tasks.rb +18 -44
  86. data/lib/active_record/tasks/postgresql_database_tasks.rb +5 -1
  87. data/lib/active_record/validations/uniqueness.rb +1 -1
  88. data/lib/active_record.rb +41 -33
  89. data/lib/arel/crud.rb +12 -2
  90. data/lib/arel/delete_manager.rb +16 -0
  91. data/lib/arel/filter_predications.rb +9 -0
  92. data/lib/arel/nodes/delete_statement.rb +5 -1
  93. data/lib/arel/nodes/filter.rb +10 -0
  94. data/lib/arel/nodes/function.rb +1 -0
  95. data/lib/arel/nodes/update_statement.rb +5 -1
  96. data/lib/arel/nodes.rb +1 -0
  97. data/lib/arel/predications.rb +10 -2
  98. data/lib/arel/update_manager.rb +16 -0
  99. data/lib/arel/visitors/mysql.rb +2 -1
  100. data/lib/arel/visitors/to_sql.rb +15 -0
  101. data/lib/arel.rb +1 -0
  102. 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
- # Determines whether to dump the schema for a database.
113
- def schema_dump
114
- configuration_hash.fetch(:schema_dump, true)
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, spec_name: nil, name: nil, include_replicas: false, include_hidden: false)
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> - An +Scheme+ with the encryption properties for this attribute.
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
- if klass.deterministic_encrypted_attributes&.each do |attribute_name|
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
- raise ActiveRecord::Encryption::Errors::Decryption, "More than one level of hash nesting in headers is not supported" if level > 2
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
@@ -57,13 +57,20 @@ module ActiveRecord
57
57
  # conversation = Conversation.new
58
58
  # conversation.status # => "active"
59
59
  #
60
- # Finally, it's also possible to explicitly map the relation between attribute and
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>
@@ -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, "Call tech support!"
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/per_thread_registry"
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
- extend ActiveSupport::PerThreadRegistry
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 :queries, :collect
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
- model_class.defined_enums.each do |name, values|
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.to_s(:db) %>
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 = Concurrent.monotonic_time
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 = (Concurrent.monotonic_time - start) * 1_000
110
+ @lock_wait = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start)
111
111
  end
112
112
  end
113
113
  else
@@ -10,7 +10,7 @@ module ActiveRecord
10
10
  MAJOR = 7
11
11
  MINOR = 0
12
12
  TINY = 0
13
- PRE = "alpha1"
13
+ PRE = "rc3"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -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
- if Arel.arel_node?(on_duplicate)
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
- keys.map do |key|
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 keys != attributes.keys.to_set
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: 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
- update_duplicates? && !insert_all.updatable_columns.include?(column_name)
253
+ insert_all.updatable_columns.exclude?(column_name)
217
254
  end
218
255
 
219
256
  def columns_list
220
- format_columns(insert_all.keys)
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.to_s(cache_timestamp_format)
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.to_s(cache_timestamp_format)
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(klass, coder)
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
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
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>lock!</tt>).
85
- def with_lock(lock = true)
86
- transaction do
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
- binds << render_bind(attr, casted_params[i])
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