activerecord 8.0.3 → 8.1.0.beta1

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +427 -522
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/association_relation.rb +1 -1
  5. data/lib/active_record/associations/association.rb +1 -1
  6. data/lib/active_record/associations/builder/association.rb +16 -5
  7. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  8. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  9. data/lib/active_record/associations/builder/has_one.rb +1 -1
  10. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  11. data/lib/active_record/associations/collection_proxy.rb +22 -4
  12. data/lib/active_record/associations/deprecation.rb +88 -0
  13. data/lib/active_record/associations/errors.rb +3 -0
  14. data/lib/active_record/associations/join_dependency.rb +2 -0
  15. data/lib/active_record/associations/preloader/branch.rb +1 -0
  16. data/lib/active_record/associations.rb +159 -21
  17. data/lib/active_record/attribute_methods/serialization.rb +16 -3
  18. data/lib/active_record/attribute_methods.rb +1 -1
  19. data/lib/active_record/attributes.rb +3 -0
  20. data/lib/active_record/autosave_association.rb +1 -1
  21. data/lib/active_record/base.rb +2 -3
  22. data/lib/active_record/coders/json.rb +14 -5
  23. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +1 -3
  24. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +15 -0
  25. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
  26. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +382 -51
  27. data/lib/active_record/connection_adapters/abstract/database_statements.rb +26 -30
  28. data/lib/active_record/connection_adapters/abstract/query_cache.rb +25 -18
  29. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  30. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  31. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +26 -34
  32. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  33. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +85 -22
  34. data/lib/active_record/connection_adapters/abstract/transaction.rb +16 -3
  35. data/lib/active_record/connection_adapters/abstract_adapter.rb +66 -14
  36. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -11
  37. data/lib/active_record/connection_adapters/column.rb +17 -4
  38. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  39. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  40. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  41. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +26 -4
  42. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +27 -22
  43. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -0
  44. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +18 -23
  45. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  46. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  47. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  48. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -1
  49. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +8 -21
  50. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +65 -30
  51. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +74 -38
  52. data/lib/active_record/connection_adapters/postgresql_adapter.rb +10 -5
  53. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  54. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +37 -25
  55. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  56. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
  57. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +54 -30
  58. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  59. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  60. data/lib/active_record/connection_adapters.rb +1 -0
  61. data/lib/active_record/core.rb +5 -4
  62. data/lib/active_record/counter_cache.rb +33 -8
  63. data/lib/active_record/database_configurations/database_config.rb +5 -1
  64. data/lib/active_record/database_configurations/hash_config.rb +50 -9
  65. data/lib/active_record/database_configurations/url_config.rb +13 -3
  66. data/lib/active_record/database_configurations.rb +7 -3
  67. data/lib/active_record/delegated_type.rb +1 -1
  68. data/lib/active_record/dynamic_matchers.rb +54 -69
  69. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  70. data/lib/active_record/encryption/encrypted_attribute_type.rb +1 -1
  71. data/lib/active_record/encryption/scheme.rb +1 -1
  72. data/lib/active_record/enum.rb +24 -8
  73. data/lib/active_record/errors.rb +23 -7
  74. data/lib/active_record/explain_registry.rb +0 -1
  75. data/lib/active_record/filter_attribute_handler.rb +73 -0
  76. data/lib/active_record/fixtures.rb +2 -2
  77. data/lib/active_record/gem_version.rb +3 -3
  78. data/lib/active_record/inheritance.rb +1 -1
  79. data/lib/active_record/insert_all.rb +12 -7
  80. data/lib/active_record/locking/optimistic.rb +7 -0
  81. data/lib/active_record/locking/pessimistic.rb +5 -0
  82. data/lib/active_record/log_subscriber.rb +1 -5
  83. data/lib/active_record/middleware/shard_selector.rb +34 -17
  84. data/lib/active_record/migration/command_recorder.rb +14 -1
  85. data/lib/active_record/migration/compatibility.rb +34 -24
  86. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  87. data/lib/active_record/migration.rb +26 -16
  88. data/lib/active_record/model_schema.rb +10 -7
  89. data/lib/active_record/nested_attributes.rb +2 -0
  90. data/lib/active_record/persistence.rb +34 -3
  91. data/lib/active_record/query_cache.rb +22 -15
  92. data/lib/active_record/query_logs.rb +3 -7
  93. data/lib/active_record/railtie.rb +32 -3
  94. data/lib/active_record/railties/databases.rake +16 -4
  95. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  96. data/lib/active_record/railties/job_runtime.rb +10 -11
  97. data/lib/active_record/reflection.rb +42 -3
  98. data/lib/active_record/relation/batches.rb +26 -12
  99. data/lib/active_record/relation/calculations.rb +20 -9
  100. data/lib/active_record/relation/delegation.rb +0 -1
  101. data/lib/active_record/relation/finder_methods.rb +27 -11
  102. data/lib/active_record/relation/merger.rb +2 -2
  103. data/lib/active_record/relation/predicate_builder.rb +2 -2
  104. data/lib/active_record/relation/query_attribute.rb +3 -1
  105. data/lib/active_record/relation/query_methods.rb +39 -29
  106. data/lib/active_record/relation/where_clause.rb +1 -10
  107. data/lib/active_record/relation.rb +25 -13
  108. data/lib/active_record/result.rb +44 -21
  109. data/lib/active_record/sanitization.rb +2 -0
  110. data/lib/active_record/schema_dumper.rb +12 -10
  111. data/lib/active_record/scoping.rb +0 -1
  112. data/lib/active_record/signed_id.rb +43 -15
  113. data/lib/active_record/statement_cache.rb +13 -9
  114. data/lib/active_record/store.rb +44 -19
  115. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  116. data/lib/active_record/tasks/database_tasks.rb +2 -21
  117. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  118. data/lib/active_record/tasks/postgresql_database_tasks.rb +5 -39
  119. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  120. data/lib/active_record/test_databases.rb +10 -2
  121. data/lib/active_record/test_fixtures.rb +27 -2
  122. data/lib/active_record/testing/query_assertions.rb +8 -2
  123. data/lib/active_record/timestamp.rb +4 -2
  124. data/lib/active_record/transaction.rb +2 -5
  125. data/lib/active_record/transactions.rb +32 -10
  126. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  127. data/lib/active_record/type/internal/timezone.rb +7 -0
  128. data/lib/active_record/type/json.rb +15 -2
  129. data/lib/active_record/type/serialized.rb +11 -4
  130. data/lib/active_record/type/type_map.rb +1 -1
  131. data/lib/active_record/type_caster/connection.rb +2 -1
  132. data/lib/active_record/validations/associated.rb +1 -1
  133. data/lib/active_record.rb +65 -3
  134. data/lib/arel/alias_predication.rb +2 -0
  135. data/lib/arel/crud.rb +6 -11
  136. data/lib/arel/nodes/count.rb +2 -2
  137. data/lib/arel/nodes/function.rb +4 -10
  138. data/lib/arel/nodes/named_function.rb +2 -2
  139. data/lib/arel/nodes/node.rb +1 -1
  140. data/lib/arel/nodes.rb +0 -2
  141. data/lib/arel/select_manager.rb +7 -2
  142. data/lib/arel/visitors/dot.rb +0 -3
  143. data/lib/arel/visitors/postgresql.rb +55 -0
  144. data/lib/arel/visitors/sqlite.rb +55 -8
  145. data/lib/arel/visitors/to_sql.rb +3 -21
  146. data/lib/arel.rb +3 -1
  147. metadata +13 -9
  148. data/lib/active_record/normalization.rb +0 -163
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Tasks # :nodoc:
5
+ class AbstractTasks # :nodoc:
6
+ def self.using_database_configurations?
7
+ true
8
+ end
9
+
10
+ def initialize(db_config)
11
+ @db_config = db_config
12
+ @configuration_hash = db_config.configuration_hash
13
+ end
14
+
15
+ def charset
16
+ connection.encoding
17
+ end
18
+
19
+ def collation
20
+ connection.collation
21
+ end
22
+
23
+ def check_current_protected_environment!(db_config, migration_class)
24
+ with_temporary_pool(db_config, migration_class) do |pool|
25
+ migration_context = pool.migration_context
26
+ current = migration_context.current_environment
27
+ stored = migration_context.last_stored_environment
28
+
29
+ if migration_context.protected_environment?
30
+ raise ActiveRecord::ProtectedEnvironmentError.new(stored)
31
+ end
32
+
33
+ if stored && stored != current
34
+ raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored)
35
+ end
36
+ rescue ActiveRecord::NoDatabaseError
37
+ end
38
+ end
39
+
40
+ private
41
+ attr_reader :db_config, :configuration_hash
42
+
43
+ def connection
44
+ ActiveRecord::Base.lease_connection
45
+ end
46
+
47
+ def establish_connection(config = db_config)
48
+ ActiveRecord::Base.establish_connection(config)
49
+ end
50
+
51
+ def configuration_hash_without_database
52
+ configuration_hash.merge(database: nil)
53
+ end
54
+
55
+ def run_cmd(cmd, *args, **opts)
56
+ fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, opts)
57
+ end
58
+
59
+ def run_cmd_error(cmd, args)
60
+ msg = +"failed to execute:\n"
61
+ msg << "#{cmd} #{args.join(' ')}\n\n"
62
+ msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
63
+ msg
64
+ end
65
+
66
+ def with_temporary_pool(db_config, migration_class, clobber: false)
67
+ original_db_config = migration_class.connection_db_config
68
+ pool = migration_class.connection_handler.establish_connection(db_config, clobber: clobber)
69
+
70
+ yield pool
71
+ ensure
72
+ migration_class.connection_handler.establish_connection(original_db_config, clobber: clobber)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -66,7 +66,7 @@ module ActiveRecord
66
66
  return if ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"]
67
67
 
68
68
  configs_for(env_name: environment).each do |db_config|
69
- check_current_protected_environment!(db_config)
69
+ database_adapter_for(db_config).check_current_protected_environment!(db_config, migration_class)
70
70
  end
71
71
  end
72
72
 
@@ -147,8 +147,6 @@ module ActiveRecord
147
147
  return if database_configs.count == 1
148
148
 
149
149
  database_configs.each do |db_config|
150
- next unless db_config.database_tasks?
151
-
152
150
  yield db_config.name
153
151
  end
154
152
  end
@@ -453,7 +451,7 @@ module ActiveRecord
453
451
  structure_dump(db_config, filename)
454
452
  if migration_connection_pool.schema_migration.table_exists?
455
453
  File.open(filename, "a") do |f|
456
- f.puts migration_connection.dump_schema_information
454
+ f.puts migration_connection.dump_schema_versions
457
455
  f.print "\n"
458
456
  end
459
457
  end
@@ -640,23 +638,6 @@ module ActiveRecord
640
638
  end
641
639
  end
642
640
 
643
- def check_current_protected_environment!(db_config)
644
- with_temporary_pool(db_config) do |pool|
645
- migration_context = pool.migration_context
646
- current = migration_context.current_environment
647
- stored = migration_context.last_stored_environment
648
-
649
- if migration_context.protected_environment?
650
- raise ActiveRecord::ProtectedEnvironmentError.new(stored)
651
- end
652
-
653
- if stored && stored != current
654
- raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored)
655
- end
656
- rescue ActiveRecord::NoDatabaseError
657
- end
658
- end
659
-
660
641
  def initialize_database(db_config)
661
642
  with_temporary_pool(db_config) do
662
643
  begin
@@ -2,16 +2,7 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Tasks # :nodoc:
5
- class MySQLDatabaseTasks # :nodoc:
6
- def self.using_database_configurations?
7
- true
8
- end
9
-
10
- def initialize(db_config)
11
- @db_config = db_config
12
- @configuration_hash = db_config.configuration_hash
13
- end
14
-
5
+ class MySQLDatabaseTasks < AbstractTasks # :nodoc:
15
6
  def create
16
7
  establish_connection(configuration_hash_without_database)
17
8
  connection.create_database(db_config.database, creation_options)
@@ -33,10 +24,6 @@ module ActiveRecord
33
24
  connection.charset
34
25
  end
35
26
 
36
- def collation
37
- connection.collation
38
- end
39
-
40
27
  def structure_dump(filename, extra_flags)
41
28
  args = prepare_command_options
42
29
  args.concat(["--result-file", "#{filename}"])
@@ -53,7 +40,7 @@ module ActiveRecord
53
40
  args.concat([db_config.database.to_s])
54
41
  args.unshift(*extra_flags) if extra_flags
55
42
 
56
- run_cmd("mysqldump", args, "dumping")
43
+ run_cmd("mysqldump", *args)
57
44
  end
58
45
 
59
46
  def structure_load(filename, extra_flags)
@@ -62,24 +49,10 @@ module ActiveRecord
62
49
  args.concat(["--database", db_config.database.to_s])
63
50
  args.unshift(*extra_flags) if extra_flags
64
51
 
65
- run_cmd("mysql", args, "loading")
52
+ run_cmd("mysql", *args)
66
53
  end
67
54
 
68
55
  private
69
- attr_reader :db_config, :configuration_hash
70
-
71
- def connection
72
- ActiveRecord::Base.lease_connection
73
- end
74
-
75
- def establish_connection(config = db_config)
76
- ActiveRecord::Base.establish_connection(config)
77
- end
78
-
79
- def configuration_hash_without_database
80
- configuration_hash.merge(database: nil)
81
- end
82
-
83
56
  def creation_options
84
57
  Hash.new.tap do |options|
85
58
  options[:charset] = configuration_hash[:encoding] if configuration_hash.include?(:encoding)
@@ -105,16 +78,6 @@ module ActiveRecord
105
78
 
106
79
  args
107
80
  end
108
-
109
- def run_cmd(cmd, args, action)
110
- fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args)
111
- end
112
-
113
- def run_cmd_error(cmd, args, action)
114
- msg = +"failed to execute: `#{cmd}`\n"
115
- msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
116
- msg
117
- end
118
81
  end
119
82
  end
120
83
  end
@@ -4,20 +4,11 @@ require "tempfile"
4
4
 
5
5
  module ActiveRecord
6
6
  module Tasks # :nodoc:
7
- class PostgreSQLDatabaseTasks # :nodoc:
7
+ class PostgreSQLDatabaseTasks < AbstractTasks # :nodoc:
8
8
  DEFAULT_ENCODING = ENV["CHARSET"] || "utf8"
9
9
  ON_ERROR_STOP_1 = "ON_ERROR_STOP=1"
10
10
  SQL_COMMENT_BEGIN = "--"
11
11
 
12
- def self.using_database_configurations?
13
- true
14
- end
15
-
16
- def initialize(db_config)
17
- @db_config = db_config
18
- @configuration_hash = db_config.configuration_hash
19
- end
20
-
21
12
  def create(connection_already_established = false)
22
13
  establish_connection(public_schema_config) unless connection_already_established
23
14
  connection.create_database(db_config.database, configuration_hash.merge(encoding: encoding))
@@ -29,14 +20,6 @@ module ActiveRecord
29
20
  connection.drop_database(db_config.database)
30
21
  end
31
22
 
32
- def charset
33
- connection.encoding
34
- end
35
-
36
- def collation
37
- connection.collation
38
- end
39
-
40
23
  def purge
41
24
  ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
42
25
  drop
@@ -72,7 +55,7 @@ module ActiveRecord
72
55
  end
73
56
 
74
57
  args << db_config.database
75
- run_cmd("pg_dump", args, "dumping")
58
+ run_cmd("pg_dump", *args)
76
59
  remove_sql_header_comments(filename)
77
60
  File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" }
78
61
  end
@@ -82,20 +65,10 @@ module ActiveRecord
82
65
  args.concat(Array(extra_flags)) if extra_flags
83
66
  args.concat(["--file", filename])
84
67
  args << db_config.database
85
- run_cmd("psql", args, "loading")
68
+ run_cmd("psql", *args)
86
69
  end
87
70
 
88
71
  private
89
- attr_reader :db_config, :configuration_hash
90
-
91
- def connection
92
- ActiveRecord::Base.lease_connection
93
- end
94
-
95
- def establish_connection(config = db_config)
96
- ActiveRecord::Base.establish_connection(config)
97
- end
98
-
99
72
  def encoding
100
73
  configuration_hash[:encoding] || DEFAULT_ENCODING
101
74
  end
@@ -117,15 +90,8 @@ module ActiveRecord
117
90
  end
118
91
  end
119
92
 
120
- def run_cmd(cmd, args, action)
121
- fail run_cmd_error(cmd, args, action) unless Kernel.system(psql_env, cmd, *args)
122
- end
123
-
124
- def run_cmd_error(cmd, args, action)
125
- msg = +"failed to execute:\n"
126
- msg << "#{cmd} #{args.join(' ')}\n\n"
127
- msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
128
- msg
93
+ def run_cmd(cmd, *args, **opts)
94
+ fail run_cmd_error(cmd, args) unless Kernel.system(psql_env, cmd, *args, **opts)
129
95
  end
130
96
 
131
97
  def remove_sql_header_comments(filename)
@@ -2,11 +2,7 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Tasks # :nodoc:
5
- class SQLiteDatabaseTasks # :nodoc:
6
- def self.using_database_configurations?
7
- true
8
- end
9
-
5
+ class SQLiteDatabaseTasks < AbstractTasks # :nodoc:
10
6
  def initialize(db_config, root = ActiveRecord::Tasks::DatabaseTasks.root)
11
7
  @db_config = db_config
12
8
  @root = root
@@ -37,10 +33,6 @@ module ActiveRecord
37
33
  connection.reconnect!
38
34
  end
39
35
 
40
- def charset
41
- connection.encoding
42
- end
43
-
44
36
  def structure_dump(filename, extra_flags)
45
37
  args = []
46
38
  args.concat(Array(extra_flags)) if extra_flags
@@ -54,7 +46,8 @@ module ActiveRecord
54
46
  else
55
47
  args << ".schema --nosys"
56
48
  end
57
- run_cmd("sqlite3", args, filename)
49
+
50
+ run_cmd("sqlite3", *args, out: filename)
58
51
  end
59
52
 
60
53
  def structure_load(filename, extra_flags)
@@ -62,28 +55,23 @@ module ActiveRecord
62
55
  `sqlite3 #{flags} #{db_config.database} < "#{filename}"`
63
56
  end
64
57
 
65
- private
66
- attr_reader :db_config, :root
67
-
68
- def connection
69
- ActiveRecord::Base.lease_connection
58
+ def check_current_protected_environment!(db_config, migration_class)
59
+ super
60
+ rescue ActiveRecord::StatementInvalid => e
61
+ case e.cause
62
+ when SQLite3::ReadOnlyException
63
+ else
64
+ raise e
70
65
  end
66
+ end
67
+
68
+ private
69
+ attr_reader :root
71
70
 
72
71
  def establish_connection(config = db_config)
73
72
  ActiveRecord::Base.establish_connection(config)
74
73
  connection.connect!
75
74
  end
76
-
77
- def run_cmd(cmd, args, out)
78
- fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out)
79
- end
80
-
81
- def run_cmd_error(cmd, args)
82
- msg = +"failed to execute:\n"
83
- msg << "#{cmd} #{args.join(' ')}\n\n"
84
- msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
85
- msg
86
- end
87
75
  end
88
76
  end
89
77
  end
@@ -4,15 +4,23 @@ require "active_support/testing/parallelization"
4
4
 
5
5
  module ActiveRecord
6
6
  module TestDatabases # :nodoc:
7
+ ActiveSupport::Testing::Parallelization.before_fork_hook do
8
+ if ActiveSupport.parallelize_test_databases
9
+ ActiveRecord::Base.connection_handler.clear_all_connections!
10
+ end
11
+ end
12
+
7
13
  ActiveSupport::Testing::Parallelization.after_fork_hook do |i|
8
- create_and_load_schema(i, env_name: ActiveRecord::ConnectionHandling::DEFAULT_ENV.call)
14
+ if ActiveSupport.parallelize_test_databases
15
+ create_and_load_schema(i, env_name: ActiveRecord::ConnectionHandling::DEFAULT_ENV.call)
16
+ end
9
17
  end
10
18
 
11
19
  def self.create_and_load_schema(i, env_name:)
12
20
  old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
13
21
 
14
22
  ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config|
15
- db_config._database = "#{db_config.database}-#{i}"
23
+ db_config._database = "#{db_config.database}_#{i}"
16
24
 
17
25
  ActiveRecord::Tasks::DatabaseTasks.reconstruct_from_schema(db_config, nil)
18
26
  end
@@ -36,11 +36,26 @@ module ActiveRecord
36
36
  class_attribute :pre_loaded_fixtures, default: false
37
37
  class_attribute :lock_threads, default: true
38
38
  class_attribute :fixture_sets, default: {}
39
+ class_attribute :database_transactions_config, default: {}
39
40
 
40
41
  ActiveSupport.run_load_hooks(:active_record_fixtures, self)
41
42
  end
42
43
 
43
44
  module ClassMethods
45
+ # Do not use transactional tests for the given database. This overrides
46
+ # the default setting as defined by `use_transactional_tests`, which
47
+ # applies to all database connection pools not explicitly configured here.
48
+ def skip_transactional_tests_for_database(database_name)
49
+ use_transactional_tests_for_database(database_name, false)
50
+ end
51
+
52
+ # Enable or disable transactions per database. This overrides the default
53
+ # setting as defined by `use_transactional_tests`, which applies to all
54
+ # database connection pools not explicitly configured here.
55
+ def use_transactional_tests_for_database(database_name, enabled = true)
56
+ self.database_transactions_config = database_transactions_config.merge(database_name => enabled)
57
+ end
58
+
44
59
  # Sets the model class for a fixture when the class name cannot be inferred from the fixture name.
45
60
  #
46
61
  # Examples:
@@ -106,7 +121,8 @@ module ActiveRecord
106
121
 
107
122
  private
108
123
  def run_in_transaction?
109
- use_transactional_tests &&
124
+ has_explicit_config = database_transactions_config.any? { |_, enabled| enabled }
125
+ (use_transactional_tests || has_explicit_config) &&
110
126
  !self.class.uses_transaction?(name)
111
127
  end
112
128
 
@@ -169,11 +185,19 @@ module ActiveRecord
169
185
  @@already_loaded_fixtures.clear
170
186
  end
171
187
 
188
+ def transactional_tests_for_pool?(pool)
189
+ database_transactions_config.fetch(pool.db_config.name.to_sym, use_transactional_tests)
190
+ end
191
+
172
192
  def setup_transactional_fixtures
173
193
  setup_shared_connection_pool
174
194
 
175
195
  # Begin transactions for connections already established
176
196
  @fixture_connection_pools = ActiveRecord::Base.connection_handler.connection_pool_list(:writing)
197
+
198
+ # Filter to pools that want to use transactions
199
+ @fixture_connection_pools.select! { |pool| transactional_tests_for_pool?(pool) }
200
+
177
201
  @fixture_connection_pools.each do |pool|
178
202
  pool.pin_connection!(lock_threads)
179
203
  pool.lease_connection
@@ -189,7 +213,8 @@ module ActiveRecord
189
213
  if pool
190
214
  setup_shared_connection_pool
191
215
 
192
- unless @fixture_connection_pools.include?(pool)
216
+ # Don't begin a transaction if we've already done so, or are not using them for this pool
217
+ if !@fixture_connection_pools.include?(pool) && transactional_tests_for_pool?(pool)
193
218
  pool.pin_connection!(lock_threads)
194
219
  pool.lease_connection
195
220
  @fixture_connection_pools << pool
@@ -11,12 +11,18 @@ module ActiveRecord
11
11
  # # Check for any number of queries
12
12
  # assert_queries_count { Post.first }
13
13
  #
14
- # If the +:include_schema+ option is provided, any queries (including schema related) are counted.
14
+ # Any unmaterialized transactions will be materialized to ensure only
15
+ # queries attempted intside the block are counted.
16
+ #
17
+ # If the +:include_schema+ option is provided, any queries (including
18
+ # schema related) are counted. Setting this option also skips leasing a
19
+ # connection to materialize pending transactions since we want to count
20
+ # queries executed at connection open (e.g., type map).
15
21
  #
16
22
  # assert_queries_count(1, include_schema: true) { Post.columns }
17
23
  #
18
24
  def assert_queries_count(count = nil, include_schema: false, &block)
19
- ActiveRecord::Base.lease_connection.materialize_transactions
25
+ ActiveRecord::Base.lease_connection.materialize_transactions unless include_schema
20
26
 
21
27
  counter = SQLCounter.new
22
28
  ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
@@ -169,8 +169,10 @@ module ActiveRecord
169
169
  # Clear attributes and changed_attributes
170
170
  def clear_timestamp_attributes
171
171
  all_timestamp_attributes_in_model.each do |attribute_name|
172
- self[attribute_name] = nil
173
- clear_attribute_change(attribute_name)
172
+ if self[attribute_name]
173
+ self[attribute_name] = nil
174
+ clear_attribute_change(attribute_name)
175
+ end
174
176
  end
175
177
  end
176
178
  end
@@ -26,7 +26,7 @@ module ActiveRecord
26
26
  # # We are inside a real and not finalized transaction.
27
27
  # end
28
28
  #
29
- # Closed transactions are `blank?` too.
29
+ # Closed transactions are +blank?+ too.
30
30
  #
31
31
  # == Callbacks
32
32
  #
@@ -101,9 +101,6 @@ module ActiveRecord
101
101
  #
102
102
  # If the entire chain of nested transactions are all successfully committed,
103
103
  # the block is never called.
104
- #
105
- # If the transaction is already finalized, attempting to register a callback
106
- # will raise ActiveRecord::ActiveRecordError.
107
104
  def after_rollback(&block)
108
105
  @internal_transaction&.after_rollback(&block)
109
106
  end
@@ -115,7 +112,7 @@ module ActiveRecord
115
112
 
116
113
  # Returns true if the transaction doesn't exist or is finalized.
117
114
  def closed?
118
- @internal_transaction.nil? || @internal_transaction.state.finalized?
115
+ @internal_transaction.nil? || @internal_transaction.closed?
119
116
  end
120
117
 
121
118
  alias_method :blank?, :closed?
@@ -230,8 +230,28 @@ module ActiveRecord
230
230
  # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
231
231
  def transaction(**options, &block)
232
232
  with_connection do |connection|
233
- connection.transaction(**options, &block)
233
+ connection.pool.with_pool_transaction_isolation_level(ActiveRecord.default_transaction_isolation_level, connection.transaction_open?) do
234
+ connection.transaction(**options, &block)
235
+ end
236
+ end
237
+ end
238
+
239
+ # Makes all transactions the current pool use the isolation level initiated within the block.
240
+ def with_pool_transaction_isolation_level(isolation_level, &block)
241
+ if current_transaction.open?
242
+ raise ActiveRecord::TransactionIsolationError, "cannot set default isolation level while transaction is open"
234
243
  end
244
+
245
+ old_level = connection_pool.pool_transaction_isolation_level
246
+ connection_pool.pool_transaction_isolation_level = isolation_level
247
+ yield
248
+ ensure
249
+ connection_pool.pool_transaction_isolation_level = old_level
250
+ end
251
+
252
+ # Returns the default isolation level for the connection pool, set earlier by #with_pool_transaction_isolation_level.
253
+ def pool_transaction_isolation_level
254
+ connection_pool.pool_transaction_isolation_level
235
255
  end
236
256
 
237
257
  # Returns a representation of the current transaction state,
@@ -407,18 +427,20 @@ module ActiveRecord
407
427
  # instance.
408
428
  def with_transaction_returning_status
409
429
  self.class.with_connection do |connection|
410
- status = nil
411
- ensure_finalize = !connection.transaction_open?
430
+ connection.pool.with_pool_transaction_isolation_level(ActiveRecord.default_transaction_isolation_level, connection.transaction_open?) do
431
+ status = nil
432
+ ensure_finalize = !connection.transaction_open?
412
433
 
413
- connection.transaction do
414
- add_to_transaction(ensure_finalize || has_transactional_callbacks?)
415
- remember_transaction_record_state
434
+ connection.transaction do
435
+ add_to_transaction(ensure_finalize || has_transactional_callbacks?)
436
+ remember_transaction_record_state
416
437
 
417
- status = yield
418
- raise ActiveRecord::Rollback unless status
438
+ status = yield
439
+ raise ActiveRecord::Rollback unless status
440
+ end
441
+ @_last_transaction_return_status = status
442
+ status
419
443
  end
420
- @_last_transaction_return_status = status
421
- status
422
444
  end
423
445
  end
424
446
 
@@ -26,6 +26,7 @@ module ActiveRecord
26
26
  if block
27
27
  @mapping[key] = block
28
28
  else
29
+ value.freeze
29
30
  @mapping[key] = proc { value }
30
31
  end
31
32
  @cache.clear
@@ -50,7 +51,7 @@ module ActiveRecord
50
51
 
51
52
  private
52
53
  def perform_fetch(type, *args, &block)
53
- @mapping.fetch(type, block).call(type, *args)
54
+ @mapping.fetch(type, block).call(type, *args).freeze
54
55
  end
55
56
  end
56
57
  end
@@ -16,6 +16,13 @@ module ActiveRecord
16
16
  def default_timezone
17
17
  @timezone || ActiveRecord.default_timezone
18
18
  end
19
+
20
+ def ==(other)
21
+ super(other) && timezone == other.timezone
22
+ end
23
+
24
+ protected
25
+ attr_reader :timezone
19
26
  end
20
27
  end
21
28
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/json"
4
+
3
5
  module ActiveRecord
4
6
  module Type
5
7
  class Json < ActiveModel::Type::Value
@@ -11,11 +13,22 @@ module ActiveRecord
11
13
 
12
14
  def deserialize(value)
13
15
  return value unless value.is_a?(::String)
14
- ActiveSupport::JSON.decode(value) rescue nil
16
+ begin
17
+ ActiveSupport::JSON.decode(value)
18
+ rescue JSON::ParserError => e
19
+ # NOTE: This may hide json with duplicate keys. We don't really want to just ignore it
20
+ # but it's the best we can do in order to still allow updating columns that somehow already
21
+ # contain invalid json from some other source.
22
+ # See https://github.com/rails/rails/pull/55536
23
+ ActiveSupport.error_reporter.report(e, source: "application.active_record")
24
+ nil
25
+ end
15
26
  end
16
27
 
28
+ JSON_ENCODER = ActiveSupport::JSON::Encoding.json_encoder.new(escape: false)
29
+
17
30
  def serialize(value)
18
- ActiveSupport::JSON.encode(value) unless value.nil?
31
+ JSON_ENCODER.encode(value) unless value.nil?
19
32
  end
20
33
 
21
34
  def changed_in_place?(raw_old_value, new_value)
@@ -9,9 +9,10 @@ module ActiveRecord
9
9
 
10
10
  attr_reader :subtype, :coder
11
11
 
12
- def initialize(subtype, coder)
12
+ def initialize(subtype, coder, comparable: false)
13
13
  @subtype = subtype
14
14
  @coder = coder
15
+ @comparable = comparable
15
16
  super(subtype)
16
17
  end
17
18
 
@@ -34,9 +35,15 @@ module ActiveRecord
34
35
 
35
36
  def changed_in_place?(raw_old_value, value)
36
37
  return false if value.nil?
37
- raw_new_value = encoded(value)
38
- raw_old_value.nil? != raw_new_value.nil? ||
39
- subtype.changed_in_place?(raw_old_value, raw_new_value)
38
+
39
+ if @comparable
40
+ old_value = deserialize(raw_old_value)
41
+ old_value != value
42
+ else
43
+ raw_new_value = encoded(value)
44
+ raw_old_value.nil? != raw_new_value.nil? ||
45
+ subtype.changed_in_place?(raw_old_value, raw_new_value)
46
+ end
40
47
  end
41
48
 
42
49
  def accessor