activerecord 7.0.0 → 7.2.1

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 (289) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +515 -1268
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +31 -31
  5. data/examples/performance.rb +2 -2
  6. data/lib/active_record/aggregations.rb +16 -13
  7. data/lib/active_record/association_relation.rb +2 -2
  8. data/lib/active_record/associations/alias_tracker.rb +25 -19
  9. data/lib/active_record/associations/association.rb +35 -12
  10. data/lib/active_record/associations/association_scope.rb +16 -9
  11. data/lib/active_record/associations/belongs_to_association.rb +23 -8
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  13. data/lib/active_record/associations/builder/association.rb +3 -3
  14. data/lib/active_record/associations/builder/belongs_to.rb +22 -8
  15. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +3 -7
  16. data/lib/active_record/associations/builder/has_many.rb +3 -4
  17. data/lib/active_record/associations/builder/has_one.rb +3 -4
  18. data/lib/active_record/associations/builder/singular_association.rb +4 -0
  19. data/lib/active_record/associations/collection_association.rb +28 -17
  20. data/lib/active_record/associations/collection_proxy.rb +36 -13
  21. data/lib/active_record/associations/errors.rb +265 -0
  22. data/lib/active_record/associations/foreign_association.rb +10 -3
  23. data/lib/active_record/associations/has_many_association.rb +28 -18
  24. data/lib/active_record/associations/has_many_through_association.rb +10 -6
  25. data/lib/active_record/associations/has_one_association.rb +10 -3
  26. data/lib/active_record/associations/join_dependency/join_association.rb +27 -25
  27. data/lib/active_record/associations/join_dependency.rb +18 -14
  28. data/lib/active_record/associations/nested_error.rb +47 -0
  29. data/lib/active_record/associations/preloader/association.rb +33 -8
  30. data/lib/active_record/associations/preloader/branch.rb +7 -1
  31. data/lib/active_record/associations/preloader/through_association.rb +2 -4
  32. data/lib/active_record/associations/preloader.rb +13 -10
  33. data/lib/active_record/associations/singular_association.rb +7 -1
  34. data/lib/active_record/associations/through_association.rb +22 -11
  35. data/lib/active_record/associations.rb +378 -491
  36. data/lib/active_record/attribute_assignment.rb +1 -13
  37. data/lib/active_record/attribute_methods/before_type_cast.rb +17 -0
  38. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  39. data/lib/active_record/attribute_methods/dirty.rb +53 -35
  40. data/lib/active_record/attribute_methods/primary_key.rb +45 -25
  41. data/lib/active_record/attribute_methods/query.rb +28 -16
  42. data/lib/active_record/attribute_methods/read.rb +8 -7
  43. data/lib/active_record/attribute_methods/serialization.rb +153 -70
  44. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -6
  45. data/lib/active_record/attribute_methods/write.rb +6 -6
  46. data/lib/active_record/attribute_methods.rb +153 -40
  47. data/lib/active_record/attributes.rb +63 -48
  48. data/lib/active_record/autosave_association.rb +70 -38
  49. data/lib/active_record/base.rb +12 -8
  50. data/lib/active_record/callbacks.rb +16 -32
  51. data/lib/active_record/coders/column_serializer.rb +61 -0
  52. data/lib/active_record/coders/json.rb +1 -1
  53. data/lib/active_record/coders/yaml_column.rb +70 -34
  54. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +124 -132
  55. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +2 -0
  56. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +4 -1
  57. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +297 -88
  58. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -0
  59. data/lib/active_record/connection_adapters/abstract/database_statements.rb +160 -45
  60. data/lib/active_record/connection_adapters/abstract/query_cache.rb +215 -63
  61. data/lib/active_record/connection_adapters/abstract/quoting.rb +83 -65
  62. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  63. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +18 -4
  64. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +163 -29
  65. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +14 -1
  66. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +319 -135
  67. data/lib/active_record/connection_adapters/abstract/transaction.rb +367 -75
  68. data/lib/active_record/connection_adapters/abstract_adapter.rb +512 -126
  69. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +282 -119
  70. data/lib/active_record/connection_adapters/column.rb +9 -0
  71. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  72. data/lib/active_record/connection_adapters/mysql/database_statements.rb +27 -140
  73. data/lib/active_record/connection_adapters/mysql/quoting.rb +64 -52
  74. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  75. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +10 -1
  76. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +8 -2
  77. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +45 -14
  78. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +152 -0
  79. data/lib/active_record/connection_adapters/mysql2_adapter.rb +101 -68
  80. data/lib/active_record/connection_adapters/pool_config.rb +20 -10
  81. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  82. data/lib/active_record/connection_adapters/postgresql/column.rb +16 -3
  83. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +101 -48
  84. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  85. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +6 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +2 -2
  87. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  88. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +3 -2
  89. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +11 -2
  90. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +4 -2
  91. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  92. data/lib/active_record/connection_adapters/postgresql/quoting.rb +94 -61
  93. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +6 -10
  94. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  95. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +151 -2
  96. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +53 -0
  97. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +379 -66
  98. data/lib/active_record/connection_adapters/postgresql/utils.rb +9 -10
  99. data/lib/active_record/connection_adapters/postgresql_adapter.rb +370 -203
  100. data/lib/active_record/connection_adapters/schema_cache.rb +302 -79
  101. data/lib/active_record/connection_adapters/sqlite3/column.rb +62 -0
  102. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +60 -43
  103. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +61 -46
  104. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  105. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +20 -0
  106. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  107. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +64 -22
  108. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +321 -110
  109. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  110. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
  111. data/lib/active_record/connection_adapters/trilogy_adapter.rb +229 -0
  112. data/lib/active_record/connection_adapters.rb +124 -1
  113. data/lib/active_record/connection_handling.rb +98 -106
  114. data/lib/active_record/core.rb +220 -177
  115. data/lib/active_record/counter_cache.rb +68 -34
  116. data/lib/active_record/database_configurations/connection_url_resolver.rb +8 -2
  117. data/lib/active_record/database_configurations/database_config.rb +26 -5
  118. data/lib/active_record/database_configurations/hash_config.rb +52 -34
  119. data/lib/active_record/database_configurations/url_config.rb +37 -12
  120. data/lib/active_record/database_configurations.rb +88 -35
  121. data/lib/active_record/delegated_type.rb +40 -11
  122. data/lib/active_record/deprecator.rb +7 -0
  123. data/lib/active_record/destroy_association_async_job.rb +3 -1
  124. data/lib/active_record/disable_joins_association_relation.rb +1 -1
  125. data/lib/active_record/dynamic_matchers.rb +2 -2
  126. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  127. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  128. data/lib/active_record/encryption/config.rb +25 -1
  129. data/lib/active_record/encryption/configurable.rb +13 -14
  130. data/lib/active_record/encryption/context.rb +10 -3
  131. data/lib/active_record/encryption/contexts.rb +8 -4
  132. data/lib/active_record/encryption/derived_secret_key_provider.rb +9 -3
  133. data/lib/active_record/encryption/deterministic_key_provider.rb +1 -1
  134. data/lib/active_record/encryption/encryptable_record.rb +47 -25
  135. data/lib/active_record/encryption/encrypted_attribute_type.rb +49 -14
  136. data/lib/active_record/encryption/encryptor.rb +25 -10
  137. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +3 -3
  138. data/lib/active_record/encryption/extended_deterministic_queries.rb +83 -86
  139. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +3 -3
  140. data/lib/active_record/encryption/key_generator.rb +12 -1
  141. data/lib/active_record/encryption/message.rb +1 -1
  142. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  143. data/lib/active_record/encryption/message_serializer.rb +6 -0
  144. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  145. data/lib/active_record/encryption/properties.rb +4 -4
  146. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  147. data/lib/active_record/encryption/scheme.rb +23 -22
  148. data/lib/active_record/encryption.rb +1 -0
  149. data/lib/active_record/enum.rb +131 -27
  150. data/lib/active_record/errors.rb +151 -31
  151. data/lib/active_record/explain.rb +21 -12
  152. data/lib/active_record/explain_subscriber.rb +1 -1
  153. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  154. data/lib/active_record/fixture_set/render_context.rb +2 -0
  155. data/lib/active_record/fixture_set/table_row.rb +29 -8
  156. data/lib/active_record/fixtures.rb +169 -99
  157. data/lib/active_record/future_result.rb +47 -8
  158. data/lib/active_record/gem_version.rb +3 -3
  159. data/lib/active_record/inheritance.rb +34 -18
  160. data/lib/active_record/insert_all.rb +72 -22
  161. data/lib/active_record/integration.rb +13 -10
  162. data/lib/active_record/internal_metadata.rb +124 -20
  163. data/lib/active_record/locking/optimistic.rb +39 -24
  164. data/lib/active_record/locking/pessimistic.rb +8 -5
  165. data/lib/active_record/log_subscriber.rb +28 -27
  166. data/lib/active_record/marshalling.rb +56 -0
  167. data/lib/active_record/message_pack.rb +124 -0
  168. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  169. data/lib/active_record/middleware/database_selector.rb +18 -13
  170. data/lib/active_record/middleware/shard_selector.rb +7 -5
  171. data/lib/active_record/migration/command_recorder.rb +110 -13
  172. data/lib/active_record/migration/compatibility.rb +174 -64
  173. data/lib/active_record/migration/default_strategy.rb +22 -0
  174. data/lib/active_record/migration/execution_strategy.rb +19 -0
  175. data/lib/active_record/migration/pending_migration_connection.rb +21 -0
  176. data/lib/active_record/migration.rb +292 -125
  177. data/lib/active_record/model_schema.rb +113 -112
  178. data/lib/active_record/nested_attributes.rb +35 -9
  179. data/lib/active_record/normalization.rb +163 -0
  180. data/lib/active_record/persistence.rb +177 -345
  181. data/lib/active_record/promise.rb +84 -0
  182. data/lib/active_record/query_cache.rb +19 -25
  183. data/lib/active_record/query_logs.rb +102 -51
  184. data/lib/active_record/query_logs_formatter.rb +41 -0
  185. data/lib/active_record/querying.rb +34 -9
  186. data/lib/active_record/railtie.rb +153 -100
  187. data/lib/active_record/railties/controller_runtime.rb +24 -10
  188. data/lib/active_record/railties/databases.rake +148 -152
  189. data/lib/active_record/railties/job_runtime.rb +23 -0
  190. data/lib/active_record/readonly_attributes.rb +32 -5
  191. data/lib/active_record/reflection.rb +278 -69
  192. data/lib/active_record/relation/batches/batch_enumerator.rb +20 -5
  193. data/lib/active_record/relation/batches.rb +198 -63
  194. data/lib/active_record/relation/calculations.rb +293 -108
  195. data/lib/active_record/relation/delegation.rb +31 -20
  196. data/lib/active_record/relation/finder_methods.rb +93 -18
  197. data/lib/active_record/relation/merger.rb +6 -6
  198. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  199. data/lib/active_record/relation/predicate_builder/association_query_value.rb +38 -4
  200. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +10 -7
  201. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  202. data/lib/active_record/relation/predicate_builder.rb +28 -16
  203. data/lib/active_record/relation/query_attribute.rb +25 -1
  204. data/lib/active_record/relation/query_methods.rb +625 -107
  205. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  206. data/lib/active_record/relation/spawn_methods.rb +5 -4
  207. data/lib/active_record/relation/where_clause.rb +7 -19
  208. data/lib/active_record/relation.rb +602 -96
  209. data/lib/active_record/result.rb +55 -52
  210. data/lib/active_record/runtime_registry.rb +63 -1
  211. data/lib/active_record/sanitization.rb +76 -30
  212. data/lib/active_record/schema.rb +39 -23
  213. data/lib/active_record/schema_dumper.rb +82 -30
  214. data/lib/active_record/schema_migration.rb +75 -24
  215. data/lib/active_record/scoping/default.rb +20 -12
  216. data/lib/active_record/scoping/named.rb +3 -2
  217. data/lib/active_record/scoping.rb +2 -1
  218. data/lib/active_record/secure_password.rb +60 -0
  219. data/lib/active_record/secure_token.rb +21 -3
  220. data/lib/active_record/serialization.rb +5 -0
  221. data/lib/active_record/signed_id.rb +29 -8
  222. data/lib/active_record/statement_cache.rb +7 -7
  223. data/lib/active_record/store.rb +16 -11
  224. data/lib/active_record/suppressor.rb +3 -1
  225. data/lib/active_record/table_metadata.rb +7 -3
  226. data/lib/active_record/tasks/database_tasks.rb +191 -121
  227. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  228. data/lib/active_record/tasks/postgresql_database_tasks.rb +17 -15
  229. data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -7
  230. data/lib/active_record/test_fixtures.rb +174 -152
  231. data/lib/active_record/testing/query_assertions.rb +121 -0
  232. data/lib/active_record/timestamp.rb +31 -17
  233. data/lib/active_record/token_for.rb +123 -0
  234. data/lib/active_record/touch_later.rb +12 -7
  235. data/lib/active_record/transaction.rb +132 -0
  236. data/lib/active_record/transactions.rb +109 -27
  237. data/lib/active_record/translation.rb +1 -3
  238. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  239. data/lib/active_record/type/internal/timezone.rb +7 -2
  240. data/lib/active_record/type/serialized.rb +9 -7
  241. data/lib/active_record/type/time.rb +4 -0
  242. data/lib/active_record/type_caster/connection.rb +4 -4
  243. data/lib/active_record/validations/absence.rb +1 -1
  244. data/lib/active_record/validations/associated.rb +12 -6
  245. data/lib/active_record/validations/numericality.rb +5 -4
  246. data/lib/active_record/validations/presence.rb +5 -28
  247. data/lib/active_record/validations/uniqueness.rb +63 -14
  248. data/lib/active_record/validations.rb +12 -5
  249. data/lib/active_record/version.rb +1 -1
  250. data/lib/active_record.rb +266 -30
  251. data/lib/arel/alias_predication.rb +1 -1
  252. data/lib/arel/collectors/bind.rb +2 -0
  253. data/lib/arel/collectors/composite.rb +7 -0
  254. data/lib/arel/collectors/sql_string.rb +1 -1
  255. data/lib/arel/collectors/substitute_binds.rb +1 -1
  256. data/lib/arel/errors.rb +10 -0
  257. data/lib/arel/factory_methods.rb +4 -0
  258. data/lib/arel/filter_predications.rb +1 -1
  259. data/lib/arel/nodes/binary.rb +6 -7
  260. data/lib/arel/nodes/bound_sql_literal.rb +65 -0
  261. data/lib/arel/nodes/cte.rb +36 -0
  262. data/lib/arel/nodes/filter.rb +1 -1
  263. data/lib/arel/nodes/fragments.rb +35 -0
  264. data/lib/arel/nodes/homogeneous_in.rb +1 -9
  265. data/lib/arel/nodes/leading_join.rb +8 -0
  266. data/lib/arel/nodes/{and.rb → nary.rb} +9 -2
  267. data/lib/arel/nodes/node.rb +115 -5
  268. data/lib/arel/nodes/sql_literal.rb +13 -0
  269. data/lib/arel/nodes/table_alias.rb +4 -0
  270. data/lib/arel/nodes.rb +6 -2
  271. data/lib/arel/predications.rb +3 -1
  272. data/lib/arel/select_manager.rb +1 -1
  273. data/lib/arel/table.rb +9 -5
  274. data/lib/arel/tree_manager.rb +8 -3
  275. data/lib/arel/update_manager.rb +2 -1
  276. data/lib/arel/visitors/dot.rb +1 -0
  277. data/lib/arel/visitors/mysql.rb +17 -5
  278. data/lib/arel/visitors/postgresql.rb +1 -12
  279. data/lib/arel/visitors/to_sql.rb +112 -34
  280. data/lib/arel/visitors/visitor.rb +2 -2
  281. data/lib/arel.rb +21 -3
  282. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  283. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  284. data/lib/rails/generators/active_record/migration.rb +3 -1
  285. data/lib/rails/generators/active_record/model/USAGE +113 -0
  286. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  287. metadata +59 -17
  288. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  289. data/lib/active_record/null_relation.rb +0 -63
@@ -3,8 +3,6 @@
3
3
  module ActiveRecord
4
4
  module Tasks # :nodoc:
5
5
  class SQLiteDatabaseTasks # :nodoc:
6
- delegate :connection, :establish_connection, to: ActiveRecord::Base
7
-
8
6
  def self.using_database_configurations?
9
7
  true
10
8
  end
@@ -17,25 +15,26 @@ module ActiveRecord
17
15
  def create
18
16
  raise DatabaseAlreadyExists if File.exist?(db_config.database)
19
17
 
20
- establish_connection(db_config)
18
+ establish_connection
21
19
  connection
22
20
  end
23
21
 
24
22
  def drop
25
- require "pathname"
26
- path = Pathname.new(db_config.database)
27
- file = path.absolute? ? path.to_s : File.join(root, path)
28
-
23
+ db_path = db_config.database
24
+ file = File.absolute_path?(db_path) ? db_path : File.join(root, db_path)
29
25
  FileUtils.rm(file)
26
+ FileUtils.rm_f(["#{file}-shm", "#{file}-wal"])
30
27
  rescue Errno::ENOENT => error
31
28
  raise NoDatabaseError.new(error.message)
32
29
  end
33
30
 
34
31
  def purge
32
+ connection.disconnect!
35
33
  drop
36
34
  rescue NoDatabaseError
37
35
  ensure
38
36
  create
37
+ connection.reconnect!
39
38
  end
40
39
 
41
40
  def charset
@@ -49,6 +48,7 @@ module ActiveRecord
49
48
 
50
49
  ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
51
50
  if ignore_tables.any?
51
+ ignore_tables = connection.data_sources.select { |table| ignore_tables.any? { |pattern| pattern === table } }
52
52
  condition = ignore_tables.map { |table| connection.quote(table) }.join(", ")
53
53
  args << "SELECT sql FROM sqlite_master WHERE tbl_name NOT IN (#{condition}) ORDER BY tbl_name, type DESC, name"
54
54
  else
@@ -65,6 +65,15 @@ module ActiveRecord
65
65
  private
66
66
  attr_reader :db_config, :root
67
67
 
68
+ def connection
69
+ ActiveRecord::Base.lease_connection
70
+ end
71
+
72
+ def establish_connection(config = db_config)
73
+ ActiveRecord::Base.establish_connection(config)
74
+ connection.connect!
75
+ end
76
+
68
77
  def run_cmd(cmd, args, out)
69
78
  fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out)
70
79
  end
@@ -13,17 +13,31 @@ module ActiveRecord
13
13
 
14
14
  def after_teardown # :nodoc:
15
15
  super
16
+ ensure
16
17
  teardown_fixtures
17
18
  end
18
19
 
19
20
  included do
20
- class_attribute :fixture_path, instance_writer: false
21
+ ##
22
+ # :singleton-method: fixture_paths
23
+ #
24
+ # Returns the ActiveRecord::FixtureSet collection
25
+
26
+ ##
27
+ # :singleton-method: fixture_paths=
28
+ #
29
+ # :call-seq:
30
+ # fixture_paths=(fixture_paths)
31
+ class_attribute :fixture_paths, instance_writer: false, default: []
21
32
  class_attribute :fixture_table_names, default: []
22
33
  class_attribute :fixture_class_names, default: {}
23
34
  class_attribute :use_transactional_tests, default: true
24
35
  class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances
25
36
  class_attribute :pre_loaded_fixtures, default: false
26
37
  class_attribute :lock_threads, default: true
38
+ class_attribute :fixture_sets, default: {}
39
+
40
+ ActiveSupport.run_load_hooks(:active_record_fixtures, self)
27
41
  end
28
42
 
29
43
  module ClassMethods
@@ -41,51 +55,36 @@ module ActiveRecord
41
55
 
42
56
  def fixtures(*fixture_set_names)
43
57
  if fixture_set_names.first == :all
44
- raise StandardError, "No fixture path found. Please set `#{self}.fixture_path`." if fixture_path.blank?
45
- fixture_set_names = Dir[::File.join(fixture_path, "{**,*}/*.{yml}")].uniq
46
- fixture_set_names.reject! { |f| f.start_with?(file_fixture_path.to_s) } if defined?(file_fixture_path) && file_fixture_path
47
- fixture_set_names.map! { |f| f[fixture_path.to_s.size..-5].delete_prefix("/") }
58
+ raise StandardError, "No fixture path found. Please set `#{self}.fixture_paths`." if fixture_paths.blank?
59
+ fixture_set_names = fixture_paths.flat_map do |path|
60
+ names = Dir[::File.join(path, "{**,*}/*.{yml}")].uniq
61
+ names.reject! { |f| f.start_with?(file_fixture_path.to_s) } if defined?(file_fixture_path) && file_fixture_path
62
+ names.map! { |f| f[path.to_s.size..-5].delete_prefix("/") }
63
+ end.uniq
48
64
  else
49
65
  fixture_set_names = fixture_set_names.flatten.map(&:to_s)
50
66
  end
51
67
 
52
- self.fixture_table_names |= fixture_set_names
68
+ self.fixture_table_names = (fixture_table_names | fixture_set_names).sort
53
69
  setup_fixture_accessors(fixture_set_names)
54
70
  end
55
71
 
56
72
  def setup_fixture_accessors(fixture_set_names = nil)
57
73
  fixture_set_names = Array(fixture_set_names || fixture_table_names)
58
- methods = Module.new do
74
+ unless fixture_set_names.empty?
75
+ self.fixture_sets = fixture_sets.dup
59
76
  fixture_set_names.each do |fs_name|
60
- fs_name = fs_name.to_s
61
- accessor_name = fs_name.tr("/", "_").to_sym
62
-
63
- define_method(accessor_name) do |*fixture_names|
64
- force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
65
- return_single_record = fixture_names.size == 1
66
- fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty?
67
-
68
- @fixture_cache[fs_name] ||= {}
69
-
70
- instances = fixture_names.map do |f_name|
71
- f_name = f_name.to_s if f_name.is_a?(Symbol)
72
- @fixture_cache[fs_name].delete(f_name) if force_reload
73
-
74
- if @loaded_fixtures[fs_name][f_name]
75
- @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find
76
- else
77
- raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'"
78
- end
79
- end
80
-
81
- return_single_record ? instances.first : instances
82
- end
83
- private accessor_name
77
+ key = fs_name.to_s.include?("/") ? -fs_name.to_s.tr("/", "_") : fs_name
78
+ key = -key.to_s if key.is_a?(Symbol)
79
+ fs_name = -fs_name.to_s if fs_name.is_a?(Symbol)
80
+ fixture_sets[key] = fs_name
84
81
  end
85
82
  end
86
- include methods
87
83
  end
88
84
 
85
+ # Prevents automatically wrapping each specified test in a transaction,
86
+ # to allow application logic transactions to be tested in a top-level
87
+ # (non-nested) context.
89
88
  def uses_transaction(*methods)
90
89
  @uses_transaction = [] unless defined?(@uses_transaction)
91
90
  @uses_transaction.concat methods.map(&:to_s)
@@ -97,95 +96,109 @@ module ActiveRecord
97
96
  end
98
97
  end
99
98
 
100
- def run_in_transaction?
101
- use_transactional_tests &&
102
- !self.class.uses_transaction?(name)
99
+ # Generic fixture accessor for fixture names that may conflict with other methods.
100
+ #
101
+ # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
102
+ # assert_equal "Ruby on Rails", fixture(:web_sites, :rubyonrails).name
103
+ def fixture(fixture_set_name, *fixture_names)
104
+ active_record_fixture(fixture_set_name, *fixture_names)
103
105
  end
104
106
 
105
- def setup_fixtures(config = ActiveRecord::Base)
106
- if pre_loaded_fixtures && !use_transactional_tests
107
- raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests"
107
+ private
108
+ def run_in_transaction?
109
+ use_transactional_tests &&
110
+ !self.class.uses_transaction?(name)
108
111
  end
109
112
 
110
- @fixture_cache = {}
111
- @fixture_connections = []
112
- @@already_loaded_fixtures ||= {}
113
- @connection_subscriber = nil
114
- @legacy_saved_pool_configs = Hash.new { |hash, key| hash[key] = {} }
115
- @saved_pool_configs = Hash.new { |hash, key| hash[key] = {} }
116
-
117
- # Load fixtures once and begin transaction.
118
- if run_in_transaction?
119
- if @@already_loaded_fixtures[self.class]
120
- @loaded_fixtures = @@already_loaded_fixtures[self.class]
113
+ def setup_fixtures(config = ActiveRecord::Base)
114
+ if pre_loaded_fixtures && !use_transactional_tests
115
+ raise RuntimeError, "pre_loaded_fixtures requires use_transactional_tests"
116
+ end
117
+
118
+ @fixture_cache = {}
119
+ @fixture_cache_key = [self.class.fixture_table_names.dup, self.class.fixture_paths.dup, self.class.fixture_class_names.dup]
120
+ @fixture_connection_pools = []
121
+ @@already_loaded_fixtures ||= {}
122
+ @connection_subscriber = nil
123
+ @saved_pool_configs = Hash.new { |hash, key| hash[key] = {} }
124
+
125
+ if run_in_transaction?
126
+ # Load fixtures once and begin transaction.
127
+ @loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key]
128
+ unless @loaded_fixtures
129
+ @@already_loaded_fixtures.clear
130
+ @loaded_fixtures = @@already_loaded_fixtures[@fixture_cache_key] = load_fixtures(config)
131
+ end
132
+
133
+ setup_transactional_fixtures
121
134
  else
135
+ # Load fixtures for every test.
136
+ ActiveRecord::FixtureSet.reset_cache
137
+ invalidate_already_loaded_fixtures
122
138
  @loaded_fixtures = load_fixtures(config)
123
- @@already_loaded_fixtures[self.class] = @loaded_fixtures
124
139
  end
125
140
 
141
+ # Instantiate fixtures for every test if requested.
142
+ instantiate_fixtures if use_instantiated_fixtures
143
+ end
144
+
145
+ def teardown_fixtures
146
+ # Rollback changes if a transaction is active.
147
+ if run_in_transaction?
148
+ teardown_transactional_fixtures
149
+ else
150
+ ActiveRecord::FixtureSet.reset_cache
151
+ invalidate_already_loaded_fixtures
152
+ end
153
+
154
+ ActiveRecord::Base.connection_handler.clear_active_connections!(:all)
155
+ end
156
+
157
+ def invalidate_already_loaded_fixtures
158
+ @@already_loaded_fixtures.clear
159
+ end
160
+
161
+ def setup_transactional_fixtures
162
+ setup_shared_connection_pool
163
+
126
164
  # Begin transactions for connections already established
127
- @fixture_connections = enlist_fixture_connections
128
- @fixture_connections.each do |connection|
129
- connection.begin_transaction joinable: false, _lazy: false
130
- connection.pool.lock_thread = true if lock_threads
165
+ @fixture_connection_pools = ActiveRecord::Base.connection_handler.connection_pool_list(:writing)
166
+ @fixture_connection_pools.each do |pool|
167
+ pool.pin_connection!(lock_threads)
168
+ pool.lease_connection
131
169
  end
132
170
 
133
171
  # When connections are established in the future, begin a transaction too
134
172
  @connection_subscriber = ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
135
- spec_name = payload[:spec_name] if payload.key?(:spec_name)
173
+ connection_name = payload[:connection_name] if payload.key?(:connection_name)
136
174
  shard = payload[:shard] if payload.key?(:shard)
137
- setup_shared_connection_pool
138
175
 
139
- if spec_name
140
- begin
141
- connection = ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
142
- rescue ConnectionNotEstablished
143
- connection = nil
144
- end
176
+ if connection_name
177
+ pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(connection_name, shard: shard)
178
+ if pool
179
+ setup_shared_connection_pool
145
180
 
146
- if connection && !@fixture_connections.include?(connection)
147
- connection.begin_transaction joinable: false, _lazy: false
148
- connection.pool.lock_thread = true if lock_threads
149
- @fixture_connections << connection
181
+ unless @fixture_connection_pools.include?(pool)
182
+ pool.pin_connection!(lock_threads)
183
+ pool.lease_connection
184
+ @fixture_connection_pools << pool
185
+ end
150
186
  end
151
187
  end
152
188
  end
153
-
154
- # Load fixtures for every test.
155
- else
156
- ActiveRecord::FixtureSet.reset_cache
157
- @@already_loaded_fixtures[self.class] = nil
158
- @loaded_fixtures = load_fixtures(config)
159
189
  end
160
190
 
161
- # Instantiate fixtures for every test if requested.
162
- instantiate_fixtures if use_instantiated_fixtures
163
- end
164
-
165
- def teardown_fixtures
166
- # Rollback changes if a transaction is active.
167
- if run_in_transaction?
191
+ def teardown_transactional_fixtures
168
192
  ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber
169
- @fixture_connections.each do |connection|
170
- connection.rollback_transaction if connection.transaction_open?
171
- connection.pool.lock_thread = false
193
+ unless @fixture_connection_pools.map(&:unpin_connection!).all?
194
+ # Something caused the transaction to be committed or rolled back
195
+ # We can no longer trust the database is in a clean state.
196
+ @@already_loaded_fixtures.clear
172
197
  end
173
- @fixture_connections.clear
198
+ @fixture_connection_pools.clear
174
199
  teardown_shared_connection_pool
175
- else
176
- ActiveRecord::FixtureSet.reset_cache
177
200
  end
178
201
 
179
- ActiveRecord::Base.clear_active_connections!
180
- end
181
-
182
- def enlist_fixture_connections
183
- setup_shared_connection_pool
184
-
185
- ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
186
- end
187
-
188
- private
189
202
  # Shares the writing connection pool with connections on
190
203
  # other handlers.
191
204
  #
@@ -193,79 +206,43 @@ module ActiveRecord
193
206
  # need to share a connection pool so that the reading connection
194
207
  # can see data in the open transaction on the writing connection.
195
208
  def setup_shared_connection_pool
196
- if ActiveRecord.legacy_connection_handling
197
- writing_handler = ActiveRecord::Base.connection_handlers[ActiveRecord.writing_role]
198
-
199
- ActiveRecord::Base.connection_handlers.values.each do |handler|
200
- if handler != writing_handler
201
- handler.connection_pool_names.each do |name|
202
- writing_pool_manager = writing_handler.send(:owner_to_pool_manager)[name]
203
- return unless writing_pool_manager
204
-
205
- pool_manager = handler.send(:owner_to_pool_manager)[name]
206
- @legacy_saved_pool_configs[handler][name] ||= {}
207
- pool_manager.shard_names.each do |shard_name|
208
- writing_pool_config = writing_pool_manager.get_pool_config(nil, shard_name)
209
- pool_config = pool_manager.get_pool_config(nil, shard_name)
210
- next if pool_config == writing_pool_config
211
-
212
- @legacy_saved_pool_configs[handler][name][shard_name] = pool_config
213
- pool_manager.set_pool_config(nil, shard_name, writing_pool_config)
214
- end
215
- end
216
- end
217
- end
218
- else
219
- handler = ActiveRecord::Base.connection_handler
220
-
221
- handler.connection_pool_names.each do |name|
222
- pool_manager = handler.send(:owner_to_pool_manager)[name]
223
- pool_manager.shard_names.each do |shard_name|
224
- writing_pool_config = pool_manager.get_pool_config(ActiveRecord.writing_role, shard_name)
225
- @saved_pool_configs[name][shard_name] ||= {}
226
- pool_manager.role_names.each do |role|
227
- next unless pool_config = pool_manager.get_pool_config(role, shard_name)
228
- next if pool_config == writing_pool_config
229
-
230
- @saved_pool_configs[name][shard_name][role] = pool_config
231
- pool_manager.set_pool_config(role, shard_name, writing_pool_config)
232
- end
209
+ handler = ActiveRecord::Base.connection_handler
210
+
211
+ handler.connection_pool_names.each do |name|
212
+ pool_manager = handler.send(:connection_name_to_pool_manager)[name]
213
+ pool_manager.shard_names.each do |shard_name|
214
+ writing_pool_config = pool_manager.get_pool_config(ActiveRecord.writing_role, shard_name)
215
+ @saved_pool_configs[name][shard_name] ||= {}
216
+ pool_manager.role_names.each do |role|
217
+ next unless pool_config = pool_manager.get_pool_config(role, shard_name)
218
+ next if pool_config == writing_pool_config
219
+
220
+ @saved_pool_configs[name][shard_name][role] = pool_config
221
+ pool_manager.set_pool_config(role, shard_name, writing_pool_config)
233
222
  end
234
223
  end
235
224
  end
236
225
  end
237
226
 
238
227
  def teardown_shared_connection_pool
239
- if ActiveRecord.legacy_connection_handling
240
- @legacy_saved_pool_configs.each_pair do |handler, names|
241
- names.each_pair do |name, shards|
242
- shards.each_pair do |shard_name, pool_config|
243
- pool_manager = handler.send(:owner_to_pool_manager)[name]
244
- pool_manager.set_pool_config(nil, shard_name, pool_config)
245
- end
246
- end
247
- end
248
- else
249
- handler = ActiveRecord::Base.connection_handler
228
+ handler = ActiveRecord::Base.connection_handler
250
229
 
251
- @saved_pool_configs.each_pair do |name, shards|
252
- pool_manager = handler.send(:owner_to_pool_manager)[name]
253
- shards.each_pair do |shard_name, roles|
254
- roles.each_pair do |role, pool_config|
255
- next unless pool_manager.get_pool_config(role, shard_name)
230
+ @saved_pool_configs.each_pair do |name, shards|
231
+ pool_manager = handler.send(:connection_name_to_pool_manager)[name]
232
+ shards.each_pair do |shard_name, roles|
233
+ roles.each_pair do |role, pool_config|
234
+ next unless pool_manager.get_pool_config(role, shard_name)
256
235
 
257
- pool_manager.set_pool_config(role, shard_name, pool_config)
258
- end
236
+ pool_manager.set_pool_config(role, shard_name, pool_config)
259
237
  end
260
238
  end
261
239
  end
262
240
 
263
- @legacy_saved_pool_configs.clear
264
241
  @saved_pool_configs.clear
265
242
  end
266
243
 
267
244
  def load_fixtures(config)
268
- ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config).index_by(&:name)
245
+ ActiveRecord::FixtureSet.create_fixtures(fixture_paths, fixture_table_names, fixture_class_names, config).index_by(&:name)
269
246
  end
270
247
 
271
248
  def instantiate_fixtures
@@ -283,5 +260,50 @@ module ActiveRecord
283
260
  def load_instances?
284
261
  use_instantiated_fixtures != :no_instances
285
262
  end
263
+
264
+ def method_missing(method, ...)
265
+ if fixture_sets.key?(method.name)
266
+ active_record_fixture(method, ...)
267
+ else
268
+ super
269
+ end
270
+ end
271
+
272
+ def respond_to_missing?(method, include_private = false)
273
+ if include_private && fixture_sets.key?(method.name)
274
+ true
275
+ else
276
+ super
277
+ end
278
+ end
279
+
280
+ def active_record_fixture(fixture_set_name, *fixture_names)
281
+ if fs_name = fixture_sets[fixture_set_name.name]
282
+ access_fixture(fs_name, *fixture_names)
283
+ else
284
+ raise StandardError, "No fixture set named '#{fixture_set_name.inspect}'"
285
+ end
286
+ end
287
+
288
+ def access_fixture(fs_name, *fixture_names)
289
+ force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
290
+ return_single_record = fixture_names.size == 1
291
+
292
+ fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty?
293
+ @fixture_cache[fs_name] ||= {}
294
+
295
+ instances = fixture_names.map do |f_name|
296
+ f_name = f_name.to_s if f_name.is_a?(Symbol)
297
+ @fixture_cache[fs_name].delete(f_name) if force_reload
298
+
299
+ if @loaded_fixtures[fs_name][f_name]
300
+ @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find
301
+ else
302
+ raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'"
303
+ end
304
+ end
305
+
306
+ return_single_record ? instances.first : instances
307
+ end
286
308
  end
287
309
  end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Assertions
5
+ module QueryAssertions
6
+ # Asserts that the number of SQL queries executed in the given block matches the expected count.
7
+ #
8
+ # # Check for exact number of queries
9
+ # assert_queries_count(1) { Post.first }
10
+ #
11
+ # # Check for any number of queries
12
+ # assert_queries_count { Post.first }
13
+ #
14
+ # If the +:include_schema+ option is provided, any queries (including schema related) are counted.
15
+ #
16
+ # assert_queries_count(1, include_schema: true) { Post.columns }
17
+ #
18
+ def assert_queries_count(count = nil, include_schema: false, &block)
19
+ ActiveRecord::Base.lease_connection.materialize_transactions
20
+
21
+ counter = SQLCounter.new
22
+ ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
23
+ result = _assert_nothing_raised_or_warn("assert_queries_count", &block)
24
+ queries = include_schema ? counter.log_all : counter.log
25
+ if count
26
+ assert_equal count, queries.size, "#{queries.size} instead of #{count} queries were executed. Queries: #{queries.join("\n\n")}"
27
+ else
28
+ assert_operator queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
29
+ end
30
+ result
31
+ end
32
+ end
33
+
34
+ # Asserts that no SQL queries are executed in the given block.
35
+ #
36
+ # assert_no_queries { post.comments }
37
+ #
38
+ # If the +:include_schema+ option is provided, any queries (including schema related) are counted.
39
+ #
40
+ # assert_no_queries(include_schema: true) { Post.columns }
41
+ #
42
+ def assert_no_queries(include_schema: false, &block)
43
+ assert_queries_count(0, include_schema: include_schema, &block)
44
+ end
45
+
46
+ # Asserts that the SQL queries executed in the given block match expected pattern.
47
+ #
48
+ # # Check for exact number of queries
49
+ # assert_queries_match(/LIMIT \?/, count: 1) { Post.first }
50
+ #
51
+ # # Check for any number of queries
52
+ # assert_queries_match(/LIMIT \?/) { Post.first }
53
+ #
54
+ # If the +:include_schema+ option is provided, any queries (including schema related)
55
+ # that match the matcher are considered.
56
+ #
57
+ # assert_queries_match(/FROM pg_attribute/i, include_schema: true) { Post.columns }
58
+ #
59
+ def assert_queries_match(match, count: nil, include_schema: false, &block)
60
+ ActiveRecord::Base.lease_connection.materialize_transactions
61
+
62
+ counter = SQLCounter.new
63
+ ActiveSupport::Notifications.subscribed(counter, "sql.active_record") do
64
+ result = _assert_nothing_raised_or_warn("assert_queries_match", &block)
65
+ queries = include_schema ? counter.log_all : counter.log
66
+ matched_queries = queries.select { |query| match === query }
67
+
68
+ if count
69
+ assert_equal count, matched_queries.size, "#{matched_queries.size} instead of #{count} queries were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
70
+ else
71
+ assert_operator matched_queries.size, :>=, 1, "1 or more queries expected, but none were executed.#{queries.empty? ? '' : "\nQueries:\n#{queries.join("\n")}"}"
72
+ end
73
+
74
+ result
75
+ end
76
+ end
77
+
78
+ # Asserts that no SQL queries matching the pattern are executed in the given block.
79
+ #
80
+ # assert_no_queries_match(/SELECT/i) { post.comments }
81
+ #
82
+ # If the +:include_schema+ option is provided, any queries (including schema related)
83
+ # that match the matcher are counted.
84
+ #
85
+ # assert_no_queries_match(/FROM pg_attribute/i, include_schema: true) { Post.columns }
86
+ #
87
+ def assert_no_queries_match(match, include_schema: false, &block)
88
+ assert_queries_match(match, count: 0, include_schema: include_schema, &block)
89
+ end
90
+
91
+ class SQLCounter # :nodoc:
92
+ attr_reader :log_full, :log_all
93
+
94
+ def initialize
95
+ @log_full = []
96
+ @log_all = []
97
+ end
98
+
99
+ def log
100
+ @log_full.map(&:first)
101
+ end
102
+
103
+ def call(*, payload)
104
+ return if payload[:cached]
105
+
106
+ sql = payload[:sql]
107
+ @log_all << sql
108
+
109
+ unless payload[:name] == "SCHEMA"
110
+ bound_values = (payload[:binds] || []).map do |value|
111
+ value = value.value_for_database if value.respond_to?(:value_for_database)
112
+ value
113
+ end
114
+
115
+ @log_full << [sql, bound_values]
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end