activerecord 7.2.4 → 8.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1013 -878
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/association_relation.rb +2 -1
  5. data/lib/active_record/associations/association.rb +69 -27
  6. data/lib/active_record/associations/association_scope.rb +1 -1
  7. data/lib/active_record/associations/belongs_to_association.rb +2 -2
  8. data/lib/active_record/associations/builder/association.rb +23 -11
  9. data/lib/active_record/associations/builder/belongs_to.rb +36 -8
  10. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  11. data/lib/active_record/associations/builder/has_one.rb +1 -1
  12. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  13. data/lib/active_record/associations/collection_association.rb +7 -3
  14. data/lib/active_record/associations/collection_proxy.rb +26 -4
  15. data/lib/active_record/associations/deprecation.rb +88 -0
  16. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  17. data/lib/active_record/associations/errors.rb +3 -0
  18. data/lib/active_record/associations/foreign_association.rb +3 -1
  19. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  20. data/lib/active_record/associations/has_one_association.rb +1 -0
  21. data/lib/active_record/associations/join_dependency.rb +3 -2
  22. data/lib/active_record/associations/preloader/association.rb +3 -3
  23. data/lib/active_record/associations/preloader/batch.rb +7 -1
  24. data/lib/active_record/associations/preloader/branch.rb +1 -0
  25. data/lib/active_record/associations/singular_association.rb +8 -3
  26. data/lib/active_record/associations.rb +192 -24
  27. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  28. data/lib/active_record/attribute_methods/primary_key.rb +4 -8
  29. data/lib/active_record/attribute_methods/query.rb +34 -0
  30. data/lib/active_record/attribute_methods/serialization.rb +18 -5
  31. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
  32. data/lib/active_record/attributes.rb +3 -0
  33. data/lib/active_record/autosave_association.rb +69 -27
  34. data/lib/active_record/base.rb +1 -2
  35. data/lib/active_record/callbacks.rb +1 -2
  36. data/lib/active_record/coders/column_serializer.rb +1 -1
  37. data/lib/active_record/coders/json.rb +16 -6
  38. data/lib/active_record/coders/yaml_column.rb +2 -2
  39. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +35 -28
  40. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -4
  41. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +68 -13
  42. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +429 -93
  43. data/lib/active_record/connection_adapters/abstract/database_statements.rb +148 -74
  44. data/lib/active_record/connection_adapters/abstract/query_cache.rb +27 -5
  45. data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -25
  46. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +11 -7
  47. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +32 -35
  48. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  49. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +124 -34
  50. data/lib/active_record/connection_adapters/abstract/transaction.rb +40 -8
  51. data/lib/active_record/connection_adapters/abstract_adapter.rb +129 -63
  52. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +132 -65
  53. data/lib/active_record/connection_adapters/column.rb +14 -4
  54. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  55. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  56. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  57. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +41 -10
  58. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +79 -66
  59. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +89 -94
  60. data/lib/active_record/connection_adapters/mysql2_adapter.rb +2 -10
  61. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  62. data/lib/active_record/connection_adapters/postgresql/column.rb +8 -2
  63. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +79 -61
  64. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +3 -3
  65. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  66. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +46 -3
  67. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  68. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  69. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  70. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +9 -17
  71. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +14 -33
  72. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +71 -32
  73. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +156 -68
  74. data/lib/active_record/connection_adapters/postgresql_adapter.rb +78 -105
  75. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  76. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  77. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +90 -98
  78. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  79. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  80. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
  81. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +15 -16
  82. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +104 -37
  83. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  84. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +41 -66
  85. data/lib/active_record/connection_adapters/trilogy_adapter.rb +15 -18
  86. data/lib/active_record/connection_adapters.rb +1 -56
  87. data/lib/active_record/connection_handling.rb +25 -2
  88. data/lib/active_record/core.rb +33 -17
  89. data/lib/active_record/counter_cache.rb +35 -8
  90. data/lib/active_record/database_configurations/database_config.rb +9 -1
  91. data/lib/active_record/database_configurations/hash_config.rb +74 -9
  92. data/lib/active_record/database_configurations/url_config.rb +15 -3
  93. data/lib/active_record/database_configurations.rb +7 -3
  94. data/lib/active_record/delegated_type.rb +1 -1
  95. data/lib/active_record/disable_joins_association_relation.rb +5 -1
  96. data/lib/active_record/dynamic_matchers.rb +54 -69
  97. data/lib/active_record/encryption/config.rb +3 -1
  98. data/lib/active_record/encryption/encryptable_record.rb +8 -8
  99. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  100. data/lib/active_record/encryption/encrypted_fixtures.rb +12 -7
  101. data/lib/active_record/encryption/encryptor.rb +28 -8
  102. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  103. data/lib/active_record/encryption/scheme.rb +9 -2
  104. data/lib/active_record/enum.rb +33 -30
  105. data/lib/active_record/errors.rb +33 -9
  106. data/lib/active_record/explain.rb +1 -1
  107. data/lib/active_record/explain_registry.rb +51 -2
  108. data/lib/active_record/filter_attribute_handler.rb +76 -0
  109. data/lib/active_record/fixtures.rb +2 -4
  110. data/lib/active_record/future_result.rb +15 -9
  111. data/lib/active_record/gem_version.rb +2 -2
  112. data/lib/active_record/inheritance.rb +9 -4
  113. data/lib/active_record/insert_all.rb +14 -9
  114. data/lib/active_record/locking/optimistic.rb +8 -1
  115. data/lib/active_record/locking/pessimistic.rb +5 -0
  116. data/lib/active_record/log_subscriber.rb +31 -29
  117. data/lib/active_record/message_pack.rb +13 -0
  118. data/lib/active_record/middleware/shard_selector.rb +34 -17
  119. data/lib/active_record/migration/command_recorder.rb +56 -13
  120. data/lib/active_record/migration/compatibility.rb +37 -24
  121. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  122. data/lib/active_record/migration.rb +48 -42
  123. data/lib/active_record/model_schema.rb +40 -15
  124. data/lib/active_record/nested_attributes.rb +14 -12
  125. data/lib/active_record/persistence.rb +163 -133
  126. data/lib/active_record/query_cache.rb +22 -15
  127. data/lib/active_record/query_logs.rb +100 -52
  128. data/lib/active_record/query_logs_formatter.rb +17 -28
  129. data/lib/active_record/querying.rb +21 -9
  130. data/lib/active_record/railtie.rb +35 -30
  131. data/lib/active_record/railties/controller_runtime.rb +11 -6
  132. data/lib/active_record/railties/databases.rake +26 -38
  133. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  134. data/lib/active_record/railties/job_runtime.rb +10 -11
  135. data/lib/active_record/reflection.rb +54 -22
  136. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  137. data/lib/active_record/relation/batches.rb +147 -73
  138. data/lib/active_record/relation/calculations.rb +60 -43
  139. data/lib/active_record/relation/delegation.rb +25 -15
  140. data/lib/active_record/relation/finder_methods.rb +73 -43
  141. data/lib/active_record/relation/merger.rb +12 -10
  142. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  143. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -9
  144. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +8 -8
  145. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  146. data/lib/active_record/relation/predicate_builder.rb +23 -8
  147. data/lib/active_record/relation/query_attribute.rb +3 -1
  148. data/lib/active_record/relation/query_methods.rb +200 -96
  149. data/lib/active_record/relation/spawn_methods.rb +7 -7
  150. data/lib/active_record/relation/where_clause.rb +2 -9
  151. data/lib/active_record/relation.rb +127 -87
  152. data/lib/active_record/result.rb +109 -24
  153. data/lib/active_record/runtime_registry.rb +42 -58
  154. data/lib/active_record/sanitization.rb +9 -6
  155. data/lib/active_record/schema.rb +1 -1
  156. data/lib/active_record/schema_dumper.rb +17 -10
  157. data/lib/active_record/schema_migration.rb +2 -1
  158. data/lib/active_record/scoping/named.rb +5 -2
  159. data/lib/active_record/scoping.rb +0 -1
  160. data/lib/active_record/signed_id.rb +44 -16
  161. data/lib/active_record/statement_cache.rb +24 -20
  162. data/lib/active_record/store.rb +53 -22
  163. data/lib/active_record/structured_event_subscriber.rb +85 -0
  164. data/lib/active_record/table_metadata.rb +6 -23
  165. data/lib/active_record/tasks/abstract_tasks.rb +83 -0
  166. data/lib/active_record/tasks/database_tasks.rb +85 -85
  167. data/lib/active_record/tasks/mysql_database_tasks.rb +4 -58
  168. data/lib/active_record/tasks/postgresql_database_tasks.rb +7 -40
  169. data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -28
  170. data/lib/active_record/test_databases.rb +18 -4
  171. data/lib/active_record/test_fixtures.rb +52 -6
  172. data/lib/active_record/testing/query_assertions.rb +8 -2
  173. data/lib/active_record/timestamp.rb +4 -2
  174. data/lib/active_record/token_for.rb +1 -1
  175. data/lib/active_record/transaction.rb +2 -5
  176. data/lib/active_record/transactions.rb +56 -32
  177. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  178. data/lib/active_record/type/internal/timezone.rb +7 -0
  179. data/lib/active_record/type/json.rb +13 -2
  180. data/lib/active_record/type/serialized.rb +16 -4
  181. data/lib/active_record/type/type_map.rb +1 -1
  182. data/lib/active_record/type_caster/connection.rb +2 -1
  183. data/lib/active_record/validations/associated.rb +1 -1
  184. data/lib/active_record/validations/uniqueness.rb +8 -8
  185. data/lib/active_record.rb +85 -50
  186. data/lib/arel/alias_predication.rb +2 -0
  187. data/lib/arel/collectors/bind.rb +2 -2
  188. data/lib/arel/collectors/sql_string.rb +1 -1
  189. data/lib/arel/collectors/substitute_binds.rb +2 -2
  190. data/lib/arel/crud.rb +6 -11
  191. data/lib/arel/nodes/binary.rb +1 -1
  192. data/lib/arel/nodes/count.rb +2 -2
  193. data/lib/arel/nodes/function.rb +4 -10
  194. data/lib/arel/nodes/named_function.rb +2 -2
  195. data/lib/arel/nodes/node.rb +2 -2
  196. data/lib/arel/nodes/sql_literal.rb +1 -1
  197. data/lib/arel/nodes.rb +0 -2
  198. data/lib/arel/predications.rb +1 -3
  199. data/lib/arel/select_manager.rb +7 -2
  200. data/lib/arel/table.rb +3 -7
  201. data/lib/arel/visitors/dot.rb +0 -3
  202. data/lib/arel/visitors/postgresql.rb +55 -0
  203. data/lib/arel/visitors/sqlite.rb +55 -8
  204. data/lib/arel/visitors/to_sql.rb +4 -22
  205. data/lib/arel.rb +3 -1
  206. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  207. metadata +15 -12
  208. data/lib/active_record/explain_subscriber.rb +0 -34
  209. data/lib/active_record/normalization.rb +0 -163
  210. data/lib/active_record/relation/record_fetch_warning.rb +0 -52
@@ -5,6 +5,23 @@ module ActiveRecord
5
5
  module PostgreSQL
6
6
  class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
7
7
  private
8
+ attr_accessor :schema_name
9
+
10
+ def initialize(connection, options = {})
11
+ super
12
+
13
+ @dump_schemas =
14
+ case ActiveRecord.dump_schemas
15
+ when :schema_search_path
16
+ connection.current_schemas
17
+ when String
18
+ schema_names = ActiveRecord.dump_schemas.split(",").map(&:strip)
19
+ schema_names & connection.schema_names
20
+ else
21
+ connection.schema_names
22
+ end
23
+ end
24
+
8
25
  def extensions(stream)
9
26
  extensions = @connection.extensions
10
27
  if extensions.any?
@@ -17,19 +34,21 @@ module ActiveRecord
17
34
  end
18
35
 
19
36
  def types(stream)
20
- types = @connection.enum_types
21
- if types.any?
22
- stream.puts " # Custom types defined in this database."
23
- stream.puts " # Note that some types may not work with other database engines. Be careful if changing database."
24
- types.sort.each do |name, values|
25
- stream.puts " create_enum #{name.inspect}, #{values.inspect}"
37
+ within_each_schema do
38
+ types = @connection.enum_types
39
+ if types.any?
40
+ stream.puts " # Custom types defined in this database."
41
+ stream.puts " # Note that some types may not work with other database engines. Be careful if changing database."
42
+ types.sort.each do |name, values|
43
+ stream.puts " create_enum #{relation_name(name).inspect}, #{values.inspect}"
44
+ end
45
+ stream.puts
26
46
  end
27
- stream.puts
28
47
  end
29
48
  end
30
49
 
31
50
  def schemas(stream)
32
- schema_names = @connection.schema_names - ["public"]
51
+ schema_names = @dump_schemas - ["public"]
33
52
 
34
53
  if schema_names.any?
35
54
  schema_names.sort.each do |name|
@@ -39,45 +58,43 @@ module ActiveRecord
39
58
  end
40
59
  end
41
60
 
61
+ def tables(stream)
62
+ previous_schema_had_tables = false
63
+ within_each_schema do
64
+ stream.puts if previous_schema_had_tables
65
+ super
66
+ previous_schema_had_tables = @connection.tables.any?
67
+ end
68
+ end
69
+
42
70
  def exclusion_constraints_in_create(table, stream)
43
71
  if (exclusion_constraints = @connection.exclusion_constraints(table)).any?
44
- add_exclusion_constraint_statements = exclusion_constraints.map do |exclusion_constraint|
45
- parts = [
46
- "t.exclusion_constraint #{exclusion_constraint.expression.inspect}"
47
- ]
48
-
72
+ exclusion_constraint_statements = exclusion_constraints.map do |exclusion_constraint|
73
+ parts = [ exclusion_constraint.expression.inspect ]
49
74
  parts << "where: #{exclusion_constraint.where.inspect}" if exclusion_constraint.where
50
75
  parts << "using: #{exclusion_constraint.using.inspect}" if exclusion_constraint.using
51
76
  parts << "deferrable: #{exclusion_constraint.deferrable.inspect}" if exclusion_constraint.deferrable
77
+ parts << "name: #{exclusion_constraint.name.inspect}" if exclusion_constraint.export_name_on_schema_dump?
52
78
 
53
- if exclusion_constraint.export_name_on_schema_dump?
54
- parts << "name: #{exclusion_constraint.name.inspect}"
55
- end
56
-
57
- " #{parts.join(', ')}"
79
+ " t.exclusion_constraint #{parts.join(', ')}"
58
80
  end
59
81
 
60
- stream.puts add_exclusion_constraint_statements.sort.join("\n")
82
+ stream.puts exclusion_constraint_statements.sort.join("\n")
61
83
  end
62
84
  end
63
85
 
64
86
  def unique_constraints_in_create(table, stream)
65
87
  if (unique_constraints = @connection.unique_constraints(table)).any?
66
- add_unique_constraint_statements = unique_constraints.map do |unique_constraint|
67
- parts = [
68
- "t.unique_constraint #{unique_constraint.column.inspect}"
69
- ]
70
-
88
+ unique_constraint_statements = unique_constraints.map do |unique_constraint|
89
+ parts = [ unique_constraint.column.inspect ]
90
+ parts << "nulls_not_distinct: #{unique_constraint.nulls_not_distinct.inspect}" if unique_constraint.nulls_not_distinct
71
91
  parts << "deferrable: #{unique_constraint.deferrable.inspect}" if unique_constraint.deferrable
92
+ parts << "name: #{unique_constraint.name.inspect}" if unique_constraint.export_name_on_schema_dump?
72
93
 
73
- if unique_constraint.export_name_on_schema_dump?
74
- parts << "name: #{unique_constraint.name.inspect}"
75
- end
76
-
77
- " #{parts.join(', ')}"
94
+ " t.unique_constraint #{parts.join(', ')}"
78
95
  end
79
96
 
80
- stream.puts add_unique_constraint_statements.sort.join("\n")
97
+ stream.puts unique_constraint_statements.sort.join("\n")
81
98
  end
82
99
  end
83
100
 
@@ -87,11 +104,11 @@ module ActiveRecord
87
104
 
88
105
  if @connection.supports_virtual_columns? && column.virtual?
89
106
  spec[:as] = extract_expression_for_virtual_column(column)
90
- spec[:stored] = true
107
+ spec[:stored] = "true" if column.virtual_stored?
91
108
  spec = { type: schema_type(column).inspect }.merge!(spec)
92
109
  end
93
110
 
94
- spec[:enum_type] = "\"#{column.sql_type}\"" if column.enum?
111
+ spec[:enum_type] = relation_name(column.sql_type).inspect if column.enum?
95
112
 
96
113
  spec
97
114
  end
@@ -121,6 +138,28 @@ module ActiveRecord
121
138
  def extract_expression_for_virtual_column(column)
122
139
  column.default_function.inspect
123
140
  end
141
+
142
+ def within_each_schema
143
+ @dump_schemas.each do |schema_name|
144
+ old_search_path = @connection.schema_search_path
145
+ @connection.schema_search_path = schema_name
146
+ self.schema_name = schema_name
147
+ yield
148
+ ensure
149
+ self.schema_name = nil
150
+ @connection.schema_search_path = old_search_path
151
+ end
152
+ end
153
+
154
+ def relation_name(name)
155
+ if @dump_schemas.size == 1
156
+ name
157
+ elsif name.include?(".")
158
+ name # Already schema-qualified, don't add another prefix
159
+ else
160
+ "#{schema_name}.#{name}"
161
+ end
162
+ end
124
163
  end
125
164
  end
126
165
  end
@@ -12,9 +12,10 @@ module ActiveRecord
12
12
  end
13
13
 
14
14
  # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
15
- # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>,
16
- # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
17
- # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
15
+ # <tt>:encoding</tt> (defaults to utf8), <tt>:locale_provider</tt>, <tt>:locale</tt>,
16
+ # <tt>:collation</tt>, <tt>:ctype</tt>, <tt>:tablespace</tt>, and
17
+ # <tt>:connection_limit</tt> (note that MySQL uses <tt>:charset</tt> while PostgreSQL
18
+ # uses <tt>:encoding</tt>).
18
19
  #
19
20
  # Example:
20
21
  # create_database config[:database], config
@@ -30,6 +31,10 @@ module ActiveRecord
30
31
  " TEMPLATE = \"#{value}\""
31
32
  when :encoding
32
33
  " ENCODING = '#{value}'"
34
+ when :locale_provider
35
+ " LOCALE_PROVIDER = '#{value}'"
36
+ when :locale
37
+ " LOCALE = '#{value}'"
33
38
  when :collation
34
39
  " LC_COLLATE = '#{value}'"
35
40
  when :ctype
@@ -54,9 +59,9 @@ module ActiveRecord
54
59
  execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
55
60
  end
56
61
 
57
- def drop_table(table_name, **options) # :nodoc:
58
- schema_cache.clear_data_source_cache!(table_name.to_s)
59
- execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
62
+ def drop_table(*table_names, **options) # :nodoc:
63
+ table_names.each { |table_name| schema_cache.clear_data_source_cache!(table_name.to_s) }
64
+ execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{table_names.map { |table_name| quote_table_name(table_name) }.join(', ')}#{' CASCADE' if options[:force] == :cascade}"
60
65
  end
61
66
 
62
67
  # Returns true if schema exists.
@@ -87,8 +92,13 @@ module ActiveRecord
87
92
  scope = quoted_scope(table_name)
88
93
 
89
94
  result = query(<<~SQL, "SCHEMA")
90
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
91
- pg_catalog.obj_description(i.oid, 'pg_class') AS comment, d.indisvalid
95
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid),
96
+ pg_catalog.obj_description(i.oid, 'pg_class') AS comment, d.indisvalid,
97
+ ARRAY(
98
+ SELECT pg_get_indexdef(d.indexrelid, k + 1, true)
99
+ FROM generate_subscripts(d.indkey, 1) AS k
100
+ ORDER BY k
101
+ ) AS columns
92
102
  FROM pg_class t
93
103
  INNER JOIN pg_index d ON t.oid = d.indrelid
94
104
  INNER JOIN pg_class i ON d.indexrelid = i.oid
@@ -105,9 +115,10 @@ module ActiveRecord
105
115
  unique = row[1]
106
116
  indkey = row[2].split(" ").map(&:to_i)
107
117
  inddef = row[3]
108
- oid = row[4]
109
- comment = row[5]
110
- valid = row[6]
118
+ comment = row[4]
119
+ valid = row[5]
120
+ columns = decode_string_array(row[6]).map { |c| Utils.unquote_identifier(c.strip.gsub('""', '"')) }
121
+
111
122
  using, expressions, include, nulls_not_distinct, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: INCLUDE \((.+?)\))?( NULLS NOT DISTINCT)?(?: WHERE (.+))?\z/m).flatten
112
123
 
113
124
  orders = {}
@@ -117,15 +128,14 @@ module ActiveRecord
117
128
  if indkey.include?(0)
118
129
  columns = expressions
119
130
  else
120
- columns = column_names_from_column_numbers(oid, indkey)
121
-
122
131
  # prevent INCLUDE columns from being matched
123
132
  columns.reject! { |c| include_columns.include?(c) }
124
133
 
125
134
  # add info on sort order (only desc order is explicitly specified, asc is the default)
126
135
  # and non-default opclasses
127
- expressions.scan(/(?<column>\w+)"?\s?(?<opclass>\w+_ops(_\w+)?)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
128
- opclasses[column] = opclass.to_sym if opclass
136
+ expressions.scan(/(?<column>\w+)"?\s?(?<opclass>(?:\w+\.)?\w+_ops(_\w+)?)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
137
+ opclasses[column] = opclass.split(".").last.to_sym if opclass
138
+
129
139
  if nulls
130
140
  orders[column] = [desc, nulls].compact.join(" ")
131
141
  else
@@ -152,9 +162,23 @@ module ActiveRecord
152
162
  end
153
163
 
154
164
  def table_options(table_name) # :nodoc:
155
- if comment = table_comment(table_name)
156
- { comment: comment }
165
+ options = {}
166
+
167
+ comment = table_comment(table_name)
168
+
169
+ options[:comment] = comment if comment
170
+
171
+ inherited_table_names = inherited_table_names(table_name).presence
172
+
173
+ options[:options] = "INHERITS (#{inherited_table_names.join(", ")})" if inherited_table_names
174
+
175
+ if !options[:options] && supports_native_partitioning?
176
+ partition_definition = table_partition_definition(table_name)
177
+
178
+ options[:options] = "PARTITION BY #{partition_definition}" if partition_definition
157
179
  end
180
+
181
+ options
158
182
  end
159
183
 
160
184
  # Returns a comment stored in database for given table
@@ -172,6 +196,37 @@ module ActiveRecord
172
196
  end
173
197
  end
174
198
 
199
+ # Returns the partition definition of a given table
200
+ def table_partition_definition(table_name) # :nodoc:
201
+ scope = quoted_scope(table_name, type: "BASE TABLE")
202
+
203
+ query_value(<<~SQL, "SCHEMA")
204
+ SELECT pg_catalog.pg_get_partkeydef(c.oid)
205
+ FROM pg_catalog.pg_class c
206
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
207
+ WHERE c.relname = #{scope[:name]}
208
+ AND c.relkind IN (#{scope[:type]})
209
+ AND n.nspname = #{scope[:schema]}
210
+ SQL
211
+ end
212
+
213
+ # Returns the inherited table name of a given table
214
+ def inherited_table_names(table_name) # :nodoc:
215
+ scope = quoted_scope(table_name, type: "BASE TABLE")
216
+
217
+ query_values(<<~SQL, "SCHEMA")
218
+ SELECT parent.relname
219
+ FROM pg_catalog.pg_inherits i
220
+ JOIN pg_catalog.pg_class child ON i.inhrelid = child.oid
221
+ JOIN pg_catalog.pg_class parent ON i.inhparent = parent.oid
222
+ LEFT JOIN pg_namespace n ON n.oid = child.relnamespace
223
+ WHERE child.relname = #{scope[:name]}
224
+ AND child.relkind IN (#{scope[:type]})
225
+ AND n.nspname = #{scope[:schema]}
226
+ ORDER BY i.inhseqno
227
+ SQL
228
+ end
229
+
175
230
  # Returns the current database name.
176
231
  def current_database
177
232
  query_value("SELECT current_database()", "SCHEMA")
@@ -182,6 +237,14 @@ module ActiveRecord
182
237
  query_value("SELECT current_schema", "SCHEMA")
183
238
  end
184
239
 
240
+ # Returns an array of the names of all schemas presently in the effective search path,
241
+ # in their priority order.
242
+ def current_schemas # :nodoc:
243
+ schemas = query_value("SELECT current_schemas(false)", "SCHEMA")
244
+ decoder = PG::TextDecoder::Array.new
245
+ decoder.decode(schemas)
246
+ end
247
+
185
248
  # Returns the current database encoding format.
186
249
  def encoding
187
250
  query_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA")
@@ -226,12 +289,18 @@ module ActiveRecord
226
289
  execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE"
227
290
  end
228
291
 
292
+ # Renames the schema for the given schema name.
293
+ def rename_schema(schema_name, new_name)
294
+ execute "ALTER SCHEMA #{quote_schema_name(schema_name)} RENAME TO #{quote_schema_name(new_name)}"
295
+ end
296
+
229
297
  # Sets the schema search path to a string of comma-separated schema names.
230
298
  # Names beginning with $ have to be quoted (e.g. $user => '$user').
231
299
  # See: https://www.postgresql.org/docs/current/static/ddl-schemas.html
232
300
  #
233
301
  # This should be not be called manually but set in database.yml.
234
302
  def schema_search_path=(schema_csv)
303
+ return if schema_csv == @schema_search_path
235
304
  if schema_csv
236
305
  internal_execute("SET search_path TO #{schema_csv}")
237
306
  @schema_search_path = schema_csv
@@ -250,7 +319,7 @@ module ActiveRecord
250
319
 
251
320
  # Set the client message level.
252
321
  def client_min_messages=(level)
253
- internal_execute("SET client_min_messages TO '#{level}'")
322
+ internal_execute("SET client_min_messages TO '#{level}'", "SCHEMA")
254
323
  end
255
324
 
256
325
  # Returns the sequence name for a table's primary key or some other specified key.
@@ -276,7 +345,7 @@ module ActiveRecord
276
345
  if sequence
277
346
  quoted_sequence = quote_table_name(sequence)
278
347
 
279
- query_value("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA")
348
+ internal_execute("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA")
280
349
  else
281
350
  @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger
282
351
  end
@@ -307,7 +376,7 @@ module ActiveRecord
307
376
  end
308
377
  end
309
378
 
310
- query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk || minvalue}, #{max_pk ? true : false})", "SCHEMA")
379
+ internal_execute("SELECT setval(#{quote(quoted_sequence)}, #{max_pk || minvalue}, #{max_pk ? true : false})", "SCHEMA")
311
380
  end
312
381
  end
313
382
 
@@ -368,16 +437,13 @@ module ActiveRecord
368
437
  def primary_keys(table_name) # :nodoc:
369
438
  query_values(<<~SQL, "SCHEMA")
370
439
  SELECT a.attname
371
- FROM (
372
- SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
373
- FROM pg_index
374
- WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass
375
- AND indisprimary
376
- ) i
377
- JOIN pg_attribute a
378
- ON a.attrelid = i.indrelid
379
- AND a.attnum = i.indkey[i.idx]
380
- ORDER BY i.idx
440
+ FROM pg_index i
441
+ JOIN pg_attribute a
442
+ ON a.attrelid = i.indrelid
443
+ AND a.attnum = ANY(i.indkey)
444
+ WHERE i.indrelid = #{quote(quote_table_name(table_name))}::regclass
445
+ AND i.indisprimary
446
+ ORDER BY array_position(i.indkey, a.attnum)
381
447
  SQL
382
448
  end
383
449
 
@@ -487,7 +553,12 @@ module ActiveRecord
487
553
  result = execute schema_creation.accept(create_index)
488
554
 
489
555
  index = create_index.index
490
- execute "COMMENT ON INDEX #{quote_column_name(index.name)} IS #{quote(index.comment)}" if index.comment
556
+ if index.comment
557
+ name = Utils.extract_schema_qualified_name(table_name.to_s)
558
+ index_name = name.schema ? PostgreSQL::Name.new(name.schema, index.name).to_s : index.name
559
+ execute "COMMENT ON INDEX #{quote_table_name(index_name)} IS #{quote(index.comment)}"
560
+ end
561
+
491
562
  result
492
563
  end
493
564
 
@@ -540,36 +611,45 @@ module ActiveRecord
540
611
  def foreign_keys(table_name)
541
612
  scope = quoted_scope(table_name)
542
613
  fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
543
- SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred, c.conkey, c.confkey, c.conrelid, c.confrelid
614
+ SELECT t2.oid::regclass::text AS to_table, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred, c.conrelid, c.confrelid,
615
+ (
616
+ SELECT array_agg(a.attname ORDER BY idx)
617
+ FROM (
618
+ SELECT idx, c.conkey[idx] AS conkey_elem
619
+ FROM generate_subscripts(c.conkey, 1) AS idx
620
+ ) indexed_conkeys
621
+ JOIN pg_attribute a ON a.attrelid = t1.oid
622
+ AND a.attnum = indexed_conkeys.conkey_elem
623
+ ) AS conkey_names,
624
+ (
625
+ SELECT array_agg(a.attname ORDER BY idx)
626
+ FROM (
627
+ SELECT idx, c.confkey[idx] AS confkey_elem
628
+ FROM generate_subscripts(c.confkey, 1) AS idx
629
+ ) indexed_confkeys
630
+ JOIN pg_attribute a ON a.attrelid = t2.oid
631
+ AND a.attnum = indexed_confkeys.confkey_elem
632
+ ) AS confkey_names
544
633
  FROM pg_constraint c
545
634
  JOIN pg_class t1 ON c.conrelid = t1.oid
546
635
  JOIN pg_class t2 ON c.confrelid = t2.oid
547
- JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
548
- JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
549
- JOIN pg_namespace t3 ON c.connamespace = t3.oid
636
+ JOIN pg_namespace n ON c.connamespace = n.oid
550
637
  WHERE c.contype = 'f'
551
638
  AND t1.relname = #{scope[:name]}
552
- AND t3.nspname = #{scope[:schema]}
639
+ AND n.nspname = #{scope[:schema]}
553
640
  ORDER BY c.conname
554
641
  SQL
555
642
 
556
643
  fk_info.map do |row|
557
- to_table = Utils.unquote_identifier(row["to_table"])
558
- conkey = row["conkey"].scan(/\d+/).map(&:to_i)
559
- confkey = row["confkey"].scan(/\d+/).map(&:to_i)
644
+ to_table = Utils.extract_schema_qualified_name(row["to_table"]).to_s
560
645
 
561
- if conkey.size > 1
562
- column = column_names_from_column_numbers(row["conrelid"], conkey)
563
- primary_key = column_names_from_column_numbers(row["confrelid"], confkey)
564
- else
565
- column = Utils.unquote_identifier(row["column"])
566
- primary_key = row["primary_key"]
567
- end
646
+ column = decode_string_array(row["conkey_names"])
647
+ primary_key = decode_string_array(row["confkey_names"])
568
648
 
569
649
  options = {
570
- column: column,
650
+ column: column.size == 1 ? column.first : column,
571
651
  name: row["name"],
572
- primary_key: primary_key
652
+ primary_key: primary_key.size == 1 ? primary_key.first : primary_key
573
653
  }
574
654
 
575
655
  options[:on_delete] = extract_foreign_key_action(row["on_delete"])
@@ -631,7 +711,7 @@ module ActiveRecord
631
711
 
632
712
  exclusion_info.map do |row|
633
713
  method_and_elements, predicate = row["constraintdef"].split(" WHERE ")
634
- method_and_elements_parts = method_and_elements.match(/EXCLUDE(?: USING (?<using>\S+))? \((?<expression>.+)\)/)
714
+ method_and_elements_parts = method_and_elements.match(/EXCLUDE(?: USING (?<using>\S+))? \((?<expression>.+)\)/m)
635
715
  predicate.remove!(/ DEFERRABLE(?: INITIALLY (?:IMMEDIATE|DEFERRED))?/) if predicate
636
716
  predicate = predicate.from(2).to(-3) if predicate # strip 2 opening and closing parentheses
637
717
 
@@ -654,7 +734,16 @@ module ActiveRecord
654
734
  scope = quoted_scope(table_name)
655
735
 
656
736
  unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
657
- SELECT c.conname, c.conrelid, c.conkey, c.condeferrable, c.condeferred
737
+ SELECT c.conname, c.conrelid, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef,
738
+ (
739
+ SELECT array_agg(a.attname ORDER BY idx)
740
+ FROM (
741
+ SELECT idx, c.conkey[idx] AS conkey_elem
742
+ FROM generate_subscripts(c.conkey, 1) AS idx
743
+ ) indexed_conkeys
744
+ JOIN pg_attribute a ON a.attrelid = t.oid
745
+ AND a.attnum = indexed_conkeys.conkey_elem
746
+ ) AS conkey_names
658
747
  FROM pg_constraint c
659
748
  JOIN pg_class t ON c.conrelid = t.oid
660
749
  JOIN pg_namespace n ON n.oid = c.connamespace
@@ -664,13 +753,14 @@ module ActiveRecord
664
753
  SQL
665
754
 
666
755
  unique_info.map do |row|
667
- conkey = row["conkey"].delete("{}").split(",").map(&:to_i)
668
- columns = column_names_from_column_numbers(row["conrelid"], conkey)
756
+ columns = decode_string_array(row["conkey_names"])
669
757
 
758
+ nulls_not_distinct = row["constraintdef"].start_with?("UNIQUE NULLS NOT DISTINCT")
670
759
  deferrable = extract_constraint_deferrable(row["condeferrable"], row["condeferred"])
671
760
 
672
761
  options = {
673
762
  name: row["conname"],
763
+ nulls_not_distinct: nulls_not_distinct,
674
764
  deferrable: deferrable
675
765
  }
676
766
 
@@ -722,15 +812,12 @@ module ActiveRecord
722
812
  def remove_exclusion_constraint(table_name, expression = nil, **options)
723
813
  excl_name_to_delete = exclusion_constraint_for!(table_name, expression: expression, **options).name
724
814
 
725
- at = create_alter_table(table_name)
726
- at.drop_exclusion_constraint(excl_name_to_delete)
727
-
728
- execute schema_creation.accept(at)
815
+ remove_constraint(table_name, excl_name_to_delete)
729
816
  end
730
817
 
731
818
  # Adds a new unique constraint to the table.
732
819
  #
733
- # add_unique_constraint :sections, [:position], deferrable: :deferred, name: "unique_position"
820
+ # add_unique_constraint :sections, [:position], deferrable: :deferred, name: "unique_position", nulls_not_distinct: true
734
821
  #
735
822
  # generates:
736
823
  #
@@ -747,6 +834,9 @@ module ActiveRecord
747
834
  # Specify whether or not the unique constraint should be deferrable. Valid values are +false+ or +:immediate+ or +:deferred+ to specify the default behavior. Defaults to +false+.
748
835
  # [<tt>:using_index</tt>]
749
836
  # To specify an existing unique index name. Defaults to +nil+.
837
+ # [<tt>:nulls_not_distinct</tt>]
838
+ # Create a unique constraint where NULLs are treated equally.
839
+ # Note: only supported by PostgreSQL version 15.0.0 and greater.
750
840
  def add_unique_constraint(table_name, column_name = nil, **options)
751
841
  options = unique_constraint_options(table_name, column_name, options)
752
842
  at = create_alter_table(table_name)
@@ -777,10 +867,7 @@ module ActiveRecord
777
867
  def remove_unique_constraint(table_name, column_name = nil, **options)
778
868
  unique_name_to_delete = unique_constraint_for!(table_name, column: column_name, **options).name
779
869
 
780
- at = create_alter_table(table_name)
781
- at.drop_unique_constraint(unique_name_to_delete)
782
-
783
- execute schema_creation.accept(at)
870
+ remove_constraint(table_name, unique_name_to_delete)
784
871
  end
785
872
 
786
873
  # Maps logical Rails types to PostgreSQL-specific data types.
@@ -937,6 +1024,7 @@ module ActiveRecord
937
1024
 
938
1025
  PostgreSQL::Column.new(
939
1026
  column_name,
1027
+ get_oid_type(oid.to_i, fmod.to_i, column_name, type),
940
1028
  default_value,
941
1029
  type_metadata,
942
1030
  !notnull,
@@ -1032,6 +1120,11 @@ module ActiveRecord
1032
1120
  super
1033
1121
  end
1034
1122
 
1123
+ def validate_table_length!(table_name)
1124
+ _schema, table_name = extract_schema_qualified_name(table_name)
1125
+ super
1126
+ end
1127
+
1035
1128
  def exclusion_constraint_name(table_name, **options)
1036
1129
  options.fetch(:name) do
1037
1130
  expression = options.fetch(:expression)
@@ -1106,13 +1199,8 @@ module ActiveRecord
1106
1199
  [name.schema, name.identifier]
1107
1200
  end
1108
1201
 
1109
- def column_names_from_column_numbers(table_oid, column_numbers)
1110
- Hash[query(<<~SQL, "SCHEMA")].values_at(*column_numbers).compact
1111
- SELECT a.attnum, a.attname
1112
- FROM pg_attribute a
1113
- WHERE a.attrelid = #{table_oid}
1114
- AND a.attnum IN (#{column_numbers.join(", ")})
1115
- SQL
1202
+ def decode_string_array(value)
1203
+ PG::TextDecoder::Array.new.decode(value)
1116
1204
  end
1117
1205
  end
1118
1206
  end