activerecord 5.2.0.rc1 → 5.2.0.rc2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -16
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/associations/association.rb +4 -4
  5. data/lib/active_record/associations/association_scope.rb +6 -6
  6. data/lib/active_record/associations/belongs_to_association.rb +9 -1
  7. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +1 -1
  8. data/lib/active_record/associations/has_one_association.rb +1 -0
  9. data/lib/active_record/associations/join_dependency.rb +2 -4
  10. data/lib/active_record/associations/preloader/association.rb +1 -1
  11. data/lib/active_record/attribute_assignment.rb +0 -5
  12. data/lib/active_record/attribute_methods.rb +19 -14
  13. data/lib/active_record/attribute_methods/dirty.rb +4 -2
  14. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  15. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +1 -1
  16. data/lib/active_record/connection_adapters/abstract_adapter.rb +2 -2
  17. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +1 -1
  18. data/lib/active_record/connection_adapters/postgresql/oid.rb +1 -0
  19. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +23 -0
  20. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +2 -2
  21. data/lib/active_record/connection_adapters/postgresql_adapter.rb +2 -1
  22. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +1 -1
  23. data/lib/active_record/gem_version.rb +1 -1
  24. data/lib/active_record/inheritance.rb +40 -10
  25. data/lib/active_record/locking/optimistic.rb +27 -41
  26. data/lib/active_record/migration.rb +8 -2
  27. data/lib/active_record/model_schema.rb +1 -0
  28. data/lib/active_record/persistence.rb +60 -51
  29. data/lib/active_record/query_cache.rb +6 -6
  30. data/lib/active_record/railties/databases.rake +1 -1
  31. data/lib/active_record/reflection.rb +7 -4
  32. data/lib/active_record/relation/calculations.rb +1 -1
  33. data/lib/active_record/relation/finder_methods.rb +3 -3
  34. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -5
  35. data/lib/active_record/relation/predicate_builder/range_handler.rb +4 -3
  36. data/lib/active_record/relation/query_attribute.rb +17 -0
  37. data/lib/active_record/validations/uniqueness.rb +1 -1
  38. metadata +10 -9
@@ -119,11 +119,11 @@ module ActiveRecord
119
119
  end
120
120
  end
121
121
 
122
- def migrations_paths
122
+ def migrations_paths # :nodoc:
123
123
  @config[:migrations_paths] || Migrator.migrations_paths
124
124
  end
125
125
 
126
- def migration_context
126
+ def migration_context # :nodoc:
127
127
  MigrationContext.new(migrations_paths)
128
128
  end
129
129
 
@@ -515,7 +515,7 @@ module ActiveRecord
515
515
  s.gsub(/\s+(?:ASC|DESC)\b/i, "")
516
516
  }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
517
517
 
518
- [super, *order_columns].join(", ")
518
+ (order_columns << super).join(", ")
519
519
  end
520
520
 
521
521
  def strict_mode?
@@ -5,6 +5,7 @@ require "active_record/connection_adapters/postgresql/oid/bit"
5
5
  require "active_record/connection_adapters/postgresql/oid/bit_varying"
6
6
  require "active_record/connection_adapters/postgresql/oid/bytea"
7
7
  require "active_record/connection_adapters/postgresql/oid/cidr"
8
+ require "active_record/connection_adapters/postgresql/oid/date"
8
9
  require "active_record/connection_adapters/postgresql/oid/date_time"
9
10
  require "active_record/connection_adapters/postgresql/oid/decimal"
10
11
  require "active_record/connection_adapters/postgresql/oid/enum"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module PostgreSQL
6
+ module OID # :nodoc:
7
+ class Date < Type::Date # :nodoc:
8
+ def cast_value(value)
9
+ case value
10
+ when "infinity" then ::Float::INFINITY
11
+ when "-infinity" then -::Float::INFINITY
12
+ when / BC$/
13
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
14
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
15
+ else
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -107,7 +107,7 @@ module ActiveRecord
107
107
  oid = row[4]
108
108
  comment = row[5]
109
109
 
110
- using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten
110
+ using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten
111
111
 
112
112
  orders = {}
113
113
  opclasses = {}
@@ -583,7 +583,7 @@ module ActiveRecord
583
583
  .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
584
584
  }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
585
585
 
586
- [super, *order_columns].join(", ")
586
+ (order_columns << super).join(", ")
587
587
  end
588
588
 
589
589
  def update_table_definition(table_name, base) # :nodoc:
@@ -465,7 +465,7 @@ module ActiveRecord
465
465
  register_class_with_limit m, "bit", OID::Bit
466
466
  register_class_with_limit m, "varbit", OID::BitVarying
467
467
  m.alias_type "timestamptz", "timestamp"
468
- m.register_type "date", Type::Date.new
468
+ m.register_type "date", OID::Date.new
469
469
 
470
470
  m.register_type "money", OID::Money.new
471
471
  m.register_type "bytea", OID::Bytea.new
@@ -837,6 +837,7 @@ module ActiveRecord
837
837
  ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql)
838
838
  ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql)
839
839
  ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql)
840
+ ActiveRecord::Type.register(:date, OID::Date, adapter: :postgresql)
840
841
  ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgresql)
841
842
  ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql)
842
843
  ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql)
@@ -17,7 +17,7 @@ module ActiveRecord
17
17
  end
18
18
 
19
19
  def quoted_time(value)
20
- quoted_date(value)
20
+ quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ")
21
21
  end
22
22
 
23
23
  def quoted_binary(value)
@@ -10,7 +10,7 @@ module ActiveRecord
10
10
  MAJOR = 5
11
11
  MINOR = 2
12
12
  TINY = 0
13
- PRE = "rc1"
13
+ PRE = "rc2"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -104,21 +104,47 @@ module ActiveRecord
104
104
  end
105
105
  end
106
106
 
107
- # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
108
- # If you are using inheritance with ActiveRecord and don't want child classes
109
- # to utilize the implied STI table name of the parent class, this will need to be true.
110
- # For example, given the following:
107
+ # Set this to +true+ if this is an abstract class (see
108
+ # <tt>abstract_class?</tt>).
109
+ # If you are using inheritance with Active Record and don't want a class
110
+ # to be considered as part of the STI hierarchy, you must set this to
111
+ # true.
112
+ # +ApplicationRecord+, for example, is generated as an abstract class.
111
113
  #
112
- # class SuperClass < ActiveRecord::Base
114
+ # Consider the following default behaviour:
115
+ #
116
+ # Shape = Class.new(ActiveRecord::Base)
117
+ # Polygon = Class.new(Shape)
118
+ # Square = Class.new(Polygon)
119
+ #
120
+ # Shape.table_name # => "shapes"
121
+ # Polygon.table_name # => "shapes"
122
+ # Square.table_name # => "shapes"
123
+ # Shape.create! # => #<Shape id: 1, type: nil>
124
+ # Polygon.create! # => #<Polygon id: 2, type: "Polygon">
125
+ # Square.create! # => #<Square id: 3, type: "Square">
126
+ #
127
+ # However, when using <tt>abstract_class</tt>, +Shape+ is omitted from
128
+ # the hierarchy:
129
+ #
130
+ # class Shape < ActiveRecord::Base
113
131
  # self.abstract_class = true
114
132
  # end
115
- # class Child < SuperClass
116
- # self.table_name = 'the_table_i_really_want'
117
- # end
118
- #
133
+ # Polygon = Class.new(Shape)
134
+ # Square = Class.new(Polygon)
119
135
  #
120
- # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt>
136
+ # Shape.table_name # => nil
137
+ # Polygon.table_name # => "polygons"
138
+ # Square.table_name # => "polygons"
139
+ # Shape.create! # => NotImplementedError: Shape is an abstract class and cannot be instantiated.
140
+ # Polygon.create! # => #<Polygon id: 1, type: nil>
141
+ # Square.create! # => #<Square id: 2, type: "Square">
121
142
  #
143
+ # Note that in the above example, to disallow the creation of a plain
144
+ # +Polygon+, you should use <tt>validates :type, presence: true</tt>,
145
+ # instead of setting it as an abstract class. This way, +Polygon+ will
146
+ # stay in the hierarchy, and Active Record will continue to correctly
147
+ # derive the table name.
122
148
  attr_accessor :abstract_class
123
149
 
124
150
  # Returns whether this class is an abstract class or not.
@@ -130,6 +156,10 @@ module ActiveRecord
130
156
  store_full_sti_class ? name : name.demodulize
131
157
  end
132
158
 
159
+ def polymorphic_name
160
+ base_class.name
161
+ end
162
+
133
163
  def inherited(subclass)
134
164
  subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new)
135
165
  super
@@ -61,13 +61,6 @@ module ActiveRecord
61
61
  end
62
62
 
63
63
  private
64
-
65
- def increment_lock
66
- lock_col = self.class.locking_column
67
- previous_lock_value = send(lock_col)
68
- send("#{lock_col}=", previous_lock_value + 1)
69
- end
70
-
71
64
  def _create_record(attribute_names = self.attribute_names, *)
72
65
  if locking_enabled?
73
66
  # We always want to persist the locking version, even if we don't detect
@@ -77,63 +70,56 @@ module ActiveRecord
77
70
  super
78
71
  end
79
72
 
80
- def _update_record(attribute_names = self.attribute_names)
73
+ def _touch_row(attribute_names, time)
74
+ super
75
+ ensure
76
+ clear_attribute_change(self.class.locking_column) if locking_enabled?
77
+ end
78
+
79
+ def _update_row(attribute_names, attempted_action = "update")
81
80
  return super unless locking_enabled?
82
- return 0 if attribute_names.empty?
83
81
 
84
82
  begin
85
- lock_col = self.class.locking_column
86
-
87
- previous_lock_value = read_attribute_before_type_cast(lock_col)
88
-
89
- increment_lock
90
-
91
- attribute_names.push(lock_col)
83
+ locking_column = self.class.locking_column
84
+ previous_lock_value = read_attribute_before_type_cast(locking_column)
85
+ attribute_names << locking_column
92
86
 
93
- relation = self.class.unscoped
87
+ self[locking_column] += 1
94
88
 
95
- affected_rows = relation.where(
96
- self.class.primary_key => id,
97
- lock_col => previous_lock_value
98
- ).update_all(
99
- attributes_for_update(attribute_names).map do |name|
100
- [name, _read_attribute(name)]
101
- end.to_h
89
+ affected_rows = self.class._update_record(
90
+ attributes_with_values(attribute_names),
91
+ self.class.primary_key => id_in_database,
92
+ locking_column => previous_lock_value
102
93
  )
103
94
 
104
- unless affected_rows == 1
105
- raise ActiveRecord::StaleObjectError.new(self, "update")
95
+ if affected_rows != 1
96
+ raise ActiveRecord::StaleObjectError.new(self, attempted_action)
106
97
  end
107
98
 
108
99
  affected_rows
109
100
 
110
101
  # If something went wrong, revert the locking_column value.
111
102
  rescue Exception
112
- send("#{lock_col}=", previous_lock_value.to_i)
113
-
103
+ self[locking_column] = previous_lock_value.to_i
114
104
  raise
115
105
  end
116
106
  end
117
107
 
118
108
  def destroy_row
119
- affected_rows = super
120
-
121
- if locking_enabled? && affected_rows != 1
122
- raise ActiveRecord::StaleObjectError.new(self, "destroy")
123
- end
109
+ return super unless locking_enabled?
124
110
 
125
- affected_rows
126
- end
111
+ locking_column = self.class.locking_column
127
112
 
128
- def relation_for_destroy
129
- relation = super
113
+ affected_rows = self.class._delete_record(
114
+ self.class.primary_key => id_in_database,
115
+ locking_column => read_attribute_before_type_cast(locking_column)
116
+ )
130
117
 
131
- if locking_enabled?
132
- locking_column = self.class.locking_column
133
- relation = relation.where(locking_column => read_attribute_before_type_cast(locking_column))
118
+ if affected_rows != 1
119
+ raise ActiveRecord::StaleObjectError.new(self, "destroy")
134
120
  end
135
121
 
136
- relation
122
+ affected_rows
137
123
  end
138
124
 
139
125
  module ClassMethods
@@ -140,6 +140,7 @@ module ActiveRecord
140
140
 
141
141
  class ConcurrentMigrationError < MigrationError #:nodoc:
142
142
  DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze
143
+ RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock".freeze
143
144
 
144
145
  def initialize(message = DEFAULT_MESSAGE)
145
146
  super
@@ -1355,12 +1356,17 @@ module ActiveRecord
1355
1356
 
1356
1357
  def with_advisory_lock
1357
1358
  lock_id = generate_migrator_advisory_lock_id
1358
- got_lock = Base.connection.get_advisory_lock(lock_id)
1359
+ connection = Base.connection
1360
+ got_lock = connection.get_advisory_lock(lock_id)
1359
1361
  raise ConcurrentMigrationError unless got_lock
1360
1362
  load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
1361
1363
  yield
1362
1364
  ensure
1363
- Base.connection.release_advisory_lock(lock_id) if got_lock
1365
+ if got_lock && !connection.release_advisory_lock(lock_id)
1366
+ raise ConcurrentMigrationError.new(
1367
+ ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
1368
+ )
1369
+ end
1364
1370
  end
1365
1371
 
1366
1372
  MIGRATOR_SALT = 2053462845
@@ -379,6 +379,7 @@ module ActiveRecord
379
379
  end
380
380
 
381
381
  def _default_attributes # :nodoc:
382
+ load_schema
382
383
  @default_attributes ||= ActiveModel::AttributeSet.new({})
383
384
  end
384
385
 
@@ -169,12 +169,11 @@ module ActiveRecord
169
169
  primary_key_value = nil
170
170
 
171
171
  if primary_key && Hash === values
172
- arel_primary_key = arel_attribute(primary_key)
173
- primary_key_value = values[arel_primary_key]
172
+ primary_key_value = values[primary_key]
174
173
 
175
174
  if !primary_key_value && prefetch_primary_key?
176
175
  primary_key_value = next_sequence_value
177
- values[arel_primary_key] = primary_key_value
176
+ values[primary_key] = primary_key_value
178
177
  end
179
178
  end
180
179
 
@@ -188,15 +187,26 @@ module ActiveRecord
188
187
  connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
189
188
  end
190
189
 
191
- def _update_record(values, id, id_was) # :nodoc:
192
- bind = predicate_builder.build_bind_attribute(primary_key, id_was || id)
190
+ def _update_record(values, constraints) # :nodoc:
191
+ constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
192
+
193
193
  um = arel_table.where(
194
- arel_attribute(primary_key).eq(bind)
194
+ constraints.reduce(&:and)
195
195
  ).compile_update(_substitute_values(values), primary_key)
196
196
 
197
197
  connection.update(um, "#{self} Update")
198
198
  end
199
199
 
200
+ def _delete_record(constraints) # :nodoc:
201
+ constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
202
+
203
+ dm = Arel::DeleteManager.new
204
+ dm.from(arel_table)
205
+ dm.wheres = constraints
206
+
207
+ connection.delete(dm, "#{self} Destroy")
208
+ end
209
+
200
210
  private
201
211
  # Called by +instantiate+ to decide which class to use for a new
202
212
  # record instance.
@@ -208,8 +218,9 @@ module ActiveRecord
208
218
  end
209
219
 
210
220
  def _substitute_values(values)
211
- values.map do |attr, value|
212
- bind = predicate_builder.build_bind_attribute(attr.name, value)
221
+ values.map do |name, value|
222
+ attr = arel_attribute(name)
223
+ bind = predicate_builder.build_bind_attribute(name, value)
213
224
  [attr, bind]
214
225
  end
215
226
  end
@@ -310,7 +321,7 @@ module ActiveRecord
310
321
  # callbacks or any <tt>:dependent</tt> association
311
322
  # options, use <tt>#destroy</tt>.
312
323
  def delete
313
- _relation_for_itself.delete_all if persisted?
324
+ _delete_row if persisted?
314
325
  @destroyed = true
315
326
  freeze
316
327
  end
@@ -363,6 +374,7 @@ module ActiveRecord
363
374
  became.send(:initialize)
364
375
  became.instance_variable_set("@attributes", @attributes)
365
376
  became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database)
377
+ became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
366
378
  became.instance_variable_set("@new_record", new_record?)
367
379
  became.instance_variable_set("@destroyed", destroyed?)
368
380
  became.errors.copy!(errors)
@@ -461,13 +473,16 @@ module ActiveRecord
461
473
  verify_readonly_attribute(key.to_s)
462
474
  end
463
475
 
464
- updated_count = _relation_for_itself.update_all(attributes)
476
+ affected_rows = self.class._update_record(
477
+ attributes,
478
+ self.class.primary_key => id_in_database
479
+ )
465
480
 
466
481
  attributes.each do |k, v|
467
482
  write_attribute_without_type_cast(k, v)
468
483
  end
469
484
 
470
- updated_count == 1
485
+ affected_rows == 1
471
486
  end
472
487
 
473
488
  # Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1).
@@ -640,35 +655,12 @@ module ActiveRecord
640
655
  MSG
641
656
  end
642
657
 
643
- time ||= current_time_from_proper_timezone
644
- attributes = timestamp_attributes_for_update_in_model
645
- attributes.concat(names)
646
-
647
- unless attributes.empty?
648
- changes = {}
649
-
650
- attributes.each do |column|
651
- column = column.to_s
652
- changes[column] = write_attribute(column, time)
653
- end
654
-
655
- scope = _relation_for_itself
656
-
657
- if locking_enabled?
658
- locking_column = self.class.locking_column
659
- scope = scope.where(locking_column => read_attribute_before_type_cast(locking_column))
660
- changes[locking_column] = increment_lock
661
- end
662
-
663
- clear_attribute_changes(changes.keys)
664
- result = scope.update_all(changes) == 1
665
-
666
- if !result && locking_enabled?
667
- raise ActiveRecord::StaleObjectError.new(self, "touch")
668
- end
658
+ attribute_names = timestamp_attributes_for_update_in_model
659
+ attribute_names.concat(names)
669
660
 
670
- @_trigger_update_callback = result
671
- result
661
+ unless attribute_names.empty?
662
+ affected_rows = _touch_row(attribute_names, time)
663
+ @_trigger_update_callback = affected_rows == 1
672
664
  else
673
665
  true
674
666
  end
@@ -681,15 +673,29 @@ module ActiveRecord
681
673
  end
682
674
 
683
675
  def destroy_row
684
- relation_for_destroy.delete_all
676
+ _delete_row
685
677
  end
686
678
 
687
- def relation_for_destroy
688
- _relation_for_itself
679
+ def _delete_row
680
+ self.class._delete_record(self.class.primary_key => id_in_database)
689
681
  end
690
682
 
691
- def _relation_for_itself
692
- self.class.unscoped.where(self.class.primary_key => id)
683
+ def _touch_row(attribute_names, time)
684
+ time ||= current_time_from_proper_timezone
685
+
686
+ attribute_names.each do |attr_name|
687
+ write_attribute(attr_name, time)
688
+ clear_attribute_change(attr_name)
689
+ end
690
+
691
+ _update_row(attribute_names, "touch")
692
+ end
693
+
694
+ def _update_row(attribute_names, attempted_action = "update")
695
+ self.class._update_record(
696
+ attributes_with_values(attribute_names),
697
+ self.class.primary_key => id_in_database
698
+ )
693
699
  end
694
700
 
695
701
  def create_or_update(*args, &block)
@@ -702,24 +708,27 @@ module ActiveRecord
702
708
  # Updates the associated record with values matching those of the instance attributes.
703
709
  # Returns the number of affected rows.
704
710
  def _update_record(attribute_names = self.attribute_names)
705
- attributes_values = arel_attributes_with_values_for_update(attribute_names)
706
- if attributes_values.empty?
707
- rows_affected = 0
711
+ attribute_names &= self.class.column_names
712
+ attribute_names = attributes_for_update(attribute_names)
713
+
714
+ if attribute_names.empty?
715
+ affected_rows = 0
708
716
  @_trigger_update_callback = true
709
717
  else
710
- rows_affected = self.class._update_record(attributes_values, id, id_in_database)
711
- @_trigger_update_callback = rows_affected > 0
718
+ affected_rows = _update_row(attribute_names)
719
+ @_trigger_update_callback = affected_rows == 1
712
720
  end
713
721
 
714
722
  yield(self) if block_given?
715
723
 
716
- rows_affected
724
+ affected_rows
717
725
  end
718
726
 
719
727
  # Creates a record with values matching those of the instance attributes
720
728
  # and returns its id.
721
729
  def _create_record(attribute_names = self.attribute_names)
722
- attributes_values = arel_attributes_with_values_for_create(attribute_names)
730
+ attribute_names &= self.class.column_names
731
+ attributes_values = attributes_with_values_for_create(attribute_names)
723
732
 
724
733
  new_id = self.class._insert_record(attributes_values)
725
734
  self.id ||= new_id if self.class.primary_key