activerecord 6.0.3.3 → 6.0.4

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -0
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/associations/association.rb +2 -3
  5. data/lib/active_record/associations/association_scope.rb +7 -1
  6. data/lib/active_record/associations/collection_association.rb +7 -0
  7. data/lib/active_record/associations/collection_proxy.rb +1 -0
  8. data/lib/active_record/associations/join_dependency.rb +14 -6
  9. data/lib/active_record/associations/join_dependency/join_association.rb +7 -0
  10. data/lib/active_record/associations/join_dependency/join_part.rb +2 -2
  11. data/lib/active_record/associations/preloader.rb +6 -2
  12. data/lib/active_record/associations/preloader/association.rb +41 -23
  13. data/lib/active_record/associations/preloader/through_association.rb +1 -1
  14. data/lib/active_record/associations/through_association.rb +1 -1
  15. data/lib/active_record/autosave_association.rb +10 -10
  16. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +6 -0
  17. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +6 -1
  18. data/lib/active_record/connection_adapters/abstract_adapter.rb +6 -0
  19. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +9 -4
  20. data/lib/active_record/connection_adapters/mysql/database_statements.rb +1 -1
  21. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +8 -4
  22. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +1 -1
  23. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +8 -0
  24. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +2 -2
  25. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +24 -5
  26. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +1 -1
  27. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +1 -1
  28. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -1
  29. data/lib/active_record/core.rb +5 -6
  30. data/lib/active_record/enum.rb +13 -6
  31. data/lib/active_record/fixture_set/render_context.rb +1 -1
  32. data/lib/active_record/gem_version.rb +2 -2
  33. data/lib/active_record/insert_all.rb +1 -1
  34. data/lib/active_record/locking/optimistic.rb +9 -0
  35. data/lib/active_record/model_schema.rb +29 -0
  36. data/lib/active_record/reflection.rb +11 -13
  37. data/lib/active_record/relation.rb +6 -3
  38. data/lib/active_record/relation/calculations.rb +1 -1
  39. data/lib/active_record/relation/delegation.rb +2 -1
  40. data/lib/active_record/relation/finder_methods.rb +1 -1
  41. data/lib/active_record/relation/merger.rb +7 -2
  42. data/lib/active_record/relation/query_methods.rb +23 -11
  43. data/lib/active_record/scoping/named.rb +5 -0
  44. data/lib/active_record/test_fixtures.rb +19 -1
  45. data/lib/active_record/type/time.rb +10 -0
  46. data/lib/active_record/validations/associated.rb +1 -1
  47. metadata +9 -9
@@ -29,7 +29,7 @@ module ActiveRecord
29
29
  # == Callbacks
30
30
  #
31
31
  # Association with autosave option defines several callbacks on your
32
- # model (before_save, after_create, after_update). Please note that
32
+ # model (around_save, before_save, after_create, after_update). Please note that
33
33
  # callbacks are executed in the order they were defined in
34
34
  # model. You should avoid modifying the association content, before
35
35
  # autosave callbacks are executed. Placing your callbacks after
@@ -180,8 +180,7 @@ module ActiveRecord
180
180
  save_method = :"autosave_associated_records_for_#{reflection.name}"
181
181
 
182
182
  if reflection.collection?
183
- before_save :before_save_collection_association
184
- after_save :after_save_collection_association
183
+ around_save :around_save_collection_association
185
184
 
186
185
  define_non_cyclic_method(save_method) { save_collection_association(reflection) }
187
186
  # Doesn't use after_save as that would save associations added in after_create/after_update twice
@@ -358,14 +357,15 @@ module ActiveRecord
358
357
  end
359
358
  end
360
359
 
361
- # Is used as a before_save callback to check while saving a collection
360
+ # Is used as an around_save callback to check while saving a collection
362
361
  # association whether or not the parent was a new record before saving.
363
- def before_save_collection_association
364
- @new_record_before_save ||= new_record?
365
- end
362
+ def around_save_collection_association
363
+ previously_new_record_before_save = (@new_record_before_save ||= false)
364
+ @new_record_before_save = !previously_new_record_before_save && new_record?
366
365
 
367
- def after_save_collection_association
368
- @new_record_before_save = false
366
+ yield
367
+ ensure
368
+ @new_record_before_save = previously_new_record_before_save
369
369
  end
370
370
 
371
371
  # Saves any new associated records, or all loaded autosave associations if
@@ -444,7 +444,7 @@ module ActiveRecord
444
444
  unless reflection.through_reflection
445
445
  record[reflection.foreign_key] = key
446
446
  if inverse_reflection = reflection.inverse_of
447
- record.association(inverse_reflection.name).loaded!
447
+ record.association(inverse_reflection.name).inversed_from(self)
448
448
  end
449
449
  end
450
450
 
@@ -1035,6 +1035,12 @@ module ActiveRecord
1035
1035
  # In some cases you may want to prevent writes to the database
1036
1036
  # even if you are on a database that can write. `while_preventing_writes`
1037
1037
  # will prevent writes to the database for the duration of the block.
1038
+ #
1039
+ # This method does not provide the same protection as a readonly
1040
+ # user and is meant to be a safeguard against accidental writes.
1041
+ #
1042
+ # See `READ_QUERY` for the queries that are blocked by this
1043
+ # method.
1038
1044
  def while_preventing_writes(enabled = true)
1039
1045
  original, self.prevent_writes = self.prevent_writes, enabled
1040
1046
  yield
@@ -63,6 +63,10 @@ module ActiveRecord
63
63
  end
64
64
  CODE
65
65
  end
66
+
67
+ def aliased_types(name, fallback)
68
+ "timestamp" == name ? :datetime : fallback
69
+ end
66
70
  end
67
71
 
68
72
  AddColumnDefinition = Struct.new(:column) # :nodoc:
@@ -105,8 +109,9 @@ module ActiveRecord
105
109
  !ActiveRecord::SchemaDumper.fk_ignore_pattern.match?(name) if name
106
110
  end
107
111
 
108
- def defined_for?(to_table: nil, **options)
112
+ def defined_for?(to_table: nil, validate: nil, **options)
109
113
  (to_table.nil? || to_table.to_s == self.to_table) &&
114
+ (validate.nil? || validate == options.fetch(:validate, validate)) &&
110
115
  options.all? { |k, v| self.options[k].to_s == v.to_s }
111
116
  end
112
117
 
@@ -103,7 +103,11 @@ module ActiveRecord
103
103
  end
104
104
  end
105
105
 
106
+ DEFAULT_READ_QUERY = [:begin, :commit, :explain, :release, :rollback, :savepoint, :select, :with] # :nodoc:
107
+ private_constant :DEFAULT_READ_QUERY
108
+
106
109
  def self.build_read_query_regexp(*parts) # :nodoc:
110
+ parts += DEFAULT_READ_QUERY
107
111
  parts = parts.map { |part| /#{part}/i }
108
112
  /\A(?:[\(\s]|#{COMMENT_REGEX})*#{Regexp.union(*parts)}/
109
113
  end
@@ -168,6 +172,8 @@ module ActiveRecord
168
172
  spec_name = conn.pool.spec.name
169
173
  name = "#{spec_name}::SchemaMigration"
170
174
 
175
+ return ActiveRecord::SchemaMigration if spec_name == "primary"
176
+
171
177
  Class.new(ActiveRecord::SchemaMigration) do
172
178
  define_singleton_method(:name) { name }
173
179
  define_singleton_method(:to_s) { name }
@@ -675,9 +675,10 @@ module ActiveRecord
675
675
  end
676
676
 
677
677
  def add_index_for_alter(table_name, column_name, options = {})
678
- index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, **options)
678
+ index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, **options)
679
679
  index_algorithm[0, 0] = ", " if index_algorithm.present?
680
- "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"
680
+ sql = +"ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"
681
+ add_sql_comment!(sql, comment)
681
682
  end
682
683
 
683
684
  def remove_index_for_alter(table_name, options = {})
@@ -686,7 +687,11 @@ module ActiveRecord
686
687
  end
687
688
 
688
689
  def supports_rename_index?
689
- mariadb? ? false : database_version >= "5.7.6"
690
+ if mariadb?
691
+ database_version >= "10.5.2"
692
+ else
693
+ database_version >= "5.7.6"
694
+ end
690
695
  end
691
696
 
692
697
  def configure_connection
@@ -739,7 +744,7 @@ module ActiveRecord
739
744
  end.compact.join(", ")
740
745
 
741
746
  # ...and send them all in one query
742
- execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
747
+ execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}", "SCHEMA")
743
748
  end
744
749
 
745
750
  def column_definitions(table_name) # :nodoc:
@@ -20,7 +20,7 @@ module ActiveRecord
20
20
  end
21
21
 
22
22
  READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
23
- :begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback, :describe, :desc, :with
23
+ :desc, :describe, :set, :show, :use
24
24
  ) # :nodoc:
25
25
  private_constant :READ_QUERY
26
26
 
@@ -203,10 +203,14 @@ module ActiveRecord
203
203
  def data_source_sql(name = nil, type: nil)
204
204
  scope = quoted_scope(name, type: type)
205
205
 
206
- sql = +"SELECT table_name FROM information_schema.tables"
207
- sql << " WHERE table_schema = #{scope[:schema]}"
208
- sql << " AND table_name = #{scope[:name]}" if scope[:name]
209
- sql << " AND table_type = #{scope[:type]}" if scope[:type]
206
+ sql = +"SELECT table_name FROM (SELECT * FROM information_schema.tables "
207
+ sql << " WHERE table_schema = #{scope[:schema]}) _subquery"
208
+ if scope[:type] || scope[:name]
209
+ conditions = []
210
+ conditions << "_subquery.table_type = #{scope[:type]}" if scope[:type]
211
+ conditions << "_subquery.table_name = #{scope[:name]}" if scope[:name]
212
+ sql << " WHERE #{conditions.join(" AND ")}"
213
+ end
210
214
  sql
211
215
  end
212
216
 
@@ -68,7 +68,7 @@ module ActiveRecord
68
68
  end
69
69
 
70
70
  READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
71
- :begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback, :with
71
+ :close, :declare, :fetch, :move, :set, :show
72
72
  ) # :nodoc:
73
73
  private_constant :READ_QUERY
74
74
 
@@ -16,6 +16,14 @@ module ActiveRecord
16
16
  super
17
17
  end
18
18
  end
19
+
20
+ def type_cast_for_schema(value)
21
+ case value
22
+ when ::Float::INFINITY then "::Float::INFINITY"
23
+ when -::Float::INFINITY then "-::Float::INFINITY"
24
+ else super
25
+ end
26
+ end
19
27
  end
20
28
  end
21
29
  end
@@ -26,9 +26,9 @@ module ActiveRecord
26
26
 
27
27
  value = value.sub(/^\((.+)\)$/, '-\1') # (4)
28
28
  case value
29
- when /^-?\D*[\d,]+\.\d{2}$/ # (1)
29
+ when /^-?\D*+[\d,]+\.\d{2}$/ # (1)
30
30
  value.gsub!(/[^-\d.]/, "")
31
- when /^-?\D*[\d.]+,\d{2}$/ # (2)
31
+ when /^-?\D*+[\d.]+,\d{2}$/ # (2)
32
32
  value.gsub!(/[^-\d,]/, "").sub!(/,/, ".")
33
33
  end
34
34
 
@@ -67,15 +67,34 @@ module ActiveRecord
67
67
  end
68
68
 
69
69
  def extract_bounds(value)
70
- from, to = value[1..-2].split(",")
70
+ from, to = value[1..-2].split(",", 2)
71
71
  {
72
- from: (value[1] == "," || from == "-infinity") ? infinity(negative: true) : from,
73
- to: (value[-2] == "," || to == "infinity") ? infinity : to,
74
- exclude_start: (value[0] == "("),
75
- exclude_end: (value[-1] == ")")
72
+ from: (from == "" || from == "-infinity") ? infinity(negative: true) : unquote(from),
73
+ to: (to == "" || to == "infinity") ? infinity : unquote(to),
74
+ exclude_start: value.start_with?("("),
75
+ exclude_end: value.end_with?(")")
76
76
  }
77
77
  end
78
78
 
79
+ # When formatting the bound values of range types, PostgreSQL quotes
80
+ # the bound value using double-quotes in certain conditions. Within
81
+ # a double-quoted string, literal " and \ characters are themselves
82
+ # escaped. In input, PostgreSQL accepts multiple escape styles for "
83
+ # (either \" or "") but in output always uses "".
84
+ # See:
85
+ # * https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-IO
86
+ # * https://www.postgresql.org/docs/current/rowtypes.html#ROWTYPES-IO-SYNTAX
87
+ def unquote(value)
88
+ if value.start_with?('"') && value.end_with?('"')
89
+ unquoted_value = value[1..-2]
90
+ unquoted_value.gsub!('""', '"')
91
+ unquoted_value.gsub!('\\\\', '\\')
92
+ unquoted_value
93
+ else
94
+ value
95
+ end
96
+ end
97
+
79
98
  def infinity(negative: false)
80
99
  if subtype.respond_to?(:infinity)
81
100
  subtype.infinity(negative: negative)
@@ -5,7 +5,7 @@ module ActiveRecord
5
5
  module SQLite3
6
6
  module DatabaseStatements
7
7
  READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
8
- :begin, :commit, :explain, :select, :pragma, :release, :savepoint, :rollback, :with
8
+ :pragma
9
9
  ) # :nodoc:
10
10
  private_constant :READ_QUERY
11
11
 
@@ -61,7 +61,7 @@ module ActiveRecord
61
61
 
62
62
  def remove_foreign_key(from_table, to_table = nil, **options)
63
63
  to_table ||= options[:to_table]
64
- options = options.except(:name, :to_table)
64
+ options = options.except(:name, :to_table, :validate)
65
65
  foreign_keys = foreign_keys(from_table)
66
66
 
67
67
  fkey = foreign_keys.detect do |fk|
@@ -283,13 +283,14 @@ module ActiveRecord
283
283
  def change_column(table_name, column_name, type, options = {}) #:nodoc:
284
284
  alter_table(table_name) do |definition|
285
285
  definition[column_name].instance_eval do
286
- self.type = type
286
+ self.type = aliased_types(type.to_s, type)
287
287
  self.limit = options[:limit] if options.include?(:limit)
288
288
  self.default = options[:default] if options.include?(:default)
289
289
  self.null = options[:null] if options.include?(:null)
290
290
  self.precision = options[:precision] if options.include?(:precision)
291
291
  self.scale = options[:scale] if options.include?(:scale)
292
292
  self.collation = options[:collation] if options.include?(:collation)
293
+ self.options.merge!(options)
293
294
  end
294
295
  end
295
296
  end
@@ -285,18 +285,17 @@ module ActiveRecord
285
285
  false
286
286
  end
287
287
 
288
- private
289
- def cached_find_by_statement(key, &block)
290
- cache = @find_by_statement_cache[connection.prepared_statements]
291
- cache.compute_if_absent(key) { StatementCache.create(connection, &block) }
292
- end
288
+ def cached_find_by_statement(key, &block) # :nodoc:
289
+ cache = @find_by_statement_cache[connection.prepared_statements]
290
+ cache.compute_if_absent(key) { StatementCache.create(connection, &block) }
291
+ end
293
292
 
293
+ private
294
294
  def relation
295
295
  relation = Relation.create(self)
296
296
 
297
297
  if finder_needs_type_condition? && !ignore_default_scope?
298
298
  relation.where!(type_condition)
299
- relation.create_with!(inheritance_column.to_s => sti_name)
300
299
  else
301
300
  relation
302
301
  end
@@ -173,6 +173,7 @@ module ActiveRecord
173
173
 
174
174
  _enum_methods_module.module_eval do
175
175
  pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
176
+ value_method_names = []
176
177
  pairs.each do |label, value|
177
178
  if enum_prefix == true
178
179
  prefix = "#{name}_"
@@ -186,6 +187,7 @@ module ActiveRecord
186
187
  end
187
188
 
188
189
  value_method_name = "#{prefix}#{label}#{suffix}"
190
+ value_method_names << value_method_name
189
191
  enum_values[label] = value
190
192
  label = label.to_s
191
193
 
@@ -200,8 +202,6 @@ module ActiveRecord
200
202
  # scope :active, -> { where(status: 0) }
201
203
  # scope :not_active, -> { where.not(status: 0) }
202
204
  if enum_scopes != false
203
- klass.send(:detect_negative_condition!, value_method_name)
204
-
205
205
  klass.send(:detect_enum_conflict!, name, value_method_name, true)
206
206
  klass.scope value_method_name, -> { where(attr => value) }
207
207
 
@@ -209,6 +209,7 @@ module ActiveRecord
209
209
  klass.scope "not_#{value_method_name}", -> { where.not(attr => value) }
210
210
  end
211
211
  end
212
+ klass.send(:detect_negative_enum_conditions!, value_method_names) if enum_scopes != false
212
213
  end
213
214
  enum_values.freeze
214
215
  end
@@ -264,10 +265,16 @@ module ActiveRecord
264
265
  }
265
266
  end
266
267
 
267
- def detect_negative_condition!(method_name)
268
- if method_name.start_with?("not_") && logger
269
- logger.warn "An enum element in #{self.name} uses the prefix 'not_'." \
270
- " This will cause a conflict with auto generated negative scopes."
268
+ def detect_negative_enum_conditions!(method_names)
269
+ return unless logger
270
+
271
+ method_names.select { |m| m.start_with?("not_") }.each do |potential_not|
272
+ inverted_form = potential_not.sub("not_", "")
273
+ if method_names.include?(inverted_form)
274
+ logger.warn "Enum element '#{potential_not}' in #{self.name} uses the prefix 'not_'." \
275
+ " This has caused a conflict with auto generated negative scopes." \
276
+ " Avoid using enum elements starting with 'not' where the positive form is also an element."
277
+ end
271
278
  end
272
279
  end
273
280
  end
@@ -10,7 +10,7 @@ class ActiveRecord::FixtureSet::RenderContext # :nodoc:
10
10
  end
11
11
 
12
12
  def binary(path)
13
- %(!!binary "#{Base64.strict_encode64(File.read(path))}")
13
+ %(!!binary "#{Base64.strict_encode64(File.read(path, mode: 'rb'))}")
14
14
  end
15
15
  end
16
16
  end
@@ -9,8 +9,8 @@ module ActiveRecord
9
9
  module VERSION
10
10
  MAJOR = 6
11
11
  MINOR = 0
12
- TINY = 3
13
- PRE = "3"
12
+ TINY = 4
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -168,7 +168,7 @@ module ActiveRecord
168
168
  end
169
169
 
170
170
  def format_columns(columns)
171
- quote_columns(columns).join(",")
171
+ columns.respond_to?(:map) ? quote_columns(columns).join(",") : columns
172
172
  end
173
173
 
174
174
  def quote_columns(columns)
@@ -60,6 +60,15 @@ module ActiveRecord
60
60
  self.class.locking_enabled?
61
61
  end
62
62
 
63
+ def increment!(*, **) #:nodoc:
64
+ super.tap do
65
+ if locking_enabled?
66
+ self[self.class.locking_column] += 1
67
+ clear_attribute_change(self.class.locking_column)
68
+ end
69
+ end
70
+ end
71
+
63
72
  private
64
73
  def _create_record(attribute_names = self.attribute_names)
65
74
  if locking_enabled?
@@ -287,6 +287,35 @@ module ActiveRecord
287
287
 
288
288
  # Sets the columns names the model should ignore. Ignored columns won't have attribute
289
289
  # accessors defined, and won't be referenced in SQL queries.
290
+ #
291
+ # A common usage pattern for this method is to ensure all references to an attribute
292
+ # have been removed and deployed, before a migration to drop the column from the database
293
+ # has been deployed and run. Using this two step approach to dropping columns ensures there
294
+ # is no code that raises errors due to having a cached schema in memory at the time the
295
+ # schema migration is run.
296
+ #
297
+ # For example, given a model where you want to drop the "category" attribute, first mark it
298
+ # as ignored:
299
+ #
300
+ # class Project < ActiveRecord::Base
301
+ # # schema:
302
+ # # id :bigint
303
+ # # name :string, limit: 255
304
+ # # category :string, limit: 255
305
+ #
306
+ # self.ignored_columns = [:category]
307
+ # end
308
+ #
309
+ # The schema still contains `category`, but now the model omits it, so any meta-driven code or
310
+ # schema caching will not attempt to use the column:
311
+ #
312
+ # Project.columns_hash["category"] => nil
313
+ #
314
+ # You will get an error if accessing that attribute directly, so ensure all usages of the
315
+ # column are removed (automated tests can help you find any usages).
316
+ #
317
+ # user = Project.create!(name: "First Project")
318
+ # user.category # => raises NoMethodError
290
319
  def ignored_columns=(columns)
291
320
  @ignored_columns = columns.map(&:to_s)
292
321
  end