activerecord 8.0.3 → 8.1.0

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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +538 -512
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/association_relation.rb +1 -1
  5. data/lib/active_record/associations/association.rb +1 -1
  6. data/lib/active_record/associations/belongs_to_association.rb +2 -0
  7. data/lib/active_record/associations/builder/association.rb +16 -5
  8. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  9. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  10. data/lib/active_record/associations/builder/has_one.rb +1 -1
  11. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  12. data/lib/active_record/associations/collection_proxy.rb +22 -4
  13. data/lib/active_record/associations/deprecation.rb +88 -0
  14. data/lib/active_record/associations/errors.rb +3 -0
  15. data/lib/active_record/associations/join_dependency.rb +2 -0
  16. data/lib/active_record/associations/preloader/branch.rb +1 -0
  17. data/lib/active_record/associations.rb +159 -21
  18. data/lib/active_record/attribute_methods/serialization.rb +16 -3
  19. data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
  20. data/lib/active_record/attributes.rb +3 -0
  21. data/lib/active_record/autosave_association.rb +1 -1
  22. data/lib/active_record/base.rb +0 -2
  23. data/lib/active_record/coders/json.rb +14 -5
  24. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +1 -3
  25. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -3
  26. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
  27. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +405 -72
  28. data/lib/active_record/connection_adapters/abstract/database_statements.rb +55 -40
  29. data/lib/active_record/connection_adapters/abstract/query_cache.rb +19 -3
  30. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  31. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  32. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +26 -34
  33. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  34. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +85 -22
  35. data/lib/active_record/connection_adapters/abstract/transaction.rb +25 -3
  36. data/lib/active_record/connection_adapters/abstract_adapter.rb +86 -20
  37. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -13
  38. data/lib/active_record/connection_adapters/column.rb +17 -4
  39. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  40. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  41. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  42. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +26 -4
  43. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +27 -22
  44. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -2
  45. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  46. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +17 -15
  47. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  48. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  49. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  50. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +8 -6
  51. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +8 -21
  52. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +67 -31
  53. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +81 -48
  54. data/lib/active_record/connection_adapters/postgresql_adapter.rb +23 -7
  55. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  56. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +37 -25
  57. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  58. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
  59. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +56 -32
  60. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  61. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  62. data/lib/active_record/connection_adapters.rb +1 -0
  63. data/lib/active_record/connection_handling.rb +14 -9
  64. data/lib/active_record/core.rb +5 -4
  65. data/lib/active_record/counter_cache.rb +33 -8
  66. data/lib/active_record/database_configurations/database_config.rb +5 -1
  67. data/lib/active_record/database_configurations/hash_config.rb +53 -9
  68. data/lib/active_record/database_configurations/url_config.rb +13 -3
  69. data/lib/active_record/database_configurations.rb +7 -3
  70. data/lib/active_record/delegated_type.rb +1 -1
  71. data/lib/active_record/dynamic_matchers.rb +54 -69
  72. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  73. data/lib/active_record/encryption/encrypted_attribute_type.rb +1 -1
  74. data/lib/active_record/encryption/encryptor.rb +12 -0
  75. data/lib/active_record/encryption/scheme.rb +1 -1
  76. data/lib/active_record/enum.rb +24 -8
  77. data/lib/active_record/errors.rb +20 -4
  78. data/lib/active_record/explain.rb +1 -1
  79. data/lib/active_record/explain_registry.rb +51 -2
  80. data/lib/active_record/filter_attribute_handler.rb +73 -0
  81. data/lib/active_record/fixtures.rb +2 -2
  82. data/lib/active_record/gem_version.rb +2 -2
  83. data/lib/active_record/inheritance.rb +1 -1
  84. data/lib/active_record/insert_all.rb +12 -7
  85. data/lib/active_record/locking/optimistic.rb +7 -0
  86. data/lib/active_record/locking/pessimistic.rb +5 -0
  87. data/lib/active_record/log_subscriber.rb +2 -6
  88. data/lib/active_record/middleware/shard_selector.rb +34 -17
  89. data/lib/active_record/migration/command_recorder.rb +14 -1
  90. data/lib/active_record/migration/compatibility.rb +34 -24
  91. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  92. data/lib/active_record/migration.rb +26 -16
  93. data/lib/active_record/model_schema.rb +36 -10
  94. data/lib/active_record/nested_attributes.rb +2 -0
  95. data/lib/active_record/persistence.rb +34 -3
  96. data/lib/active_record/query_cache.rb +22 -15
  97. data/lib/active_record/query_logs.rb +3 -7
  98. data/lib/active_record/railtie.rb +32 -3
  99. data/lib/active_record/railties/controller_runtime.rb +11 -6
  100. data/lib/active_record/railties/databases.rake +15 -3
  101. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  102. data/lib/active_record/railties/job_runtime.rb +10 -11
  103. data/lib/active_record/reflection.rb +35 -0
  104. data/lib/active_record/relation/batches.rb +25 -11
  105. data/lib/active_record/relation/calculations.rb +20 -9
  106. data/lib/active_record/relation/delegation.rb +0 -1
  107. data/lib/active_record/relation/finder_methods.rb +27 -11
  108. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -9
  109. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -7
  110. data/lib/active_record/relation/predicate_builder.rb +9 -7
  111. data/lib/active_record/relation/query_attribute.rb +3 -1
  112. data/lib/active_record/relation/query_methods.rb +40 -29
  113. data/lib/active_record/relation/where_clause.rb +1 -8
  114. data/lib/active_record/relation.rb +24 -12
  115. data/lib/active_record/result.rb +44 -21
  116. data/lib/active_record/runtime_registry.rb +41 -58
  117. data/lib/active_record/sanitization.rb +2 -0
  118. data/lib/active_record/schema_dumper.rb +12 -10
  119. data/lib/active_record/scoping.rb +0 -1
  120. data/lib/active_record/signed_id.rb +43 -15
  121. data/lib/active_record/statement_cache.rb +13 -9
  122. data/lib/active_record/store.rb +44 -19
  123. data/lib/active_record/structured_event_subscriber.rb +85 -0
  124. data/lib/active_record/table_metadata.rb +5 -20
  125. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  126. data/lib/active_record/tasks/database_tasks.rb +25 -34
  127. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  128. data/lib/active_record/tasks/postgresql_database_tasks.rb +5 -39
  129. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  130. data/lib/active_record/test_databases.rb +14 -4
  131. data/lib/active_record/test_fixtures.rb +27 -2
  132. data/lib/active_record/testing/query_assertions.rb +8 -2
  133. data/lib/active_record/timestamp.rb +4 -2
  134. data/lib/active_record/transaction.rb +2 -5
  135. data/lib/active_record/transactions.rb +32 -10
  136. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  137. data/lib/active_record/type/internal/timezone.rb +7 -0
  138. data/lib/active_record/type/json.rb +15 -2
  139. data/lib/active_record/type/serialized.rb +11 -4
  140. data/lib/active_record/type/type_map.rb +1 -1
  141. data/lib/active_record/type_caster/connection.rb +2 -1
  142. data/lib/active_record/validations/associated.rb +1 -1
  143. data/lib/active_record.rb +65 -3
  144. data/lib/arel/alias_predication.rb +2 -0
  145. data/lib/arel/crud.rb +6 -11
  146. data/lib/arel/nodes/count.rb +2 -2
  147. data/lib/arel/nodes/function.rb +4 -10
  148. data/lib/arel/nodes/named_function.rb +2 -2
  149. data/lib/arel/nodes/node.rb +1 -1
  150. data/lib/arel/nodes.rb +0 -2
  151. data/lib/arel/select_manager.rb +7 -2
  152. data/lib/arel/visitors/dot.rb +0 -3
  153. data/lib/arel/visitors/postgresql.rb +55 -0
  154. data/lib/arel/visitors/sqlite.rb +55 -8
  155. data/lib/arel/visitors/to_sql.rb +3 -21
  156. data/lib/arel.rb +3 -1
  157. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  158. metadata +14 -10
  159. data/lib/active_record/explain_subscriber.rb +0 -34
  160. data/lib/active_record/normalization.rb +0 -163
@@ -6,6 +6,7 @@ module ActiveRecord
6
6
  class SchemaCreation < SchemaCreation # :nodoc:
7
7
  private
8
8
  delegate :quoted_include_columns_for_index, to: :@conn
9
+ delegate :database_version, to: :@conn
9
10
 
10
11
  def visit_AlterTable(o)
11
12
  sql = super
@@ -99,7 +100,7 @@ module ActiveRecord
99
100
  if options[:default].nil?
100
101
  change_column_sql << ", ALTER COLUMN #{quoted_column_name} DROP DEFAULT"
101
102
  else
102
- quoted_default = quote_default_expression(options[:default], column)
103
+ quoted_default = quote_default_expression_for_column_definition(options[:default], column)
103
104
  change_column_sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quoted_default}"
104
105
  end
105
106
  end
@@ -126,16 +127,17 @@ module ActiveRecord
126
127
  end
127
128
 
128
129
  if as = options[:as]
129
- sql << " GENERATED ALWAYS AS (#{as})"
130
+ stored = options[:stored]
130
131
 
131
- if options[:stored]
132
- sql << " STORED"
133
- else
132
+ if stored != true && database_version < 18_00_00
134
133
  raise ArgumentError, <<~MSG
135
- PostgreSQL currently does not support VIRTUAL (not persisted) generated columns.
134
+ PostgreSQL versions before 18 do not support VIRTUAL (not persisted) generated columns.
136
135
  Specify 'stored: true' option for '#{options[:column].name}'
137
136
  MSG
138
137
  end
138
+
139
+ sql << " GENERATED ALWAYS AS (#{as})"
140
+ sql << (stored ? " STORED" : " VIRTUAL")
139
141
  end
140
142
  super
141
143
  end
@@ -5,6 +5,7 @@ module ActiveRecord
5
5
  module PostgreSQL
6
6
  module ColumnMethods
7
7
  extend ActiveSupport::Concern
8
+ extend ConnectionAdapters::ColumnMethods::ClassMethods
8
9
 
9
10
  # Defines the primary key field.
10
11
  # Use of the native PostgreSQL UUID type is supported, and can be used
@@ -15,22 +16,10 @@ module ActiveRecord
15
16
  # t.timestamps
16
17
  # end
17
18
  #
18
- # By default, this will use the <tt>gen_random_uuid()</tt> function from the
19
- # +pgcrypto+ extension. As that extension is only available in
20
- # PostgreSQL 9.4+, for earlier versions an explicit default can be set
21
- # to use <tt>uuid_generate_v4()</tt> from the +uuid-ossp+ extension instead:
19
+ # By default, this will use the <tt>gen_random_uuid()</tt> function.
22
20
  #
23
- # create_table :stuffs, id: false do |t|
24
- # t.primary_key :id, :uuid, default: "uuid_generate_v4()"
25
- # t.uuid :foo_id
26
- # t.timestamps
27
- # end
28
- #
29
- # To enable the appropriate extension, which is a requirement, use
30
- # the +enable_extension+ method in your migrations.
31
- #
32
- # To use a UUID primary key without any of the extensions, set the
33
- # +:default+ option to +nil+:
21
+ # To use a UUID primary key without any defaults, set the +:default+
22
+ # option to +nil+:
34
23
  #
35
24
  # create_table :stuffs, id: false do |t|
36
25
  # t.primary_key :id, :uuid, default: nil
@@ -181,12 +170,10 @@ module ActiveRecord
181
170
  # :method: enum
182
171
  # :call-seq: enum(*names, **options)
183
172
 
184
- included do
185
- define_column_methods :bigserial, :bit, :bit_varying, :cidr, :citext, :daterange,
186
- :hstore, :inet, :interval, :int4range, :int8range, :jsonb, :ltree, :macaddr,
187
- :money, :numrange, :oid, :point, :line, :lseg, :box, :path, :polygon, :circle,
188
- :serial, :tsrange, :tstzrange, :tsvector, :uuid, :xml, :timestamptz, :enum
189
- end
173
+ define_column_methods :bigserial, :bit, :bit_varying, :cidr, :citext, :daterange,
174
+ :hstore, :inet, :interval, :int4range, :int8range, :jsonb, :ltree, :macaddr,
175
+ :money, :numrange, :oid, :point, :line, :lseg, :box, :path, :polygon, :circle,
176
+ :serial, :tsrange, :tstzrange, :tsvector, :uuid, :xml, :timestamptz, :enum
190
177
  end
191
178
 
192
179
  ExclusionConstraintDefinition = Struct.new(:table_name, :expression, :options) do
@@ -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,46 +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 ]
71
90
  parts << "nulls_not_distinct: #{unique_constraint.nulls_not_distinct.inspect}" if unique_constraint.nulls_not_distinct
72
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?
73
93
 
74
- if unique_constraint.export_name_on_schema_dump?
75
- parts << "name: #{unique_constraint.name.inspect}"
76
- end
77
-
78
- " #{parts.join(', ')}"
94
+ " t.unique_constraint #{parts.join(', ')}"
79
95
  end
80
96
 
81
- stream.puts add_unique_constraint_statements.sort.join("\n")
97
+ stream.puts unique_constraint_statements.sort.join("\n")
82
98
  end
83
99
  end
84
100
 
@@ -88,7 +104,7 @@ module ActiveRecord
88
104
 
89
105
  if @connection.supports_virtual_columns? && column.virtual?
90
106
  spec[:as] = extract_expression_for_virtual_column(column)
91
- spec[:stored] = true
107
+ spec[:stored] = "true" if column.virtual_stored?
92
108
  spec = { type: schema_type(column).inspect }.merge!(spec)
93
109
  end
94
110
 
@@ -122,6 +138,26 @@ module ActiveRecord
122
138
  def extract_expression_for_virtual_column(column)
123
139
  column.default_function.inspect
124
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
+ else
158
+ "#{schema_name}.#{name}"
159
+ end
160
+ end
125
161
  end
126
162
  end
127
163
  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
@@ -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,8 +128,6 @@ 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
 
@@ -226,6 +235,14 @@ module ActiveRecord
226
235
  query_value("SELECT current_schema", "SCHEMA")
227
236
  end
228
237
 
238
+ # Returns an array of the names of all schemas presently in the effective search path,
239
+ # in their priority order.
240
+ def current_schemas # :nodoc:
241
+ schemas = query_value("SELECT current_schemas(false)", "SCHEMA")
242
+ decoder = PG::TextDecoder::Array.new
243
+ decoder.decode(schemas)
244
+ end
245
+
229
246
  # Returns the current database encoding format.
230
247
  def encoding
231
248
  query_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA")
@@ -270,12 +287,18 @@ module ActiveRecord
270
287
  execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE"
271
288
  end
272
289
 
290
+ # Renames the schema for the given schema name.
291
+ def rename_schema(schema_name, new_name)
292
+ execute "ALTER SCHEMA #{quote_schema_name(schema_name)} RENAME TO #{quote_schema_name(new_name)}"
293
+ end
294
+
273
295
  # Sets the schema search path to a string of comma-separated schema names.
274
296
  # Names beginning with $ have to be quoted (e.g. $user => '$user').
275
297
  # See: https://www.postgresql.org/docs/current/static/ddl-schemas.html
276
298
  #
277
299
  # This should be not be called manually but set in database.yml.
278
300
  def schema_search_path=(schema_csv)
301
+ return if schema_csv == @schema_search_path
279
302
  if schema_csv
280
303
  internal_execute("SET search_path TO #{schema_csv}")
281
304
  @schema_search_path = schema_csv
@@ -320,7 +343,7 @@ module ActiveRecord
320
343
  if sequence
321
344
  quoted_sequence = quote_table_name(sequence)
322
345
 
323
- query_value("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA")
346
+ internal_execute("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA")
324
347
  else
325
348
  @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger
326
349
  end
@@ -351,7 +374,7 @@ module ActiveRecord
351
374
  end
352
375
  end
353
376
 
354
- query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk || minvalue}, #{max_pk ? true : false})", "SCHEMA")
377
+ internal_execute("SELECT setval(#{quote(quoted_sequence)}, #{max_pk || minvalue}, #{max_pk ? true : false})", "SCHEMA")
355
378
  end
356
379
  end
357
380
 
@@ -412,16 +435,13 @@ module ActiveRecord
412
435
  def primary_keys(table_name) # :nodoc:
413
436
  query_values(<<~SQL, "SCHEMA")
414
437
  SELECT a.attname
415
- FROM (
416
- SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
417
- FROM pg_index
418
- WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass
419
- AND indisprimary
420
- ) i
421
- JOIN pg_attribute a
422
- ON a.attrelid = i.indrelid
423
- AND a.attnum = i.indkey[i.idx]
424
- ORDER BY i.idx
438
+ FROM pg_index i
439
+ JOIN pg_attribute a
440
+ ON a.attrelid = i.indrelid
441
+ AND a.attnum = ANY(i.indkey)
442
+ WHERE i.indrelid = #{quote(quote_table_name(table_name))}::regclass
443
+ AND i.indisprimary
444
+ ORDER BY array_position(i.indkey, a.attnum)
425
445
  SQL
426
446
  end
427
447
 
@@ -584,36 +604,45 @@ module ActiveRecord
584
604
  def foreign_keys(table_name)
585
605
  scope = quoted_scope(table_name)
586
606
  fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
587
- 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
607
+ 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,
608
+ (
609
+ SELECT array_agg(a.attname ORDER BY idx)
610
+ FROM (
611
+ SELECT idx, c.conkey[idx] AS conkey_elem
612
+ FROM generate_subscripts(c.conkey, 1) AS idx
613
+ ) indexed_conkeys
614
+ JOIN pg_attribute a ON a.attrelid = t1.oid
615
+ AND a.attnum = indexed_conkeys.conkey_elem
616
+ ) AS conkey_names,
617
+ (
618
+ SELECT array_agg(a.attname ORDER BY idx)
619
+ FROM (
620
+ SELECT idx, c.confkey[idx] AS confkey_elem
621
+ FROM generate_subscripts(c.confkey, 1) AS idx
622
+ ) indexed_confkeys
623
+ JOIN pg_attribute a ON a.attrelid = t2.oid
624
+ AND a.attnum = indexed_confkeys.confkey_elem
625
+ ) AS confkey_names
588
626
  FROM pg_constraint c
589
627
  JOIN pg_class t1 ON c.conrelid = t1.oid
590
628
  JOIN pg_class t2 ON c.confrelid = t2.oid
591
- JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
592
- JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
593
- JOIN pg_namespace t3 ON c.connamespace = t3.oid
629
+ JOIN pg_namespace n ON c.connamespace = n.oid
594
630
  WHERE c.contype = 'f'
595
631
  AND t1.relname = #{scope[:name]}
596
- AND t3.nspname = #{scope[:schema]}
632
+ AND n.nspname = #{scope[:schema]}
597
633
  ORDER BY c.conname
598
634
  SQL
599
635
 
600
636
  fk_info.map do |row|
601
637
  to_table = Utils.unquote_identifier(row["to_table"])
602
- conkey = row["conkey"].scan(/\d+/).map(&:to_i)
603
- confkey = row["confkey"].scan(/\d+/).map(&:to_i)
604
638
 
605
- if conkey.size > 1
606
- column = column_names_from_column_numbers(row["conrelid"], conkey)
607
- primary_key = column_names_from_column_numbers(row["confrelid"], confkey)
608
- else
609
- column = Utils.unquote_identifier(row["column"])
610
- primary_key = row["primary_key"]
611
- end
639
+ column = decode_string_array(row["conkey_names"])
640
+ primary_key = decode_string_array(row["confkey_names"])
612
641
 
613
642
  options = {
614
- column: column,
643
+ column: column.size == 1 ? column.first : column,
615
644
  name: row["name"],
616
- primary_key: primary_key
645
+ primary_key: primary_key.size == 1 ? primary_key.first : primary_key
617
646
  }
618
647
 
619
648
  options[:on_delete] = extract_foreign_key_action(row["on_delete"])
@@ -698,7 +727,16 @@ module ActiveRecord
698
727
  scope = quoted_scope(table_name)
699
728
 
700
729
  unique_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false)
701
- SELECT c.conname, c.conrelid, c.conkey, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef
730
+ SELECT c.conname, c.conrelid, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef,
731
+ (
732
+ SELECT array_agg(a.attname ORDER BY idx)
733
+ FROM (
734
+ SELECT idx, c.conkey[idx] AS conkey_elem
735
+ FROM generate_subscripts(c.conkey, 1) AS idx
736
+ ) indexed_conkeys
737
+ JOIN pg_attribute a ON a.attrelid = t.oid
738
+ AND a.attnum = indexed_conkeys.conkey_elem
739
+ ) AS conkey_names
702
740
  FROM pg_constraint c
703
741
  JOIN pg_class t ON c.conrelid = t.oid
704
742
  JOIN pg_namespace n ON n.oid = c.connamespace
@@ -708,8 +746,7 @@ module ActiveRecord
708
746
  SQL
709
747
 
710
748
  unique_info.map do |row|
711
- conkey = row["conkey"].delete("{}").split(",").map(&:to_i)
712
- columns = column_names_from_column_numbers(row["conrelid"], conkey)
749
+ columns = decode_string_array(row["conkey_names"])
713
750
 
714
751
  nulls_not_distinct = row["constraintdef"].start_with?("UNIQUE NULLS NOT DISTINCT")
715
752
  deferrable = extract_constraint_deferrable(row["condeferrable"], row["condeferred"])
@@ -980,6 +1017,7 @@ module ActiveRecord
980
1017
 
981
1018
  PostgreSQL::Column.new(
982
1019
  column_name,
1020
+ get_oid_type(oid.to_i, fmod.to_i, column_name, type),
983
1021
  default_value,
984
1022
  type_metadata,
985
1023
  !notnull,
@@ -1149,13 +1187,8 @@ module ActiveRecord
1149
1187
  [name.schema, name.identifier]
1150
1188
  end
1151
1189
 
1152
- def column_names_from_column_numbers(table_oid, column_numbers)
1153
- Hash[query(<<~SQL, "SCHEMA")].values_at(*column_numbers).compact
1154
- SELECT a.attnum, a.attname
1155
- FROM pg_attribute a
1156
- WHERE a.attrelid = #{table_oid}
1157
- AND a.attnum IN (#{column_numbers.join(", ")})
1158
- SQL
1190
+ def decode_string_array(value)
1191
+ PG::TextDecoder::Array.new.decode(value)
1159
1192
  end
1160
1193
  end
1161
1194
  end
@@ -288,6 +288,16 @@ module ActiveRecord
288
288
  database_version >= 10_00_00 # >= 10.0
289
289
  end
290
290
 
291
+ if PG::Connection.method_defined?(:close_prepared) # pg 1.6.0 & libpq 17
292
+ def supports_close_prepared? # :nodoc:
293
+ database_version >= 17_00_00
294
+ end
295
+ else
296
+ def supports_close_prepared? # :nodoc:
297
+ false
298
+ end
299
+ end
300
+
291
301
  def index_algorithms
292
302
  { concurrently: "CONCURRENTLY" }
293
303
  end
@@ -309,8 +319,12 @@ module ActiveRecord
309
319
  # accessed while holding the connection's lock. (And we
310
320
  # don't need the complication of with_raw_connection because
311
321
  # a reconnect would invalidate the entire statement pool.)
312
- if conn = @connection.instance_variable_get(:@raw_connection)
313
- conn.query "DEALLOCATE #{key}" if conn.status == PG::CONNECTION_OK
322
+ if (conn = @connection.instance_variable_get(:@raw_connection)) && conn.status == PG::CONNECTION_OK
323
+ if @connection.supports_close_prepared?
324
+ conn.close_prepared key
325
+ else
326
+ conn.query "DEALLOCATE #{key}"
327
+ end
314
328
  end
315
329
  rescue PG::Error
316
330
  end
@@ -397,10 +411,6 @@ module ActiveRecord
397
411
  @raw_connection = nil
398
412
  end
399
413
 
400
- def native_database_types # :nodoc:
401
- self.class.native_database_types
402
- end
403
-
404
414
  def self.native_database_types # :nodoc:
405
415
  @native_database_types ||= begin
406
416
  types = NATIVE_DATABASE_TYPES.dup
@@ -636,7 +646,7 @@ module ActiveRecord
636
646
  with_raw_connection do |conn|
637
647
  version = conn.server_version
638
648
  if version == 0
639
- raise ActiveRecord::ConnectionFailed, "Could not determine PostgreSQL version"
649
+ raise ActiveRecord::ConnectionNotEstablished, "Could not determine PostgreSQL version"
640
650
  end
641
651
  version
642
652
  end
@@ -792,6 +802,8 @@ module ActiveRecord
792
802
  NOT_NULL_VIOLATION = "23502"
793
803
  FOREIGN_KEY_VIOLATION = "23503"
794
804
  UNIQUE_VIOLATION = "23505"
805
+ CHECK_VIOLATION = "23514"
806
+ EXCLUSION_VIOLATION = "23P01"
795
807
  SERIALIZATION_FAILURE = "40001"
796
808
  DEADLOCK_DETECTED = "40P01"
797
809
  DUPLICATE_DATABASE = "42P04"
@@ -823,6 +835,10 @@ module ActiveRecord
823
835
  RecordNotUnique.new(message, sql: sql, binds: binds, connection_pool: @pool)
824
836
  when FOREIGN_KEY_VIOLATION
825
837
  InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
838
+ when CHECK_VIOLATION
839
+ CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
840
+ when EXCLUSION_VIOLATION
841
+ ExclusionViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
826
842
  when VALUE_LIMIT_VIOLATION
827
843
  ValueTooLong.new(message, sql: sql, binds: binds, connection_pool: @pool)
828
844
  when NUMERIC_VALUE_OUT_OF_RANGE
@@ -271,10 +271,10 @@ module ActiveRecord
271
271
  end
272
272
 
273
273
  def encode_with(coder) # :nodoc:
274
- coder["columns"] = @columns.sort.to_h
274
+ coder["columns"] = @columns.sort.to_h.transform_values { _1.sort_by(&:name) }
275
275
  coder["primary_keys"] = @primary_keys.sort.to_h
276
276
  coder["data_sources"] = @data_sources.sort.to_h
277
- coder["indexes"] = @indexes.sort.to_h
277
+ coder["indexes"] = @indexes.sort.to_h.transform_values { _1.sort_by(&:name) }
278
278
  coder["version"] = @version
279
279
  end
280
280