activerecord 8.1.3.1 → 8.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +617 -0
  3. data/lib/active_record/associations/association.rb +34 -16
  4. data/lib/active_record/associations/association_scope.rb +1 -1
  5. data/lib/active_record/associations/belongs_to_association.rb +2 -2
  6. data/lib/active_record/associations/builder/belongs_to.rb +19 -4
  7. data/lib/active_record/associations/collection_association.rb +6 -2
  8. data/lib/active_record/associations/collection_proxy.rb +4 -0
  9. data/lib/active_record/associations/foreign_association.rb +3 -1
  10. data/lib/active_record/associations/has_one_association.rb +1 -0
  11. data/lib/active_record/associations/join_dependency.rb +1 -2
  12. data/lib/active_record/associations/preloader/association.rb +1 -1
  13. data/lib/active_record/attribute_methods/serialization.rb +2 -2
  14. data/lib/active_record/attribute_methods.rb +6 -4
  15. data/lib/active_record/callbacks.rb +1 -2
  16. data/lib/active_record/coders/column_serializer.rb +1 -1
  17. data/lib/active_record/coders/json.rb +7 -6
  18. data/lib/active_record/coders/yaml_column.rb +2 -2
  19. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +17 -0
  20. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +32 -20
  21. data/lib/active_record/connection_adapters/abstract/database_statements.rb +14 -2
  22. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +2 -2
  23. data/lib/active_record/connection_adapters/abstract_adapter.rb +12 -0
  24. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +79 -23
  25. data/lib/active_record/connection_adapters/column.rb +2 -5
  26. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +7 -21
  27. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +3 -16
  28. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +46 -3
  29. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
  30. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  31. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +17 -5
  32. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  33. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +2 -2
  34. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +4 -2
  35. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -0
  36. data/lib/active_record/connection_adapters/trilogy_adapter.rb +14 -0
  37. data/lib/active_record/counter_cache.rb +8 -6
  38. data/lib/active_record/database_configurations/hash_config.rb +9 -2
  39. data/lib/active_record/database_configurations/url_config.rb +2 -0
  40. data/lib/active_record/disable_joins_association_relation.rb +5 -1
  41. data/lib/active_record/encryption/encrypted_fixtures.rb +12 -7
  42. data/lib/active_record/filter_attribute_handler.rb +6 -3
  43. data/lib/active_record/gem_version.rb +2 -2
  44. data/lib/active_record/inheritance.rb +8 -3
  45. data/lib/active_record/log_subscriber.rb +28 -16
  46. data/lib/active_record/message_pack.rb +13 -0
  47. data/lib/active_record/migration/command_recorder.rb +12 -2
  48. data/lib/active_record/model_schema.rb +2 -2
  49. data/lib/active_record/nested_attributes.rb +8 -6
  50. data/lib/active_record/persistence.rb +1 -0
  51. data/lib/active_record/querying.rb +13 -1
  52. data/lib/active_record/reflection.rb +1 -1
  53. data/lib/active_record/relation/calculations.rb +8 -3
  54. data/lib/active_record/relation/finder_methods.rb +34 -20
  55. data/lib/active_record/relation/merger.rb +4 -2
  56. data/lib/active_record/relation/predicate_builder.rb +1 -1
  57. data/lib/active_record/relation/query_methods.rb +63 -13
  58. data/lib/active_record/relation.rb +20 -12
  59. data/lib/active_record/schema.rb +1 -1
  60. data/lib/active_record/schema_dumper.rb +1 -1
  61. data/lib/active_record/signed_id.rb +1 -1
  62. data/lib/active_record/statement_cache.rb +1 -1
  63. data/lib/active_record/store.rb +4 -2
  64. data/lib/active_record/tasks/abstract_tasks.rb +8 -1
  65. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -16
  66. data/lib/active_record/test_databases.rb +4 -0
  67. data/lib/active_record/test_fixtures.rb +13 -4
  68. data/lib/active_record/transactions.rb +19 -16
  69. data/lib/active_record/type_caster/connection.rb +1 -1
  70. data/lib/active_record.rb +1 -1
  71. data/lib/arel/visitors/to_sql.rb +1 -1
  72. metadata +9 -9
@@ -188,21 +188,7 @@ module ActiveRecord
188
188
  MySQL::TableDefinition.new(self, name, **options)
189
189
  end
190
190
 
191
- def default_type(table_name, field_name)
192
- match = create_table_info(table_name)&.match(/`#{field_name}` (.+) DEFAULT ('|\d+|[A-z]+)/)
193
- default_pre = match[2] if match
194
-
195
- if default_pre == "'"
196
- :string
197
- elsif default_pre&.match?(/^\d+$/)
198
- :integer
199
- elsif default_pre&.match?(/^[A-z]+$/)
200
- :function
201
- end
202
- end
203
-
204
191
  def new_column_from_field(table_name, field, _definitions)
205
- field_name = field.fetch("Field")
206
192
  type_metadata = fetch_type_metadata(field["Type"], field["Extra"])
207
193
  default, default_function = field["Default"], nil
208
194
 
@@ -210,16 +196,16 @@ module ActiveRecord
210
196
  default = "#{default} ON UPDATE #{default}" if /on update CURRENT_TIMESTAMP/i.match?(field["Extra"])
211
197
  default, default_function = nil, default
212
198
  elsif type_metadata.extra == "DEFAULT_GENERATED"
213
- default = +"(#{default})" unless default.start_with?("(")
214
- default = default.gsub("\\'", "'")
215
- default, default_function = nil, default
199
+ if mariadb?
200
+ default, default_function = nil, default
201
+ else
202
+ default = "(#{default})" unless default.start_with?("(")
203
+ default = default.gsub("\\'", "'")
204
+ default, default_function = nil, default
205
+ end
216
206
  elsif type_metadata.type == :text && default&.start_with?("'")
217
207
  # strip and unescape quotes
218
208
  default = default[1...-1].gsub("\\'", "'")
219
- elsif default&.match?(/\A\d/)
220
- # Its a number so we can skip the query to check if it is a function
221
- elsif default && default_type(table_name, field_name) == :function
222
- default, default_function = nil, default
223
209
  end
224
210
 
225
211
  MySQL::Column.new(
@@ -27,21 +27,6 @@ module ActiveRecord
27
27
  !READ_QUERY.match?(sql.b)
28
28
  end
29
29
 
30
- # Executes an SQL statement, returning a PG::Result object on success
31
- # or raising a PG::Error exception otherwise.
32
- #
33
- # Setting +allow_retry+ to true causes the db to reconnect and retry
34
- # executing the SQL statement in case of a connection-related exception.
35
- # This option should only be enabled for known idempotent queries.
36
- #
37
- # Note: the PG::Result object is manually memory managed; if you don't
38
- # need it specifically, you may want consider the <tt>exec_query</tt> wrapper.
39
- def execute(...) # :nodoc:
40
- super
41
- ensure
42
- @notice_receiver_sql_warnings = []
43
- end
44
-
45
30
  def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil) # :nodoc:
46
31
  if use_insert_returning? || pk == false
47
32
  super
@@ -223,7 +208,9 @@ module ActiveRecord
223
208
  end
224
209
 
225
210
  def handle_warnings(result, sql)
226
- @notice_receiver_sql_warnings.each do |warning|
211
+ warnings, @notice_receiver_sql_warnings = @notice_receiver_sql_warnings, []
212
+
213
+ warnings.each do |warning|
227
214
  next if warning_ignored?(warning)
228
215
 
229
216
  warning.sql = sql
@@ -14,7 +14,10 @@ module ActiveRecord
14
14
  end
15
15
 
16
16
  def type_cast_for_schema(value)
17
- value.inspect.gsub("Infinity", "::Float::INFINITY")
17
+ from = bound_for_schema(value.begin)
18
+ to = bound_for_schema(value.end)
19
+ op = value.exclude_end? ? "..." : ".."
20
+ "#{from}#{op}#{to}"
18
21
  end
19
22
 
20
23
  def cast_value(value)
@@ -58,6 +61,19 @@ module ActiveRecord
58
61
  end
59
62
 
60
63
  private
64
+ def bound_for_schema(bound)
65
+ case bound
66
+ when nil
67
+ "nil"
68
+ when ::Float::INFINITY
69
+ "::Float::INFINITY"
70
+ when -::Float::INFINITY
71
+ "-::Float::INFINITY"
72
+ else
73
+ @subtype.type_cast_for_schema(bound)
74
+ end
75
+ end
76
+
61
77
  def type_cast_single(value)
62
78
  infinity?(value) ? value : @subtype.deserialize(value)
63
79
  end
@@ -67,7 +83,7 @@ module ActiveRecord
67
83
  end
68
84
 
69
85
  def extract_bounds(value)
70
- from, to = value[1..-2].split(",", 2)
86
+ from, to = split_bounds(value[1..-2])
71
87
  {
72
88
  from: (from == "" || from == "-infinity") ? infinity(negative: true) : unquote(from),
73
89
  to: (to == "" || to == "infinity") ? infinity : unquote(to),
@@ -76,6 +92,33 @@ module ActiveRecord
76
92
  }
77
93
  end
78
94
 
95
+ # Matches the comma-separated lower and upper bounds of a range's
96
+ # textual representation when a bound is double-quoted. A quoted
97
+ # bound can itself contain a comma, so a naive split on the first
98
+ # comma would corrupt such values. Within a double-quoted bound,
99
+ # literal " and \ are escaped (as "" / \" and \\ respectively), so
100
+ # those escapes are skipped while scanning for the separating comma.
101
+ #
102
+ # An unquoted bound never contains a comma or a double-quote
103
+ # (PostgreSQL quotes the whole bound when it would), so the
104
+ # alternation matches an unquoted run with [^,"] rather than [^,].
105
+ # Excluding " keeps the two alternatives mutually exclusive, which
106
+ # removes any ambiguity over how a "-prefixed bound is consumed. The
107
+ # /m flag lets the captured upper bound (and quoted bounds) span
108
+ # newlines.
109
+ BOUNDS = /\A((?:"(?:[^"\\]|""|\\.)*"|[^,"])*),(.*)\z/m # :nodoc:
110
+
111
+ def split_bounds(value)
112
+ # Fast path: an unquoted representation (every built-in range
113
+ # type -- int/num/date/timestamp) has no embedded comma, so a
114
+ # plain split is correct and avoids the regexp. Only quoted
115
+ # bounds (custom text/varchar/money/... ranges) need the
116
+ # comma-skipping scan.
117
+ return value.split(",", 2) unless value.include?('"')
118
+
119
+ (match = BOUNDS.match(value)) ? [match[1], match[2]] : [value, nil]
120
+ end
121
+
79
122
  INFINITE_FLOAT_RANGE = (-::Float::INFINITY)..(::Float::INFINITY) # :nodoc:
80
123
 
81
124
  def sanitize_bounds(from, to)
@@ -94,7 +137,7 @@ module ActiveRecord
94
137
  # * https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-IO
95
138
  # * https://www.postgresql.org/docs/current/rowtypes.html#ROWTYPES-IO-SYNTAX
96
139
  def unquote(value)
97
- if value.start_with?('"') && value.end_with?('"')
140
+ if value && value.start_with?('"') && value.end_with?('"')
98
141
  unquoted_value = value[1..-2]
99
142
  unquoted_value.gsub!('""', '"')
100
143
  unquoted_value.gsub!("\\\\", "\\")
@@ -45,7 +45,7 @@ Rails needs superuser privileges to disable referential integrity.
45
45
  BEGIN
46
46
  FOR r IN (
47
47
  SELECT FORMAT(
48
- 'UPDATE pg_catalog.pg_constraint SET convalidated=false WHERE conname = ''%1$I'' AND connamespace::regnamespace = ''%2$I''::regnamespace; ALTER TABLE %2$I.%3$I VALIDATE CONSTRAINT %1$I;',
48
+ 'UPDATE pg_catalog.pg_constraint SET convalidated=false WHERE conname = ''%1$I'' AND connamespace::regnamespace = ''%2$I''::regnamespace AND conrelid::regclass = ''%3$I''::regclass; ALTER TABLE %2$I.%3$I VALIDATE CONSTRAINT %1$I;',
49
49
  constraint_name,
50
50
  table_schema,
51
51
  table_name
@@ -108,7 +108,7 @@ module ActiveRecord
108
108
  spec = { type: schema_type(column).inspect }.merge!(spec)
109
109
  end
110
110
 
111
- spec[:enum_type] = column.sql_type.inspect if column.enum?
111
+ spec[:enum_type] = relation_name(column.sql_type).inspect if column.enum?
112
112
 
113
113
  spec
114
114
  end
@@ -133,8 +133,9 @@ module ActiveRecord
133
133
 
134
134
  # add info on sort order (only desc order is explicitly specified, asc is the default)
135
135
  # and non-default opclasses
136
- expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops(_\w+)?)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
137
- opclasses[column] = opclass.to_sym if opclass
136
+ expressions.scan(/(?<column>\w+)"?\s?(?<opclass>(?:\w+\.)?\w+_ops(_\w+)?)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
137
+ opclasses[column] = opclass.split(".").last.to_sym if opclass
138
+
138
139
  if nulls
139
140
  orders[column] = [desc, nulls].compact.join(" ")
140
141
  else
@@ -222,6 +223,7 @@ module ActiveRecord
222
223
  WHERE child.relname = #{scope[:name]}
223
224
  AND child.relkind IN (#{scope[:type]})
224
225
  AND n.nspname = #{scope[:schema]}
226
+ ORDER BY i.inhseqno
225
227
  SQL
226
228
  end
227
229
 
@@ -551,7 +553,12 @@ module ActiveRecord
551
553
  result = execute schema_creation.accept(create_index)
552
554
 
553
555
  index = create_index.index
554
- execute "COMMENT ON INDEX #{quote_column_name(index.name)} IS #{quote(index.comment)}" if index.comment
556
+ if index.comment
557
+ name = Utils.extract_schema_qualified_name(table_name.to_s)
558
+ index_name = name.schema ? PostgreSQL::Name.new(name.schema, index.name).to_s : index.name
559
+ execute "COMMENT ON INDEX #{quote_table_name(index_name)} IS #{quote(index.comment)}"
560
+ end
561
+
555
562
  result
556
563
  end
557
564
 
@@ -634,7 +641,7 @@ module ActiveRecord
634
641
  SQL
635
642
 
636
643
  fk_info.map do |row|
637
- to_table = Utils.unquote_identifier(row["to_table"])
644
+ to_table = Utils.extract_schema_qualified_name(row["to_table"]).to_s
638
645
 
639
646
  column = decode_string_array(row["conkey_names"])
640
647
  primary_key = decode_string_array(row["confkey_names"])
@@ -704,7 +711,7 @@ module ActiveRecord
704
711
 
705
712
  exclusion_info.map do |row|
706
713
  method_and_elements, predicate = row["constraintdef"].split(" WHERE ")
707
- method_and_elements_parts = method_and_elements.match(/EXCLUDE(?: USING (?<using>\S+))? \((?<expression>.+)\)/)
714
+ method_and_elements_parts = method_and_elements.match(/EXCLUDE(?: USING (?<using>\S+))? \((?<expression>.+)\)/m)
708
715
  predicate.remove!(/ DEFERRABLE(?: INITIALLY (?:IMMEDIATE|DEFERRED))?/) if predicate
709
716
  predicate = predicate.from(2).to(-3) if predicate # strip 2 opening and closing parentheses
710
717
 
@@ -1113,6 +1120,11 @@ module ActiveRecord
1113
1120
  super
1114
1121
  end
1115
1122
 
1123
+ def validate_table_length!(table_name)
1124
+ _schema, table_name = extract_schema_qualified_name(table_name)
1125
+ super
1126
+ end
1127
+
1116
1128
  def exclusion_constraint_name(table_name, **options)
1117
1129
  options.fetch(:name) do
1118
1130
  expression = options.fetch(:expression)
@@ -271,10 +271,10 @@ module ActiveRecord
271
271
  end
272
272
 
273
273
  def encode_with(coder) # :nodoc:
274
- coder["columns"] = @columns.sort.to_h.transform_values { _1.sort_by(&:name) }
274
+ coder["columns"] = @columns.sort.to_h
275
275
  coder["primary_keys"] = @primary_keys.sort.to_h
276
276
  coder["data_sources"] = @data_sources.sort.to_h
277
- coder["indexes"] = @indexes.sort.to_h.transform_values { _1.sort_by(&:name) }
277
+ coder["indexes"] = @indexes.sort.to_h
278
278
  coder["version"] = @version
279
279
  end
280
280
 
@@ -21,7 +21,7 @@ module ActiveRecord
21
21
  WHERE name = #{quote(row['name'])} AND type = 'index'
22
22
  SQL
23
23
 
24
- /\bON\b\s*"?(\w+?)"?\s*\((?<expressions>.+?)\)(?:\s*WHERE\b\s*(?<where>.+))?(?:\s*\/\*.*\*\/)?\z/i =~ index_sql
24
+ /\bON\b\s*"?(\w+?)"?\s*\((?<expressions>.+?)\)(?:\s*WHERE\b\s*(?<where>.+?))?(?:\s*\/\*.*\*\/)?\s*\z/im =~ index_sql
25
25
 
26
26
  columns = internal_exec_query("PRAGMA index_info(#{quote(row['name'])})", "SCHEMA").map do |col|
27
27
  col["name"]
@@ -101,7 +101,7 @@ module ActiveRecord
101
101
  end
102
102
 
103
103
  def remove_check_constraint(table_name, expression = nil, if_exists: false, **options)
104
- return if if_exists && !check_constraint_exists?(table_name, **options)
104
+ return if if_exists && !check_constraint_exists?(table_name, expression: expression, **options)
105
105
 
106
106
  check_constraints = check_constraints(table_name)
107
107
  chk_name_to_delete = check_constraint_for!(table_name, expression: expression, **options).name
@@ -551,6 +551,10 @@ module ActiveRecord
551
551
  case default
552
552
  when /^null$/i
553
553
  nil
554
+ when /^false$/i
555
+ false
556
+ when /^true$/i
557
+ true
554
558
  # Quoted types
555
559
  when /^'([^|]*)'$/m
556
560
  $1.gsub("''", "'")
@@ -563,8 +567,6 @@ module ActiveRecord
563
567
  # Binary columns
564
568
  when /x'(.*)'/
565
569
  [ $1 ].pack("H*")
566
- when "TRUE", "FALSE"
567
- default
568
570
  else
569
571
  # Anything else is blank or some function
570
572
  # and we can't know the value of that, so return nil.
@@ -10,6 +10,10 @@ module ActiveRecord
10
10
  end
11
11
 
12
12
  private
13
+ def multi_statements_enabled?
14
+ @config[:multi_statement]
15
+ end
16
+
13
17
  def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
14
18
  reset_multi_statement = if batch && !@config[:multi_statement]
15
19
  raw_connection.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_ON)
@@ -18,6 +18,20 @@ module ActiveRecord
18
18
 
19
19
  include Trilogy::DatabaseStatements
20
20
 
21
+ # Trilogy spells its SSL options with underscores, and supports certificate
22
+ # revocation lists, which mysql2 does not.
23
+ self.cli_arg_map = cli_arg_map
24
+ .except(:sslca, :sslcert, :sslcapath, :sslcipher, :sslkey)
25
+ .merge(
26
+ ssl_ca: "--ssl-ca",
27
+ ssl_cert: "--ssl-cert",
28
+ ssl_capath: "--ssl-capath",
29
+ ssl_cipher: "--ssl-cipher",
30
+ ssl_key: "--ssl-key",
31
+ ssl_crl: "--ssl-crl",
32
+ ssl_crlpath: "--ssl-crlpath"
33
+ ).freeze
34
+
21
35
  SSL_MODES = {
22
36
  SSL_MODE_DISABLED: ::Trilogy::SSL_DISABLED,
23
37
  SSL_MODE_PREFERRED: ::Trilogy::SSL_PREFERRED_NOVERIFY,
@@ -35,14 +35,16 @@ module ActiveRecord
35
35
  # # attributes.
36
36
  # Post.reset_counters(1, :comments, touch: true)
37
37
  def reset_counters(id, *counters, touch: nil)
38
- ids = if composite_primary_key?
39
- if id.first.is_a?(Array)
40
- id
41
- else
42
- [id]
38
+ if composite_primary_key?
39
+ ids = id.first.is_a?(Array) ? id : [id]
40
+ types = primary_key.map { |column| type_for_attribute(column) }
41
+ ids = ids.map do |id_value|
42
+ id_value.map.with_index { |id, i| types[i].cast(id) }
43
43
  end
44
44
  else
45
- Array(id)
45
+ ids = Array(id)
46
+ type = type_for_attribute(primary_key)
47
+ ids = ids.map { |id| type.cast(id) }
46
48
  end
47
49
 
48
50
  updates = Hash.new { |h, k| h[k] = {} }
@@ -123,8 +123,15 @@ module ActiveRecord
123
123
  end
124
124
 
125
125
  def keepalive
126
- keepalive = (configuration_hash[:keepalive] || 600).to_f
127
- keepalive if keepalive > 0
126
+ case keepalive = configuration_hash[:keepalive]
127
+ when false
128
+ nil
129
+ when nil, true
130
+ 600.0 # default
131
+ else
132
+ keepalive = keepalive.to_f
133
+ keepalive if keepalive > 0.0
134
+ end
128
135
  end
129
136
 
130
137
  def adapter
@@ -52,6 +52,8 @@ module ActiveRecord
52
52
 
53
53
  to_boolean!(@configuration_hash, :replica)
54
54
  to_boolean!(@configuration_hash, :database_tasks)
55
+ to_boolean!(@configuration_hash, :seeds)
56
+ to_boolean!(@configuration_hash, :use_metadata_table)
55
57
 
56
58
  @configuration_hash.freeze
57
59
  end
@@ -27,7 +27,11 @@ module ActiveRecord
27
27
  records = @records
28
28
 
29
29
  records_by_id = records.group_by do |record|
30
- record[key]
30
+ if key.is_a?(Array)
31
+ key.map { |column| record[column] }
32
+ else
33
+ record[key]
34
+ end
31
35
  end
32
36
 
33
37
  records = ids.flat_map { |id| records_by_id[id] }
@@ -15,10 +15,7 @@ module ActiveRecord
15
15
  model_class&.encrypted_attributes&.each do |attribute_name|
16
16
  if clean_value = fixture[attribute_name.to_s]
17
17
  @clean_values[attribute_name.to_s] = clean_value
18
-
19
- type = model_class.type_for_attribute(attribute_name)
20
- encrypted_value = type.serialize(clean_value)
21
- fixture[attribute_name.to_s] = encrypted_value
18
+ fixture[attribute_name.to_s] = encrypt(model_class, attribute_name, clean_value)
22
19
  end
23
20
  end
24
21
  end
@@ -27,12 +24,20 @@ module ActiveRecord
27
24
  model_class&.encrypted_attributes&.each do |attribute_name|
28
25
  if source_attribute_name = model_class.source_attribute_from_preserved_attribute(attribute_name)
29
26
  clean_value = @clean_values[source_attribute_name.to_s]
30
- type = model_class.type_for_attribute(attribute_name)
31
- encrypted_value = type.serialize(clean_value)
32
- fixture[attribute_name.to_s] = encrypted_value
27
+ fixture[attribute_name.to_s] = encrypt(model_class, attribute_name, clean_value)
33
28
  end
34
29
  end
35
30
  end
31
+
32
+ def encrypt(model_class, attribute_name, clean_value)
33
+ encrypted_value = model_class.type_for_attribute(attribute_name).serialize(clean_value)
34
+
35
+ if column = model_class.columns_hash[attribute_name.to_s]
36
+ column.cast_type.deserialize(encrypted_value)
37
+ else
38
+ encrypted_value
39
+ end
40
+ end
36
41
  end
37
42
  end
38
43
  end
@@ -64,9 +64,12 @@ module ActiveRecord
64
64
  list.each do |attribute|
65
65
  next if klass.abstract_class? || klass == Base
66
66
 
67
- klass_name = klass.name ? klass.model_name.element : nil
68
- filter = [klass_name, attribute.to_s].compact.join(".")
69
- app.config.filter_parameters << filter unless app.config.filter_parameters.include?(filter)
67
+ case attribute
68
+ when Symbol, String
69
+ klass_name = klass.name ? klass.model_name.element : nil
70
+ filter = [klass_name, attribute.to_s].compact.join(".")
71
+ app.config.filter_parameters << filter unless app.config.filter_parameters.include?(filter)
72
+ end
70
73
  end
71
74
  end
72
75
  end
@@ -9,8 +9,8 @@ module ActiveRecord
9
9
  module VERSION
10
10
  MAJOR = 8
11
11
  MINOR = 1
12
- TINY = 3
13
- PRE = "1"
12
+ TINY = 4
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -32,9 +32,9 @@ module ActiveRecord
32
32
  # be triggered. In that case, it'll work just like normal subclasses with no special magic
33
33
  # for differentiating between them or reloading the right type with find.
34
34
  #
35
- # Note, all the attributes for all the cases are kept in the same table.
36
- # Read more:
37
- # * https://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
35
+ # All subclasses share the same database table. See the
36
+ # {Single Table Inheritance pattern}[https://www.martinfowler.com/eaaCatalog/singleTableInheritance.html]
37
+ # for more background.
38
38
  #
39
39
  module Inheritance
40
40
  extend ActiveSupport::Concern
@@ -237,6 +237,11 @@ module ActiveRecord
237
237
  end
238
238
 
239
239
  protected
240
+ def reload_schema_from_cache(*) # :nodoc:
241
+ @finder_needs_type_condition = nil
242
+ super
243
+ end
244
+
240
245
  # Returns the class type of the record using the current module as a prefix. So descendants of
241
246
  # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
242
247
  def compute_type(type_name)
@@ -86,23 +86,35 @@ module ActiveRecord
86
86
  end
87
87
 
88
88
  def sql_color(sql)
89
- case sql
90
- when /\A\s*rollback/mi
91
- RED
92
- when /select .*for update/mi, /\A\s*lock/mi
93
- WHITE
94
- when /\A\s*select/i
95
- BLUE
96
- when /\A\s*insert/i
97
- GREEN
98
- when /\A\s*update/i
99
- YELLOW
100
- when /\A\s*delete/i
101
- RED
102
- when /transaction\s*\Z/i
103
- CYAN
89
+ color = if (match = sql.match(/\A\s*(\w+)(?:\s|\Z)/))
90
+ case match[1].downcase
91
+ when "rollback"
92
+ RED
93
+ when "lock"
94
+ WHITE
95
+ when "select"
96
+ if sql.match?(/for update/mi)
97
+ WHITE
98
+ else
99
+ BLUE
100
+ end
101
+ when "insert"
102
+ GREEN
103
+ when "update"
104
+ YELLOW
105
+ when "delete"
106
+ RED
107
+ end
108
+ end
109
+
110
+ if color
111
+ color
104
112
  else
105
- MAGENTA
113
+ if sql.match?(/transaction\s*\Z/i)
114
+ CYAN
115
+ else
116
+ MAGENTA
117
+ end
106
118
  end
107
119
  end
108
120
 
@@ -23,6 +23,11 @@ module ActiveRecord
23
23
  extend self
24
24
 
25
25
  def install(registry)
26
+ registry.register_type 118, ActiveRecord::Type::Time::Value,
27
+ packer: method(:write_time_value),
28
+ unpacker: method(:read_time_value),
29
+ recursive: true
30
+
26
31
  registry.register_type 119, ActiveModel::Type::Binary::Data,
27
32
  packer: :to_s,
28
33
  unpacker: :new
@@ -33,6 +38,14 @@ module ActiveRecord
33
38
  recursive: true
34
39
  end
35
40
 
41
+ def write_time_value(value, packer)
42
+ packer.write(value.__getobj__)
43
+ end
44
+
45
+ def read_time_value(unpacker)
46
+ ActiveRecord::Type::Time::Value.new(unpacker.read)
47
+ end
48
+
36
49
  def write_record(record, packer)
37
50
  packer.write(ActiveRecord::MessagePack.dump(record))
38
51
  end
@@ -239,7 +239,9 @@ module ActiveRecord
239
239
  end
240
240
 
241
241
  def invert_remove_column(args)
242
- raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type." if args.size <= 2
242
+ if args.size <= 2 || args[2].is_a?(Hash)
243
+ raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type."
244
+ end
243
245
  super
244
246
  end
245
247
 
@@ -297,6 +299,13 @@ module ActiveRecord
297
299
  [:change_column_null, args]
298
300
  end
299
301
 
302
+ def invert_add_column(args)
303
+ if (options = args.last).is_a?(Hash)
304
+ options[:if_exists] = options.delete(:if_not_exists) if options.key?(:if_not_exists)
305
+ end
306
+ super
307
+ end
308
+
300
309
  def invert_add_foreign_key(args)
301
310
  args.last.delete(:validate) if args.last.is_a?(Hash)
302
311
  super
@@ -307,6 +316,7 @@ module ActiveRecord
307
316
  from_table, to_table = args
308
317
 
309
318
  to_table ||= options.delete(:to_table)
319
+ options[:if_not_exists] = options.delete(:if_exists) if options.key?(:if_exists)
310
320
 
311
321
  raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil?
312
322
 
@@ -400,7 +410,7 @@ module ActiveRecord
400
410
  end
401
411
 
402
412
  def invert_drop_virtual_table(args)
403
- _enum, values = args.dup.tap(&:extract_options!)
413
+ _table_name, _module_name, values = args.dup.tap(&:extract_options!)
404
414
  raise ActiveRecord::IrreversibleMigration, "drop_virtual_table is only reversible if given options." unless values
405
415
  super
406
416
  end
@@ -435,7 +435,7 @@ module ActiveRecord
435
435
 
436
436
  def attributes_builder # :nodoc:
437
437
  @attributes_builder ||= begin
438
- defaults = _default_attributes.except(*(column_names - [primary_key]))
438
+ defaults = _default_attributes.except(*(column_names - Array(primary_key)))
439
439
  ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
440
440
  end
441
441
  end
@@ -504,7 +504,7 @@ module ActiveRecord
504
504
  # and columns used for single table inheritance have been removed.
505
505
  def content_columns
506
506
  @content_columns ||= columns.reject do |c|
507
- c.name == primary_key ||
507
+ Array(primary_key).include?(c.name) ||
508
508
  c.name == inheritance_column ||
509
509
  c.name.end_with?("_id", "_count")
510
510
  end.freeze