activerecord 7.1.5.1 → 7.2.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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +645 -2329
  3. data/README.rdoc +15 -15
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +1 -1
  6. data/lib/active_record/associations/alias_tracker.rb +25 -19
  7. data/lib/active_record/associations/association.rb +15 -8
  8. data/lib/active_record/associations/belongs_to_association.rb +14 -7
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/belongs_to.rb +1 -0
  11. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  12. data/lib/active_record/associations/builder/has_many.rb +3 -4
  13. data/lib/active_record/associations/builder/has_one.rb +3 -4
  14. data/lib/active_record/associations/collection_association.rb +7 -1
  15. data/lib/active_record/associations/collection_proxy.rb +14 -1
  16. data/lib/active_record/associations/errors.rb +265 -0
  17. data/lib/active_record/associations/has_many_association.rb +1 -1
  18. data/lib/active_record/associations/has_many_through_association.rb +7 -1
  19. data/lib/active_record/associations/join_dependency/join_association.rb +27 -25
  20. data/lib/active_record/associations/nested_error.rb +47 -0
  21. data/lib/active_record/associations/preloader/association.rb +2 -1
  22. data/lib/active_record/associations/preloader/branch.rb +7 -1
  23. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  24. data/lib/active_record/associations/singular_association.rb +6 -0
  25. data/lib/active_record/associations/through_association.rb +1 -1
  26. data/lib/active_record/associations.rb +59 -292
  27. data/lib/active_record/attribute_assignment.rb +0 -2
  28. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  29. data/lib/active_record/attribute_methods/primary_key.rb +23 -55
  30. data/lib/active_record/attribute_methods/read.rb +1 -13
  31. data/lib/active_record/attribute_methods/serialization.rb +4 -24
  32. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -6
  33. data/lib/active_record/attribute_methods.rb +54 -63
  34. data/lib/active_record/attributes.rb +61 -47
  35. data/lib/active_record/autosave_association.rb +12 -29
  36. data/lib/active_record/base.rb +2 -3
  37. data/lib/active_record/callbacks.rb +1 -1
  38. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +24 -107
  39. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +1 -0
  40. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +270 -65
  41. data/lib/active_record/connection_adapters/abstract/database_statements.rb +34 -17
  42. data/lib/active_record/connection_adapters/abstract/query_cache.rb +189 -74
  43. data/lib/active_record/connection_adapters/abstract/quoting.rb +65 -91
  44. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  45. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +15 -6
  46. data/lib/active_record/connection_adapters/abstract/transaction.rb +125 -62
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -44
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +40 -10
  49. data/lib/active_record/connection_adapters/mysql/database_statements.rb +9 -1
  50. data/lib/active_record/connection_adapters/mysql/quoting.rb +43 -48
  51. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +6 -0
  52. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +16 -15
  53. data/lib/active_record/connection_adapters/mysql2_adapter.rb +5 -23
  54. data/lib/active_record/connection_adapters/pool_config.rb +7 -6
  55. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +27 -4
  56. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  57. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  58. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  59. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -0
  60. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +15 -11
  61. data/lib/active_record/connection_adapters/postgresql_adapter.rb +29 -24
  62. data/lib/active_record/connection_adapters/schema_cache.rb +123 -128
  63. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  64. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +10 -6
  65. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +44 -46
  66. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  67. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  68. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  69. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +25 -2
  70. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +125 -75
  71. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +15 -15
  72. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -48
  73. data/lib/active_record/connection_adapters.rb +121 -0
  74. data/lib/active_record/connection_handling.rb +56 -41
  75. data/lib/active_record/core.rb +85 -37
  76. data/lib/active_record/counter_cache.rb +18 -9
  77. data/lib/active_record/database_configurations/connection_url_resolver.rb +7 -2
  78. data/lib/active_record/database_configurations/database_config.rb +19 -4
  79. data/lib/active_record/database_configurations/hash_config.rb +38 -34
  80. data/lib/active_record/database_configurations/url_config.rb +20 -1
  81. data/lib/active_record/database_configurations.rb +1 -1
  82. data/lib/active_record/delegated_type.rb +24 -0
  83. data/lib/active_record/dynamic_matchers.rb +2 -2
  84. data/lib/active_record/encryption/encryptable_record.rb +3 -3
  85. data/lib/active_record/encryption/encrypted_attribute_type.rb +24 -4
  86. data/lib/active_record/encryption/encryptor.rb +18 -3
  87. data/lib/active_record/encryption/key_provider.rb +1 -1
  88. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  89. data/lib/active_record/encryption/message_serializer.rb +4 -0
  90. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  91. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  92. data/lib/active_record/enum.rb +18 -1
  93. data/lib/active_record/errors.rb +46 -20
  94. data/lib/active_record/explain.rb +13 -24
  95. data/lib/active_record/fixtures.rb +37 -31
  96. data/lib/active_record/future_result.rb +8 -4
  97. data/lib/active_record/gem_version.rb +2 -2
  98. data/lib/active_record/inheritance.rb +4 -2
  99. data/lib/active_record/insert_all.rb +18 -15
  100. data/lib/active_record/integration.rb +4 -1
  101. data/lib/active_record/internal_metadata.rb +48 -34
  102. data/lib/active_record/locking/optimistic.rb +7 -6
  103. data/lib/active_record/log_subscriber.rb +0 -21
  104. data/lib/active_record/message_pack.rb +1 -1
  105. data/lib/active_record/migration/command_recorder.rb +2 -3
  106. data/lib/active_record/migration/compatibility.rb +5 -3
  107. data/lib/active_record/migration/default_strategy.rb +4 -5
  108. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  109. data/lib/active_record/migration.rb +85 -76
  110. data/lib/active_record/model_schema.rb +31 -68
  111. data/lib/active_record/nested_attributes.rb +11 -3
  112. data/lib/active_record/normalization.rb +3 -7
  113. data/lib/active_record/persistence.rb +30 -352
  114. data/lib/active_record/query_cache.rb +19 -8
  115. data/lib/active_record/query_logs.rb +15 -0
  116. data/lib/active_record/querying.rb +21 -9
  117. data/lib/active_record/railtie.rb +37 -55
  118. data/lib/active_record/railties/controller_runtime.rb +13 -4
  119. data/lib/active_record/railties/databases.rake +40 -43
  120. data/lib/active_record/reflection.rb +98 -36
  121. data/lib/active_record/relation/batches/batch_enumerator.rb +15 -2
  122. data/lib/active_record/relation/batches.rb +14 -8
  123. data/lib/active_record/relation/calculations.rb +96 -63
  124. data/lib/active_record/relation/delegation.rb +8 -11
  125. data/lib/active_record/relation/finder_methods.rb +16 -2
  126. data/lib/active_record/relation/merger.rb +4 -6
  127. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  128. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -3
  129. data/lib/active_record/relation/predicate_builder.rb +3 -3
  130. data/lib/active_record/relation/query_methods.rb +224 -58
  131. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  132. data/lib/active_record/relation/spawn_methods.rb +2 -18
  133. data/lib/active_record/relation/where_clause.rb +7 -19
  134. data/lib/active_record/relation.rb +496 -72
  135. data/lib/active_record/result.rb +31 -44
  136. data/lib/active_record/runtime_registry.rb +39 -0
  137. data/lib/active_record/sanitization.rb +24 -19
  138. data/lib/active_record/schema.rb +8 -6
  139. data/lib/active_record/schema_dumper.rb +19 -9
  140. data/lib/active_record/schema_migration.rb +30 -14
  141. data/lib/active_record/scoping/named.rb +1 -0
  142. data/lib/active_record/signed_id.rb +20 -1
  143. data/lib/active_record/statement_cache.rb +7 -7
  144. data/lib/active_record/table_metadata.rb +1 -10
  145. data/lib/active_record/tasks/database_tasks.rb +69 -41
  146. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  147. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  148. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -1
  149. data/lib/active_record/test_fixtures.rb +86 -89
  150. data/lib/active_record/testing/query_assertions.rb +121 -0
  151. data/lib/active_record/timestamp.rb +2 -2
  152. data/lib/active_record/token_for.rb +22 -12
  153. data/lib/active_record/touch_later.rb +1 -1
  154. data/lib/active_record/transaction.rb +132 -0
  155. data/lib/active_record/transactions.rb +70 -14
  156. data/lib/active_record/translation.rb +0 -2
  157. data/lib/active_record/type/serialized.rb +1 -3
  158. data/lib/active_record/type_caster/connection.rb +4 -4
  159. data/lib/active_record/validations/associated.rb +9 -3
  160. data/lib/active_record/validations/uniqueness.rb +15 -10
  161. data/lib/active_record/validations.rb +4 -1
  162. data/lib/active_record.rb +148 -39
  163. data/lib/arel/alias_predication.rb +1 -1
  164. data/lib/arel/collectors/bind.rb +2 -0
  165. data/lib/arel/collectors/composite.rb +7 -0
  166. data/lib/arel/collectors/sql_string.rb +1 -1
  167. data/lib/arel/collectors/substitute_binds.rb +1 -1
  168. data/lib/arel/nodes/binary.rb +0 -6
  169. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  170. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  171. data/lib/arel/nodes/node.rb +4 -3
  172. data/lib/arel/nodes/sql_literal.rb +7 -0
  173. data/lib/arel/nodes.rb +2 -2
  174. data/lib/arel/predications.rb +1 -1
  175. data/lib/arel/select_manager.rb +1 -1
  176. data/lib/arel/tree_manager.rb +3 -2
  177. data/lib/arel/update_manager.rb +2 -1
  178. data/lib/arel/visitors/dot.rb +1 -0
  179. data/lib/arel/visitors/mysql.rb +9 -4
  180. data/lib/arel/visitors/postgresql.rb +1 -12
  181. data/lib/arel/visitors/sqlite.rb +25 -0
  182. data/lib/arel/visitors/to_sql.rb +29 -16
  183. data/lib/arel.rb +7 -3
  184. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  185. metadata +16 -10
@@ -6,9 +6,52 @@ module ActiveRecord
6
6
  module ConnectionAdapters
7
7
  module MySQL
8
8
  module Quoting # :nodoc:
9
+ extend ActiveSupport::Concern
10
+
9
11
  QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc:
10
12
  QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc:
11
13
 
14
+ module ClassMethods # :nodoc:
15
+ def column_name_matcher
16
+ /
17
+ \A
18
+ (
19
+ (?:
20
+ # `table_name`.`column_name` | function(one or no argument)
21
+ ((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`) | \w+\((?:|\g<2>)\))
22
+ )
23
+ (?:(?:\s+AS)?\s+(?:\w+|`\w+`))?
24
+ )
25
+ (?:\s*,\s*\g<1>)*
26
+ \z
27
+ /ix
28
+ end
29
+
30
+ def column_name_with_order_matcher
31
+ /
32
+ \A
33
+ (
34
+ (?:
35
+ # `table_name`.`column_name` | function(one or no argument)
36
+ ((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`) | \w+\((?:|\g<2>)\))
37
+ )
38
+ (?:\s+COLLATE\s+(?:\w+|"\w+"))?
39
+ (?:\s+ASC|\s+DESC)?
40
+ )
41
+ (?:\s*,\s*\g<1>)*
42
+ \z
43
+ /ix
44
+ end
45
+
46
+ def quote_column_name(name)
47
+ QUOTED_COLUMN_NAMES[name] ||= "`#{name.to_s.gsub('`', '``')}`".freeze
48
+ end
49
+
50
+ def quote_table_name(name)
51
+ QUOTED_TABLE_NAMES[name] ||= "`#{name.to_s.gsub('`', '``').gsub(".", "`.`")}`".freeze
52
+ end
53
+ end
54
+
12
55
  def cast_bound_value(value)
13
56
  case value
14
57
  when Rational
@@ -21,22 +64,11 @@ module ActiveRecord
21
64
  "1"
22
65
  when false
23
66
  "0"
24
- when ActiveSupport::Duration
25
- warn_quote_duration_deprecated
26
- value.to_s
27
67
  else
28
68
  value
29
69
  end
30
70
  end
31
71
 
32
- def quote_column_name(name)
33
- QUOTED_COLUMN_NAMES[name] ||= "`#{super.gsub('`', '``')}`"
34
- end
35
-
36
- def quote_table_name(name)
37
- QUOTED_TABLE_NAMES[name] ||= super.gsub(".", "`.`").freeze
38
- end
39
-
40
72
  def unquoted_true
41
73
  1
42
74
  end
@@ -84,43 +116,6 @@ module ActiveRecord
84
116
  super
85
117
  end
86
118
  end
87
-
88
- def column_name_matcher
89
- COLUMN_NAME
90
- end
91
-
92
- def column_name_with_order_matcher
93
- COLUMN_NAME_WITH_ORDER
94
- end
95
-
96
- COLUMN_NAME = /
97
- \A
98
- (
99
- (?:
100
- # `table_name`.`column_name` | function(one or no argument)
101
- ((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`) | \w+\((?:|\g<2>)\))
102
- )
103
- (?:(?:\s+AS)?\s+(?:\w+|`\w+`))?
104
- )
105
- (?:\s*,\s*\g<1>)*
106
- \z
107
- /ix
108
-
109
- COLUMN_NAME_WITH_ORDER = /
110
- \A
111
- (
112
- (?:
113
- # `table_name`.`column_name` | function(one or no argument)
114
- ((?:\w+\.|`\w+`\.)?(?:\w+|`\w+`) | \w+\((?:|\g<2>)\))
115
- )
116
- (?:\s+COLLATE\s+(?:\w+|"\w+"))?
117
- (?:\s+ASC|\s+DESC)?
118
- )
119
- (?:\s*,\s*\g<1>)*
120
- \z
121
- /ix
122
-
123
- private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
124
119
  end
125
120
  end
126
121
  end
@@ -68,6 +68,12 @@ module ActiveRecord
68
68
 
69
69
  IndexDefinition.new(*index, **options)
70
70
  end
71
+ rescue StatementInvalid => e
72
+ if e.message.match?(/Table '.+' doesn't exist/)
73
+ []
74
+ else
75
+ raise
76
+ end
71
77
  end
72
78
 
73
79
  def remove_column(table_name, column_name, type = nil, **options)
@@ -6,21 +6,16 @@ module ActiveRecord
6
6
  module DatabaseStatements
7
7
  # Returns an ActiveRecord::Result instance.
8
8
  def select_all(*, **) # :nodoc:
9
- result = nil
10
- with_raw_connection do |conn|
11
- result = if ExplainRegistry.collect? && prepared_statements
12
- unprepared_statement { super }
13
- else
14
- super
15
- end
16
- conn.abandon_results!
9
+ if ExplainRegistry.collect? && prepared_statements
10
+ unprepared_statement { super }
11
+ else
12
+ super
17
13
  end
18
- result
19
14
  end
20
15
 
21
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) # :nodoc:
16
+ def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
22
17
  if without_prepared_statement?(binds)
23
- execute_and_free(sql, name, async: async) do |result|
18
+ execute_and_free(sql, name, async: async, allow_retry: allow_retry) do |result|
24
19
  if result
25
20
  build_result(columns: result.fields, rows: result.to_a)
26
21
  else
@@ -60,13 +55,16 @@ module ActiveRecord
60
55
  combine_multi_statements(statements).each do |statement|
61
56
  with_raw_connection do |conn|
62
57
  raw_execute(statement, name)
63
- conn.abandon_results!
64
58
  end
65
59
  end
66
60
  end
67
61
 
68
62
  def last_inserted_id(result)
69
- @raw_connection&.last_id
63
+ if supports_insert_returning?
64
+ super
65
+ else
66
+ @raw_connection&.last_id
67
+ end
70
68
  end
71
69
 
72
70
  def multi_statements_enabled?
@@ -94,12 +92,14 @@ module ActiveRecord
94
92
  end
95
93
 
96
94
  def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
97
- log(sql, name, async: async) do
95
+ log(sql, name, async: async) do |notification_payload|
98
96
  with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
99
97
  sync_timezone_changes(conn)
100
98
  result = conn.query(sql)
99
+ conn.abandon_results!
101
100
  verified!
102
101
  handle_warnings(sql)
102
+ notification_payload[:row_count] = result&.size || 0
103
103
  result
104
104
  end
105
105
  end
@@ -113,7 +113,7 @@ module ActiveRecord
113
113
 
114
114
  type_casted_binds = type_casted_binds(binds)
115
115
 
116
- log(sql, name, binds, type_casted_binds, async: async) do
116
+ log(sql, name, binds, type_casted_binds, async: async) do |notification_payload|
117
117
  with_raw_connection do |conn|
118
118
  sync_timezone_changes(conn)
119
119
 
@@ -139,6 +139,7 @@ module ActiveRecord
139
139
  end
140
140
 
141
141
  ret = yield stmt, result
142
+ notification_payload[:row_count] = result&.size || 0
142
143
  result.free if result
143
144
  stmt.close unless cache_stmt
144
145
  ret
@@ -7,17 +7,6 @@ gem "mysql2", "~> 0.5"
7
7
  require "mysql2"
8
8
 
9
9
  module ActiveRecord
10
- module ConnectionHandling # :nodoc:
11
- def mysql2_adapter_class
12
- ConnectionAdapters::Mysql2Adapter
13
- end
14
-
15
- # Establishes a connection to the database that's used by all Active Record objects.
16
- def mysql2_connection(config)
17
- mysql2_adapter_class.new(config)
18
- end
19
- end
20
-
21
10
  module ConnectionAdapters
22
11
  # = Active Record MySQL2 Adapter
23
12
  class Mysql2Adapter < AbstractMysqlAdapter
@@ -116,22 +105,15 @@ module ActiveRecord
116
105
  end
117
106
 
118
107
  #--
119
- # QUOTING ==================================================
108
+ # CONNECTION MANAGEMENT ====================================
120
109
  #++
121
110
 
122
- # Quotes strings for use in SQL input.
123
- def quote_string(string)
124
- with_raw_connection(allow_retry: true, materialize_transactions: false) do |connection|
125
- connection.escape(string)
126
- end
111
+ def connected?
112
+ !(@raw_connection.nil? || @raw_connection.closed?)
127
113
  end
128
114
 
129
- #--
130
- # CONNECTION MANAGEMENT ====================================
131
- #++
132
-
133
115
  def active?
134
- !(@raw_connection.nil? || @raw_connection.closed?) && @lock.synchronize { @raw_connection&.ping } || false
116
+ connected? && @lock.synchronize { @raw_connection&.ping } || false
135
117
  end
136
118
 
137
119
  alias :reset! :reconnect!
@@ -180,7 +162,7 @@ module ActiveRecord
180
162
  end
181
163
 
182
164
  def full_version
183
- schema_cache.database_version.full_version_string
165
+ database_version.full_version_string
184
166
  end
185
167
 
186
168
  def get_full_version
@@ -3,10 +3,10 @@
3
3
  module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  class PoolConfig # :nodoc:
6
- include Mutex_m
6
+ include MonitorMixin
7
7
 
8
8
  attr_reader :db_config, :role, :shard
9
- attr_writer :schema_reflection
9
+ attr_writer :schema_reflection, :server_version
10
10
  attr_accessor :connection_class
11
11
 
12
12
  def schema_reflection
@@ -28,6 +28,7 @@ module ActiveRecord
28
28
 
29
29
  def initialize(connection_class, db_config, role, shard)
30
30
  super()
31
+ @server_version = nil
31
32
  @connection_class = connection_class
32
33
  @db_config = db_config
33
34
  @role = role
@@ -36,6 +37,10 @@ module ActiveRecord
36
37
  INSTANCES[self] = self
37
38
  end
38
39
 
40
+ def server_version(connection)
41
+ @server_version || synchronize { @server_version ||= connection.get_database_version }
42
+ end
43
+
39
44
  def connection_name
40
45
  if connection_class.primary_class?
41
46
  "ActiveRecord::Base"
@@ -45,8 +50,6 @@ module ActiveRecord
45
50
  end
46
51
 
47
52
  def disconnect!(automatic_reconnect: false)
48
- ActiveSupport::ForkTracker.check!
49
-
50
53
  return unless @pool
51
54
 
52
55
  synchronize do
@@ -60,8 +63,6 @@ module ActiveRecord
60
63
  end
61
64
 
62
65
  def pool
63
- ActiveSupport::ForkTracker.check!
64
-
65
66
  @pool || synchronize { @pool ||= ConnectionAdapters::ConnectionPool.new(self) }
66
67
  end
67
68
 
@@ -14,10 +14,11 @@ module ActiveRecord
14
14
  def query(sql, name = nil) # :nodoc:
15
15
  mark_transaction_written_if_write(sql)
16
16
 
17
- log(sql, name) do
17
+ log(sql, name) do |notification_payload|
18
18
  with_raw_connection do |conn|
19
19
  result = conn.async_exec(sql).map_types!(@type_map_for_results).values
20
20
  verified!
21
+ notification_payload[:row_count] = result.count
21
22
  result
22
23
  end
23
24
  end
@@ -50,11 +51,12 @@ module ActiveRecord
50
51
  end
51
52
 
52
53
  def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
53
- log(sql, name, async: async) do
54
+ log(sql, name, async: async) do |notification_payload|
54
55
  with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
55
56
  result = conn.async_exec(sql)
56
57
  verified!
57
58
  handle_warnings(result)
59
+ notification_payload[:row_count] = result.count
58
60
  result
59
61
  end
60
62
  end
@@ -69,7 +71,7 @@ module ActiveRecord
69
71
  fmod = result.fmod i
70
72
  types[fname] = types[i] = get_oid_type(ftype, fmod, fname)
71
73
  end
72
- build_result(columns: fields, rows: result.values, column_types: types)
74
+ build_result(columns: fields, rows: result.values, column_types: types.freeze)
73
75
  end
74
76
  end
75
77
 
@@ -122,7 +124,7 @@ module ActiveRecord
122
124
  end
123
125
 
124
126
  # From https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-CURRENT
125
- HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP").freeze # :nodoc:
127
+ HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP", retryable: true).freeze # :nodoc:
126
128
  private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP
127
129
 
128
130
  def high_precision_current_timestamp
@@ -135,6 +137,27 @@ module ActiveRecord
135
137
  "EXPLAIN (#{options.join(", ").upcase})"
136
138
  end
137
139
 
140
+ # Set when constraints will be checked for the current transaction.
141
+ #
142
+ # Not passing any specific constraint names will set the value for all deferrable constraints.
143
+ #
144
+ # [<tt>deferred</tt>]
145
+ # Valid values are +:deferred+ or +:immediate+.
146
+ #
147
+ # See https://www.postgresql.org/docs/current/sql-set-constraints.html
148
+ def set_constraints(deferred, *constraints)
149
+ unless %i[deferred immediate].include?(deferred)
150
+ raise ArgumentError, "deferred must be :deferred or :immediate"
151
+ end
152
+
153
+ constraints = if constraints.empty?
154
+ "ALL"
155
+ else
156
+ constraints.map { |c| quote_table_name(c) }.join(", ")
157
+ end
158
+ execute("SET CONSTRAINTS #{constraints} #{deferred.to_s.upcase}")
159
+ end
160
+
138
161
  private
139
162
  IDLE_TRANSACTION_STATUSES = [PG::PQTRANS_IDLE, PG::PQTRANS_INTRANS, PG::PQTRANS_INERROR]
140
163
  private_constant :IDLE_TRANSACTION_STATUSES
@@ -33,7 +33,7 @@ module ActiveRecord
33
33
  when ::Numeric
34
34
  # Sometimes operations on Times returns just float number of seconds so we need to handle that.
35
35
  # Example: Time.current - (Time.current + 1.hour) # => -3600.000001776 (Float)
36
- value.seconds.iso8601(precision: self.precision)
36
+ ActiveSupport::Duration.build(value).iso8601(precision: self.precision)
37
37
  else
38
38
  super
39
39
  end
@@ -6,6 +6,7 @@ module ActiveRecord
6
6
  module OID # :nodoc:
7
7
  class Uuid < Type::Value # :nodoc:
8
8
  ACCEPTABLE_UUID = %r{\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z}
9
+ CANONICAL_UUID = %r{\A[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}\z}
9
10
 
10
11
  alias :serialize :deserialize
11
12
 
@@ -15,18 +16,27 @@ module ActiveRecord
15
16
 
16
17
  def changed?(old_value, new_value, _new_value_before_type_cast)
17
18
  old_value.class != new_value.class ||
18
- new_value && old_value.casecmp(new_value) != 0
19
+ new_value != old_value
19
20
  end
20
21
 
21
22
  def changed_in_place?(raw_old_value, new_value)
22
23
  raw_old_value.class != new_value.class ||
23
- new_value && raw_old_value.casecmp(new_value) != 0
24
+ new_value != raw_old_value
24
25
  end
25
26
 
26
27
  private
27
28
  def cast_value(value)
28
- casted = value.to_s
29
- casted if casted.match?(ACCEPTABLE_UUID)
29
+ value = value.to_s
30
+ format_uuid(value) if value.match?(ACCEPTABLE_UUID)
31
+ end
32
+
33
+ def format_uuid(uuid)
34
+ if uuid.match?(CANONICAL_UUID)
35
+ uuid
36
+ else
37
+ uuid = uuid.delete("{}-").downcase
38
+ "#{uuid[..7]}-#{uuid[8..11]}-#{uuid[12..15]}-#{uuid[16..19]}-#{uuid[20..]}"
39
+ end
30
40
  end
31
41
  end
32
42
  end
@@ -4,9 +4,62 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module PostgreSQL
6
6
  module Quoting
7
+ extend ActiveSupport::Concern
8
+
7
9
  QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc:
8
10
  QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc:
9
11
 
12
+ module ClassMethods # :nodoc:
13
+ def column_name_matcher
14
+ /
15
+ \A
16
+ (
17
+ (?:
18
+ # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
19
+ ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)? | \w+\((?:|\g<2>)\)(?:::\w+)?)
20
+ )
21
+ (?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
22
+ )
23
+ (?:\s*,\s*\g<1>)*
24
+ \z
25
+ /ix
26
+ end
27
+
28
+ def column_name_with_order_matcher
29
+ /
30
+ \A
31
+ (
32
+ (?:
33
+ # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
34
+ ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)? | \w+\((?:|\g<2>)\)(?:::\w+)?)
35
+ )
36
+ (?:\s+COLLATE\s+"\w+")?
37
+ (?:\s+ASC|\s+DESC)?
38
+ (?:\s+NULLS\s+(?:FIRST|LAST))?
39
+ )
40
+ (?:\s*,\s*\g<1>)*
41
+ \z
42
+ /ix
43
+ end
44
+
45
+ # Quotes column names for use in SQL queries.
46
+ def quote_column_name(name) # :nodoc:
47
+ QUOTED_COLUMN_NAMES[name] ||= PG::Connection.quote_ident(name.to_s).freeze
48
+ end
49
+
50
+ # Checks the following cases:
51
+ #
52
+ # - table_name
53
+ # - "table.name"
54
+ # - schema_name.table_name
55
+ # - schema_name."table.name"
56
+ # - "schema.name".table_name
57
+ # - "schema.name"."table.name"
58
+ def quote_table_name(name) # :nodoc:
59
+ QUOTED_TABLE_NAMES[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
60
+ end
61
+ end
62
+
10
63
  class IntegerOutOf64BitRange < StandardError
11
64
  def initialize(msg)
12
65
  super(msg)
@@ -77,30 +130,13 @@ module ActiveRecord
77
130
  end
78
131
  end
79
132
 
80
- # Checks the following cases:
81
- #
82
- # - table_name
83
- # - "table.name"
84
- # - schema_name.table_name
85
- # - schema_name."table.name"
86
- # - "schema.name".table_name
87
- # - "schema.name"."table.name"
88
- def quote_table_name(name) # :nodoc:
89
- QUOTED_TABLE_NAMES[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
90
- end
91
-
92
- # Quotes schema names for use in SQL queries.
93
- def quote_schema_name(name)
94
- PG::Connection.quote_ident(name)
95
- end
96
-
97
133
  def quote_table_name_for_assignment(table, attr)
98
134
  quote_column_name(attr)
99
135
  end
100
136
 
101
- # Quotes column names for use in SQL queries.
102
- def quote_column_name(name) # :nodoc:
103
- QUOTED_COLUMN_NAMES[name] ||= PG::Connection.quote_ident(super).freeze
137
+ # Quotes schema names for use in SQL queries.
138
+ def quote_schema_name(schema_name)
139
+ quote_column_name(schema_name)
104
140
  end
105
141
 
106
142
  # Quote date/time values for use in SQL input.
@@ -143,6 +179,8 @@ module ActiveRecord
143
179
  encode_array(value)
144
180
  when Range
145
181
  encode_range(value)
182
+ when Rational
183
+ value.to_f
146
184
  else
147
185
  super
148
186
  end
@@ -153,44 +191,6 @@ module ActiveRecord
153
191
  type_map.lookup(column.oid, column.fmod, column.sql_type)
154
192
  end
155
193
 
156
- def column_name_matcher
157
- COLUMN_NAME
158
- end
159
-
160
- def column_name_with_order_matcher
161
- COLUMN_NAME_WITH_ORDER
162
- end
163
-
164
- COLUMN_NAME = /
165
- \A
166
- (
167
- (?:
168
- # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
169
- ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)? | \w+\((?:|\g<2>)\)(?:::\w+)?)
170
- )
171
- (?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
172
- )
173
- (?:\s*,\s*\g<1>)*
174
- \z
175
- /ix
176
-
177
- COLUMN_NAME_WITH_ORDER = /
178
- \A
179
- (
180
- (?:
181
- # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
182
- ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)? | \w+\((?:|\g<2>)\)(?:::\w+)?)
183
- )
184
- (?:\s+COLLATE\s+"\w+")?
185
- (?:\s+ASC|\s+DESC)?
186
- (?:\s+NULLS\s+(?:FIRST|LAST))?
187
- )
188
- (?:\s*,\s*\g<1>)*
189
- \z
190
- /ix
191
-
192
- private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
193
-
194
194
  private
195
195
  def lookup_cast_type(sql_type)
196
196
  super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)
@@ -332,6 +332,26 @@ module ActiveRecord
332
332
  def remove_unique_constraint(*args)
333
333
  @base.remove_unique_constraint(name, *args)
334
334
  end
335
+
336
+ # Validates the given constraint on the table.
337
+ #
338
+ # t.check_constraint("price > 0", name: "price_check", validate: false)
339
+ # t.validate_constraint "price_check"
340
+ #
341
+ # See {connection.validate_constraint}[rdoc-ref:SchemaStatements#validate_constraint]
342
+ def validate_constraint(*args)
343
+ @base.validate_constraint(name, *args)
344
+ end
345
+
346
+ # Validates the given check constraint on the table
347
+ #
348
+ # t.check_constraint("price > 0", name: "price_check", validate: false)
349
+ # t.validate_check_constraint name: "price_check"
350
+ #
351
+ # See {connection.validate_check_constraint}[rdoc-ref:SchemaStatements#validate_check_constraint]
352
+ def validate_check_constraint(*args)
353
+ @base.validate_check_constraint(name, *args)
354
+ end
335
355
  end
336
356
 
337
357
  # = Active Record PostgreSQL Adapter Alter \Table
@@ -112,7 +112,7 @@ module ActiveRecord
112
112
 
113
113
  orders = {}
114
114
  opclasses = {}
115
- include_columns = include ? include.split(",").map(&:strip) : []
115
+ include_columns = include ? include.split(",").map { |c| Utils.unquote_identifier(c.strip.gsub('""', '"')) } : []
116
116
 
117
117
  if indkey.include?(0)
118
118
  columns = expressions
@@ -209,8 +209,16 @@ module ActiveRecord
209
209
  end
210
210
 
211
211
  # Creates a schema for the given schema name.
212
- def create_schema(schema_name)
213
- execute "CREATE SCHEMA #{quote_schema_name(schema_name)}"
212
+ def create_schema(schema_name, force: nil, if_not_exists: nil)
213
+ if force && if_not_exists
214
+ raise ArgumentError, "Options `:force` and `:if_not_exists` cannot be used simultaneously."
215
+ end
216
+
217
+ if force
218
+ drop_schema(schema_name, if_exists: true)
219
+ end
220
+
221
+ execute("CREATE SCHEMA#{' IF NOT EXISTS' if if_not_exists} #{quote_schema_name(schema_name)}")
214
222
  end
215
223
 
216
224
  # Drops the schema for the given schema name.
@@ -524,14 +532,6 @@ module ActiveRecord
524
532
  end
525
533
 
526
534
  def add_foreign_key(from_table, to_table, **options)
527
- if options[:deferrable] == true
528
- ActiveRecord.deprecator.warn(<<~MSG)
529
- `deferrable: true` is deprecated in favor of `deferrable: :immediate`, and will be removed in Rails 7.2.
530
- MSG
531
-
532
- options[:deferrable] = :immediate
533
- end
534
-
535
535
  assert_valid_deferrable(options[:deferrable])
536
536
 
537
537
  super
@@ -692,6 +692,10 @@ module ActiveRecord
692
692
  # The constraint name. Defaults to <tt>excl_rails_<identifier></tt>.
693
693
  # [<tt>:deferrable</tt>]
694
694
  # Specify whether or not the exclusion constraint should be deferrable. Valid values are +false+ or +:immediate+ or +:deferred+ to specify the default behavior. Defaults to +false+.
695
+ # [<tt>:using</tt>]
696
+ # Specify which index method to use when creating this exclusion constraint (e.g. +:btree+, +:gist+ etc).
697
+ # [<tt>:where</tt>]
698
+ # Specify an exclusion constraint on a subset of the table (internally PostgreSQL creates a partial index for this).
695
699
  def add_exclusion_constraint(table_name, expression, **options)
696
700
  options = exclusion_constraint_options(table_name, expression, options)
697
701
  at = create_alter_table(table_name)