activerecord 8.0.2 → 8.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +459 -413
  3. data/README.rdoc +2 -2
  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 +9 -1
  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_association.rb +3 -3
  13. data/lib/active_record/associations/collection_proxy.rb +22 -4
  14. data/lib/active_record/associations/deprecation.rb +88 -0
  15. data/lib/active_record/associations/errors.rb +3 -0
  16. data/lib/active_record/associations/join_dependency.rb +2 -0
  17. data/lib/active_record/associations/preloader/branch.rb +1 -0
  18. data/lib/active_record/associations.rb +159 -21
  19. data/lib/active_record/attribute_methods/query.rb +34 -0
  20. data/lib/active_record/attribute_methods/serialization.rb +17 -4
  21. data/lib/active_record/attributes.rb +38 -24
  22. data/lib/active_record/base.rb +0 -1
  23. data/lib/active_record/coders/json.rb +14 -5
  24. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +2 -4
  25. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +15 -0
  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 +384 -49
  28. data/lib/active_record/connection_adapters/abstract/database_statements.rb +26 -30
  29. data/lib/active_record/connection_adapters/abstract/query_cache.rb +19 -1
  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 +89 -23
  35. data/lib/active_record/connection_adapters/abstract/transaction.rb +16 -3
  36. data/lib/active_record/connection_adapters/abstract_adapter.rb +67 -13
  37. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -11
  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 -0
  45. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +18 -16
  46. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  47. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  48. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  49. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -1
  50. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +8 -21
  51. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +65 -30
  52. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +74 -38
  53. data/lib/active_record/connection_adapters/postgresql_adapter.rb +12 -7
  54. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  55. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -27
  56. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  57. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
  58. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +56 -32
  59. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  60. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  61. data/lib/active_record/connection_adapters.rb +1 -0
  62. data/lib/active_record/connection_handling.rb +1 -1
  63. data/lib/active_record/core.rb +13 -10
  64. data/lib/active_record/counter_cache.rb +33 -8
  65. data/lib/active_record/database_configurations/database_config.rb +5 -1
  66. data/lib/active_record/database_configurations/hash_config.rb +56 -9
  67. data/lib/active_record/database_configurations/url_config.rb +13 -3
  68. data/lib/active_record/database_configurations.rb +7 -3
  69. data/lib/active_record/delegated_type.rb +2 -2
  70. data/lib/active_record/dynamic_matchers.rb +54 -69
  71. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  72. data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
  73. data/lib/active_record/encryption/encryptor.rb +27 -25
  74. data/lib/active_record/encryption/scheme.rb +1 -1
  75. data/lib/active_record/enum.rb +37 -20
  76. data/lib/active_record/errors.rb +20 -4
  77. data/lib/active_record/explain_registry.rb +0 -1
  78. data/lib/active_record/filter_attribute_handler.rb +73 -0
  79. data/lib/active_record/fixture_set/table_row.rb +19 -2
  80. data/lib/active_record/fixtures.rb +2 -2
  81. data/lib/active_record/gem_version.rb +3 -3
  82. data/lib/active_record/inheritance.rb +1 -1
  83. data/lib/active_record/insert_all.rb +12 -7
  84. data/lib/active_record/locking/optimistic.rb +7 -0
  85. data/lib/active_record/locking/pessimistic.rb +5 -0
  86. data/lib/active_record/log_subscriber.rb +1 -5
  87. data/lib/active_record/middleware/shard_selector.rb +34 -17
  88. data/lib/active_record/migration/command_recorder.rb +14 -1
  89. data/lib/active_record/migration/compatibility.rb +34 -24
  90. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  91. data/lib/active_record/migration.rb +31 -21
  92. data/lib/active_record/model_schema.rb +10 -7
  93. data/lib/active_record/nested_attributes.rb +2 -0
  94. data/lib/active_record/persistence.rb +34 -3
  95. data/lib/active_record/query_cache.rb +22 -15
  96. data/lib/active_record/query_logs.rb +7 -7
  97. data/lib/active_record/querying.rb +4 -4
  98. data/lib/active_record/railtie.rb +34 -5
  99. data/lib/active_record/railties/databases.rake +23 -19
  100. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  101. data/lib/active_record/railties/job_runtime.rb +10 -11
  102. data/lib/active_record/reflection.rb +42 -3
  103. data/lib/active_record/relation/batches.rb +26 -12
  104. data/lib/active_record/relation/calculations.rb +35 -25
  105. data/lib/active_record/relation/delegation.rb +0 -1
  106. data/lib/active_record/relation/finder_methods.rb +41 -24
  107. data/lib/active_record/relation/merger.rb +2 -2
  108. data/lib/active_record/relation/predicate_builder.rb +2 -2
  109. data/lib/active_record/relation/query_attribute.rb +3 -1
  110. data/lib/active_record/relation/query_methods.rb +43 -33
  111. data/lib/active_record/relation/spawn_methods.rb +6 -6
  112. data/lib/active_record/relation/where_clause.rb +7 -10
  113. data/lib/active_record/relation.rb +37 -15
  114. data/lib/active_record/result.rb +44 -21
  115. data/lib/active_record/sanitization.rb +2 -0
  116. data/lib/active_record/schema_dumper.rb +12 -10
  117. data/lib/active_record/scoping.rb +0 -1
  118. data/lib/active_record/secure_token.rb +3 -3
  119. data/lib/active_record/signed_id.rb +46 -18
  120. data/lib/active_record/statement_cache.rb +13 -9
  121. data/lib/active_record/store.rb +44 -19
  122. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  123. data/lib/active_record/tasks/database_tasks.rb +24 -35
  124. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  125. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  126. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  127. data/lib/active_record/test_databases.rb +11 -3
  128. data/lib/active_record/test_fixtures.rb +27 -2
  129. data/lib/active_record/testing/query_assertions.rb +8 -2
  130. data/lib/active_record/timestamp.rb +4 -2
  131. data/lib/active_record/transaction.rb +2 -5
  132. data/lib/active_record/transactions.rb +34 -10
  133. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  134. data/lib/active_record/type/internal/timezone.rb +7 -0
  135. data/lib/active_record/type/json.rb +15 -2
  136. data/lib/active_record/type/serialized.rb +11 -4
  137. data/lib/active_record/type/type_map.rb +1 -1
  138. data/lib/active_record/type_caster/connection.rb +2 -1
  139. data/lib/active_record/validations/associated.rb +1 -1
  140. data/lib/active_record.rb +68 -5
  141. data/lib/arel/alias_predication.rb +2 -0
  142. data/lib/arel/crud.rb +8 -11
  143. data/lib/arel/delete_manager.rb +5 -0
  144. data/lib/arel/nodes/count.rb +2 -2
  145. data/lib/arel/nodes/delete_statement.rb +4 -2
  146. data/lib/arel/nodes/function.rb +4 -10
  147. data/lib/arel/nodes/named_function.rb +2 -2
  148. data/lib/arel/nodes/node.rb +1 -1
  149. data/lib/arel/nodes/update_statement.rb +4 -2
  150. data/lib/arel/nodes.rb +0 -2
  151. data/lib/arel/select_manager.rb +13 -4
  152. data/lib/arel/update_manager.rb +5 -0
  153. data/lib/arel/visitors/dot.rb +2 -3
  154. data/lib/arel/visitors/postgresql.rb +55 -0
  155. data/lib/arel/visitors/sqlite.rb +55 -8
  156. data/lib/arel/visitors/to_sql.rb +5 -21
  157. data/lib/arel.rb +3 -1
  158. metadata +15 -11
  159. data/lib/active_record/normalization.rb +0 -163
@@ -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
 
@@ -584,36 +607,45 @@ module ActiveRecord
584
607
  def foreign_keys(table_name)
585
608
  scope = quoted_scope(table_name)
586
609
  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
610
+ 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,
611
+ (
612
+ SELECT array_agg(a.attname ORDER BY idx)
613
+ FROM (
614
+ SELECT idx, c.conkey[idx] AS conkey_elem
615
+ FROM generate_subscripts(c.conkey, 1) AS idx
616
+ ) indexed_conkeys
617
+ JOIN pg_attribute a ON a.attrelid = t1.oid
618
+ AND a.attnum = indexed_conkeys.conkey_elem
619
+ ) AS conkey_names,
620
+ (
621
+ SELECT array_agg(a.attname ORDER BY idx)
622
+ FROM (
623
+ SELECT idx, c.confkey[idx] AS confkey_elem
624
+ FROM generate_subscripts(c.confkey, 1) AS idx
625
+ ) indexed_confkeys
626
+ JOIN pg_attribute a ON a.attrelid = t2.oid
627
+ AND a.attnum = indexed_confkeys.confkey_elem
628
+ ) AS confkey_names
588
629
  FROM pg_constraint c
589
630
  JOIN pg_class t1 ON c.conrelid = t1.oid
590
631
  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
632
+ JOIN pg_namespace n ON c.connamespace = n.oid
594
633
  WHERE c.contype = 'f'
595
634
  AND t1.relname = #{scope[:name]}
596
- AND t3.nspname = #{scope[:schema]}
635
+ AND n.nspname = #{scope[:schema]}
597
636
  ORDER BY c.conname
598
637
  SQL
599
638
 
600
639
  fk_info.map do |row|
601
640
  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
641
 
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
642
+ column = decode_string_array(row["conkey_names"])
643
+ primary_key = decode_string_array(row["confkey_names"])
612
644
 
613
645
  options = {
614
- column: column,
646
+ column: column.size == 1 ? column.first : column,
615
647
  name: row["name"],
616
- primary_key: primary_key
648
+ primary_key: primary_key.size == 1 ? primary_key.first : primary_key
617
649
  }
618
650
 
619
651
  options[:on_delete] = extract_foreign_key_action(row["on_delete"])
@@ -698,7 +730,16 @@ module ActiveRecord
698
730
  scope = quoted_scope(table_name)
699
731
 
700
732
  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
733
+ SELECT c.conname, c.conrelid, c.condeferrable, c.condeferred, pg_get_constraintdef(c.oid) AS constraintdef,
734
+ (
735
+ SELECT array_agg(a.attname ORDER BY idx)
736
+ FROM (
737
+ SELECT idx, c.conkey[idx] AS conkey_elem
738
+ FROM generate_subscripts(c.conkey, 1) AS idx
739
+ ) indexed_conkeys
740
+ JOIN pg_attribute a ON a.attrelid = t.oid
741
+ AND a.attnum = indexed_conkeys.conkey_elem
742
+ ) AS conkey_names
702
743
  FROM pg_constraint c
703
744
  JOIN pg_class t ON c.conrelid = t.oid
704
745
  JOIN pg_namespace n ON n.oid = c.connamespace
@@ -708,8 +749,7 @@ module ActiveRecord
708
749
  SQL
709
750
 
710
751
  unique_info.map do |row|
711
- conkey = row["conkey"].delete("{}").split(",").map(&:to_i)
712
- columns = column_names_from_column_numbers(row["conrelid"], conkey)
752
+ columns = decode_string_array(row["conkey_names"])
713
753
 
714
754
  nulls_not_distinct = row["constraintdef"].start_with?("UNIQUE NULLS NOT DISTINCT")
715
755
  deferrable = extract_constraint_deferrable(row["condeferrable"], row["condeferred"])
@@ -980,6 +1020,7 @@ module ActiveRecord
980
1020
 
981
1021
  PostgreSQL::Column.new(
982
1022
  column_name,
1023
+ get_oid_type(oid.to_i, fmod.to_i, column_name, type),
983
1024
  default_value,
984
1025
  type_metadata,
985
1026
  !notnull,
@@ -1149,13 +1190,8 @@ module ActiveRecord
1149
1190
  [name.schema, name.identifier]
1150
1191
  end
1151
1192
 
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
1193
+ def decode_string_array(value)
1194
+ PG::TextDecoder::Array.new.decode(value)
1159
1195
  end
1160
1196
  end
1161
1197
  end
@@ -25,7 +25,7 @@ module ActiveRecord
25
25
  #
26
26
  # The PostgreSQL adapter works with the native C (https://github.com/ged/ruby-pg) driver.
27
27
  #
28
- # Options:
28
+ # ==== Options
29
29
  #
30
30
  # * <tt>:host</tt> - Defaults to a Unix-domain socket in /tmp. On machines without Unix-domain sockets,
31
31
  # the default is to connect to localhost.
@@ -397,10 +397,6 @@ module ActiveRecord
397
397
  @raw_connection = nil
398
398
  end
399
399
 
400
- def native_database_types # :nodoc:
401
- self.class.native_database_types
402
- end
403
-
404
400
  def self.native_database_types # :nodoc:
405
401
  @native_database_types ||= begin
406
402
  types = NATIVE_DATABASE_TYPES.dup
@@ -616,7 +612,7 @@ module ActiveRecord
616
612
  }
617
613
  end
618
614
 
619
- # Returns the configured supported identifier length supported by PostgreSQL
615
+ # Returns the configured maximum supported identifier length supported by PostgreSQL
620
616
  def max_identifier_length
621
617
  @max_identifier_length ||= query_value("SHOW max_identifier_length", "SCHEMA").to_i
622
618
  end
@@ -636,7 +632,7 @@ module ActiveRecord
636
632
  with_raw_connection do |conn|
637
633
  version = conn.server_version
638
634
  if version == 0
639
- raise ActiveRecord::ConnectionFailed, "Could not determine PostgreSQL version"
635
+ raise ActiveRecord::ConnectionNotEstablished, "Could not determine PostgreSQL version"
640
636
  end
641
637
  version
642
638
  end
@@ -670,6 +666,9 @@ module ActiveRecord
670
666
  if database_version < 9_03_00 # < 9.3
671
667
  raise "Your version of PostgreSQL (#{database_version}) is too old. Active Record supports PostgreSQL >= 9.3."
672
668
  end
669
+ if database_version >= 18_00_00 && Gem::Version.new(PG::VERSION) < Gem::Version.new("1.6.0")
670
+ warn "pg gem version #{PG::VERSION} is known to be incompatible with PostgreSQL 18+. Please upgrade to pg 1.6.0 or later."
671
+ end
673
672
  end
674
673
 
675
674
  class << self
@@ -792,6 +791,8 @@ module ActiveRecord
792
791
  NOT_NULL_VIOLATION = "23502"
793
792
  FOREIGN_KEY_VIOLATION = "23503"
794
793
  UNIQUE_VIOLATION = "23505"
794
+ CHECK_VIOLATION = "23514"
795
+ EXCLUSION_VIOLATION = "23P01"
795
796
  SERIALIZATION_FAILURE = "40001"
796
797
  DEADLOCK_DETECTED = "40P01"
797
798
  DUPLICATE_DATABASE = "42P04"
@@ -823,6 +824,10 @@ module ActiveRecord
823
824
  RecordNotUnique.new(message, sql: sql, binds: binds, connection_pool: @pool)
824
825
  when FOREIGN_KEY_VIOLATION
825
826
  InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
827
+ when CHECK_VIOLATION
828
+ CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
829
+ when EXCLUSION_VIOLATION
830
+ ExclusionViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
826
831
  when VALUE_LIMIT_VIOLATION
827
832
  ValueTooLong.new(message, sql: sql, binds: binds, connection_pool: @pool)
828
833
  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
 
@@ -61,6 +61,14 @@ module ActiveRecord
61
61
  @previous_read_uncommitted = nil
62
62
  end
63
63
 
64
+ def default_insert_value(column) # :nodoc:
65
+ if column.default_function
66
+ Arel.sql(column.default_function)
67
+ else
68
+ column.default
69
+ end
70
+ end
71
+
64
72
  private
65
73
  def internal_begin_transaction(mode, isolation)
66
74
  if isolation
@@ -76,51 +84,63 @@ module ActiveRecord
76
84
  end
77
85
 
78
86
  def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
87
+ total_changes_before_query = raw_connection.total_changes
88
+ affected_rows = nil
89
+
79
90
  if batch
80
91
  raw_connection.execute_batch2(sql)
81
- elsif prepare
82
- stmt = @statements[sql] ||= raw_connection.prepare(sql)
83
- stmt.reset!
84
- stmt.bind_params(type_casted_binds)
85
-
86
- result = if stmt.column_count.zero? # No return
87
- stmt.step
88
- ActiveRecord::Result.empty
92
+ else
93
+ stmt = if prepare
94
+ @statements[sql] ||= raw_connection.prepare(sql)
95
+ @statements[sql].reset!
89
96
  else
90
- ActiveRecord::Result.new(stmt.columns, stmt.to_a)
97
+ # Don't cache statements if they are not prepared.
98
+ raw_connection.prepare(sql)
91
99
  end
92
- else
93
- # Don't cache statements if they are not prepared.
94
- stmt = raw_connection.prepare(sql)
95
100
  begin
96
101
  unless binds.nil? || binds.empty?
97
102
  stmt.bind_params(type_casted_binds)
98
103
  end
99
104
  result = if stmt.column_count.zero? # No return
100
105
  stmt.step
101
- ActiveRecord::Result.empty
106
+
107
+ affected_rows = if raw_connection.total_changes > total_changes_before_query
108
+ raw_connection.changes
109
+ else
110
+ 0
111
+ end
112
+
113
+ ActiveRecord::Result.empty(affected_rows: affected_rows)
102
114
  else
103
- ActiveRecord::Result.new(stmt.columns, stmt.to_a)
115
+ rows = stmt.to_a
116
+
117
+ affected_rows = if raw_connection.total_changes > total_changes_before_query
118
+ raw_connection.changes
119
+ else
120
+ 0
121
+ end
122
+
123
+ ActiveRecord::Result.new(stmt.columns, rows, stmt.types.map { |t| type_map.lookup(t) }, affected_rows: affected_rows)
104
124
  end
105
125
  ensure
106
- stmt.close
126
+ stmt.close unless prepare
107
127
  end
108
128
  end
109
- @last_affected_rows = raw_connection.changes
110
129
  verified!
111
130
 
131
+ notification_payload[:affected_rows] = affected_rows
112
132
  notification_payload[:row_count] = result&.length || 0
113
133
  result
114
134
  end
115
135
 
116
136
  def cast_result(result)
117
- # Given that SQLite3 doesn't really a Result type, raw_execute already return an ActiveRecord::Result
118
- # and we have nothing to cast here.
137
+ # Given that SQLite3 doesn't have a Result type, raw_execute already returns an ActiveRecord::Result
138
+ # so we have nothing to cast here.
119
139
  result
120
140
  end
121
141
 
122
142
  def affected_rows(result)
123
- @last_affected_rows
143
+ result.affected_rows
124
144
  end
125
145
 
126
146
  def execute_batch(statements, name = nil, **kwargs)
@@ -135,14 +155,6 @@ module ActiveRecord
135
155
  def returning_column_values(result)
136
156
  result.rows.first
137
157
  end
138
-
139
- def default_insert_value(column)
140
- if column.default_function
141
- Arel.sql(column.default_function)
142
- else
143
- column.default
144
- end
145
- end
146
158
  end
147
159
  end
148
160
  end
@@ -80,18 +80,10 @@ module ActiveRecord
80
80
  "x'#{value.hex}'"
81
81
  end
82
82
 
83
- def quoted_true
84
- "1"
85
- end
86
-
87
83
  def unquoted_true
88
84
  1
89
85
  end
90
86
 
91
- def quoted_false
92
- "0"
93
- end
94
-
95
87
  def unquoted_false
96
88
  0
97
89
  end
@@ -63,23 +63,13 @@ module ActiveRecord
63
63
  end
64
64
 
65
65
  def remove_foreign_key(from_table, to_table = nil, **options)
66
- return if options.delete(:if_exists) == true && !foreign_key_exists?(from_table, to_table)
66
+ return if options.delete(:if_exists) && !foreign_key_exists?(from_table, to_table, **options.slice(:column))
67
67
 
68
68
  to_table ||= options[:to_table]
69
69
  options = options.except(:name, :to_table, :validate)
70
- foreign_keys = foreign_keys(from_table)
71
-
72
- fkey = foreign_keys.detect do |fk|
73
- table = to_table || begin
74
- table = options[:column].to_s.delete_suffix("_id")
75
- Base.pluralize_table_names ? table.pluralize : table
76
- end
77
- table = strip_table_name_prefix_and_suffix(table)
78
- options = options.slice(*fk.options.keys)
79
- fk_to_table = strip_table_name_prefix_and_suffix(fk.to_table)
80
- fk_to_table == table && options.all? { |k, v| fk.options[k].to_s == v.to_s }
81
- end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table || options}")
70
+ fkey = foreign_key_for!(from_table, to_table: to_table, **options)
82
71
 
72
+ foreign_keys = foreign_keys(from_table)
83
73
  foreign_keys.delete(fkey)
84
74
  alter_table(from_table, foreign_keys)
85
75
  end
@@ -157,6 +147,7 @@ module ActiveRecord
157
147
 
158
148
  Column.new(
159
149
  field["name"],
150
+ lookup_cast_type(field["type"]),
160
151
  default_value,
161
152
  type_metadata,
162
153
  field["notnull"].to_i == 0,
@@ -19,14 +19,30 @@ SQLite3::ForkSafety.suppress_warnings!
19
19
 
20
20
  module ActiveRecord
21
21
  module ConnectionAdapters # :nodoc:
22
- # = Active Record SQLite3 Adapter
22
+ # = Active Record \SQLite3 Adapter
23
23
  #
24
- # The SQLite3 adapter works with the sqlite3-ruby drivers
25
- # (available as gem from https://rubygems.org/gems/sqlite3).
24
+ # The \SQLite3 adapter works with the sqlite3[https://sparklemotion.github.io/sqlite3-ruby/]
25
+ # driver.
26
26
  #
27
- # Options:
27
+ # ==== Options
28
+ #
29
+ # * +:database+ (String): Filesystem path to the database file.
30
+ # * +:statement_limit+ (Integer): Maximum number of prepared statements to cache per database connection. (default: 1000)
31
+ # * +:timeout+ (Integer): Timeout in milliseconds to use when waiting for a lock. (default: no wait)
32
+ # * +:strict+ (Boolean): Enable or disable strict mode. When enabled, this will
33
+ # {disallow double-quoted string literals in SQL
34
+ # statements}[https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted].
35
+ # (default: see strict_strings_by_default)
36
+ # * +:extensions+ (Array): (<b>requires sqlite3 v2.4.0</b>) Each entry specifies a sqlite extension
37
+ # to load for this database. The entry may be a filesystem path, or the name of a class that
38
+ # responds to +.to_path+ to provide the filesystem path for the extension. See {sqlite3-ruby
39
+ # documentation}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#class-SQLite3::Database-label-SQLite+Extensions]
40
+ # for more information.
41
+ #
42
+ # There may be other options available specific to the SQLite3 driver. Please read the
43
+ # documentation for
44
+ # {SQLite3::Database.new}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#method-c-new]
28
45
  #
29
- # * <tt>:database</tt> - Path to the database file.
30
46
  class SQLite3Adapter < AbstractAdapter
31
47
  ADAPTER_NAME = "SQLite"
32
48
 
@@ -46,10 +62,14 @@ module ActiveRecord
46
62
 
47
63
  args << "-#{options[:mode]}" if options[:mode]
48
64
  args << "-header" if options[:header]
49
- args << File.expand_path(config.database, Rails.respond_to?(:root) ? Rails.root : nil)
65
+ args << File.expand_path(config.database, defined?(Rails.root) ? Rails.root : nil)
50
66
 
51
67
  find_cmd_and_exec(ActiveRecord.database_cli[:sqlite], *args)
52
68
  end
69
+
70
+ def native_database_types # :nodoc:
71
+ NATIVE_DATABASE_TYPES
72
+ end
53
73
  end
54
74
 
55
75
  include SQLite3::Quoting
@@ -58,12 +78,19 @@ module ActiveRecord
58
78
 
59
79
  ##
60
80
  # :singleton-method:
61
- # Configure the SQLite3Adapter to be used in a strict strings mode.
62
- # This will disable double-quoted string literals, because otherwise typos can silently go unnoticed.
63
- # For example, it is possible to create an index for a non existing column.
81
+ #
82
+ # Configure the SQLite3Adapter to be used in a "strict strings" mode. When enabled, this will
83
+ # {disallow double-quoted string literals in SQL
84
+ # statements}[https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted],
85
+ # which may prevent some typographical errors like creating an index for a non-existent
86
+ # column. The default is +false+.
87
+ #
64
88
  # If you wish to enable this mode you can add the following line to your application.rb file:
65
89
  #
66
90
  # config.active_record.sqlite3_adapter_strict_strings_by_default = true
91
+ #
92
+ # This can also be configured on individual databases by setting the +strict:+ option.
93
+ #
67
94
  class_attribute :strict_strings_by_default, default: false
68
95
 
69
96
  NATIVE_DATABASE_TYPES = {
@@ -122,13 +149,18 @@ module ActiveRecord
122
149
  end
123
150
  end
124
151
 
125
- @last_affected_rows = nil
126
152
  @previous_read_uncommitted = nil
127
153
  @config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)
154
+
155
+ extensions = @config.fetch(:extensions, []).map do |extension|
156
+ extension.safe_constantize || extension
157
+ end
158
+
128
159
  @connection_parameters = @config.merge(
129
160
  database: @config[:database].to_s,
130
161
  results_as_hash: true,
131
162
  default_transaction_mode: :immediate,
163
+ extensions: extensions
132
164
  )
133
165
  end
134
166
 
@@ -153,7 +185,7 @@ module ActiveRecord
153
185
  end
154
186
 
155
187
  def supports_expression_index?
156
- database_version >= "3.9.0"
188
+ true
157
189
  end
158
190
 
159
191
  def requires_reloading?
@@ -181,7 +213,7 @@ module ActiveRecord
181
213
  end
182
214
 
183
215
  def supports_common_table_expressions?
184
- database_version >= "3.8.3"
216
+ true
185
217
  end
186
218
 
187
219
  def supports_insert_returning?
@@ -229,10 +261,6 @@ module ActiveRecord
229
261
  true
230
262
  end
231
263
 
232
- def native_database_types # :nodoc:
233
- NATIVE_DATABASE_TYPES
234
- end
235
-
236
264
  # Returns the current database encoding format as a string, e.g. 'UTF-8'
237
265
  def encoding
238
266
  any_raw_connection.encoding.to_s
@@ -309,7 +337,7 @@ module ActiveRecord
309
337
  # Creates a virtual table
310
338
  #
311
339
  # Example:
312
- # create_virtual_table :emails, :fts5, ['sender', 'title',' body']
340
+ # create_virtual_table :emails, :fts5, ['sender', 'title', 'body']
313
341
  def create_virtual_table(table_name, module_name, values)
314
342
  exec_query "CREATE VIRTUAL TABLE IF NOT EXISTS #{table_name} USING #{module_name} (#{values.join(", ")})"
315
343
  end
@@ -478,8 +506,8 @@ module ActiveRecord
478
506
  end
479
507
 
480
508
  def check_version # :nodoc:
481
- if database_version < "3.8.0"
482
- raise "Your version of SQLite (#{database_version}) is too old. Active Record supports SQLite >= 3.8."
509
+ if database_version < "3.23.0"
510
+ raise "Your version of SQLite (#{database_version}) is too old. Active Record supports SQLite >= 3.23.0."
483
511
  end
484
512
  end
485
513
 
@@ -535,6 +563,8 @@ module ActiveRecord
535
563
  # Binary columns
536
564
  when /x'(.*)'/
537
565
  [ $1 ].pack("H*")
566
+ when "TRUE", "FALSE"
567
+ default
538
568
  else
539
569
  # Anything else is blank or some function
540
570
  # and we can't know the value of that, so return nil.
@@ -625,8 +655,8 @@ module ActiveRecord
625
655
  column_options[:stored] = column.virtual_stored?
626
656
  column_options[:type] = column.type
627
657
  elsif column.has_default?
628
- type = lookup_cast_type_from_column(column)
629
- default = type.deserialize(column.default)
658
+ # TODO: Remove fetch_cast_type and the need for connection after we release 8.1.
659
+ default = column.fetch_cast_type(self).deserialize(column.default)
630
660
  default = -> { column.default_function } if default.nil?
631
661
 
632
662
  unless column.auto_increment?
@@ -700,6 +730,8 @@ module ActiveRecord
700
730
  NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
701
731
  elsif exception.message.match?(/FOREIGN KEY constraint failed/i)
702
732
  InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
733
+ elsif exception.message.match?(/CHECK constraint failed: .*/i)
734
+ CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
703
735
  elsif exception.message.match?(/called on a closed database/i)
704
736
  ConnectionNotEstablished.new(exception, connection_pool: @pool)
705
737
  elsif exception.is_a?(::SQLite3::BusyException)
@@ -789,9 +821,9 @@ module ActiveRecord
789
821
 
790
822
  def table_info(table_name)
791
823
  if supports_virtual_columns?
792
- internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
824
+ internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA", allow_retry: true)
793
825
  else
794
- internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
826
+ internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA", allow_retry: true)
795
827
  end
796
828
  end
797
829
 
@@ -818,18 +850,10 @@ module ActiveRecord
818
850
  end
819
851
 
820
852
  def configure_connection
821
- if @config[:timeout] && @config[:retries]
822
- raise ArgumentError, "Cannot specify both timeout and retries arguments"
823
- elsif @config[:timeout]
853
+ if @config[:timeout]
824
854
  timeout = self.class.type_cast_config_to_integer(@config[:timeout])
825
855
  raise TypeError, "timeout must be integer, not #{timeout}" unless timeout.is_a?(Integer)
826
856
  @raw_connection.busy_handler_timeout = timeout
827
- elsif @config[:retries]
828
- ActiveRecord.deprecator.warn(<<~MSG)
829
- The retries option is deprecated and will be removed in Rails 8.1. Use timeout instead.
830
- MSG
831
- retries = self.class.type_cast_config_to_integer(@config[:retries])
832
- raw_connection.busy_handler { |count| count <= retries }
833
857
  end
834
858
 
835
859
  super