activerecord 7.1.5.1 → 8.0.2

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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +369 -2484
  3. data/README.rdoc +15 -15
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +2 -1
  6. data/lib/active_record/associations/alias_tracker.rb +31 -23
  7. data/lib/active_record/associations/association.rb +43 -12
  8. data/lib/active_record/associations/belongs_to_association.rb +21 -8
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/association.rb +7 -6
  11. data/lib/active_record/associations/builder/belongs_to.rb +1 -0
  12. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  13. data/lib/active_record/associations/builder/has_many.rb +3 -4
  14. data/lib/active_record/associations/builder/has_one.rb +3 -4
  15. data/lib/active_record/associations/collection_association.rb +17 -9
  16. data/lib/active_record/associations/collection_proxy.rb +14 -1
  17. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  18. data/lib/active_record/associations/errors.rb +265 -0
  19. data/lib/active_record/associations/has_many_association.rb +1 -1
  20. data/lib/active_record/associations/has_many_through_association.rb +10 -3
  21. data/lib/active_record/associations/join_dependency/join_association.rb +1 -1
  22. data/lib/active_record/associations/nested_error.rb +47 -0
  23. data/lib/active_record/associations/preloader/association.rb +4 -3
  24. data/lib/active_record/associations/preloader/branch.rb +7 -1
  25. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  26. data/lib/active_record/associations/singular_association.rb +14 -3
  27. data/lib/active_record/associations/through_association.rb +1 -1
  28. data/lib/active_record/associations.rb +92 -295
  29. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  30. data/lib/active_record/attribute_assignment.rb +0 -2
  31. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  32. data/lib/active_record/attribute_methods/primary_key.rb +25 -61
  33. data/lib/active_record/attribute_methods/read.rb +1 -13
  34. data/lib/active_record/attribute_methods/serialization.rb +4 -24
  35. data/lib/active_record/attribute_methods/time_zone_conversion.rb +9 -18
  36. data/lib/active_record/attribute_methods.rb +71 -75
  37. data/lib/active_record/attributes.rb +63 -49
  38. data/lib/active_record/autosave_association.rb +92 -57
  39. data/lib/active_record/base.rb +2 -3
  40. data/lib/active_record/callbacks.rb +1 -1
  41. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +48 -122
  42. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  43. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +1 -1
  44. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +286 -77
  45. data/lib/active_record/connection_adapters/abstract/database_statements.rb +119 -55
  46. data/lib/active_record/connection_adapters/abstract/query_cache.rb +197 -76
  47. data/lib/active_record/connection_adapters/abstract/quoting.rb +66 -92
  48. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -5
  49. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +12 -3
  50. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +48 -12
  51. data/lib/active_record/connection_adapters/abstract/transaction.rb +140 -67
  52. data/lib/active_record/connection_adapters/abstract_adapter.rb +85 -90
  53. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +71 -52
  54. data/lib/active_record/connection_adapters/mysql/database_statements.rb +9 -1
  55. data/lib/active_record/connection_adapters/mysql/quoting.rb +50 -57
  56. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  57. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +56 -45
  58. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +92 -101
  59. data/lib/active_record/connection_adapters/mysql2_adapter.rb +13 -31
  60. data/lib/active_record/connection_adapters/pool_config.rb +14 -13
  61. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +86 -41
  62. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  63. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  64. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  65. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  66. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  67. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  68. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -11
  69. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +36 -20
  70. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +3 -2
  71. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +75 -28
  72. data/lib/active_record/connection_adapters/postgresql_adapter.rb +73 -113
  73. data/lib/active_record/connection_adapters/schema_cache.rb +124 -131
  74. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  75. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +81 -97
  76. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +57 -46
  77. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +16 -0
  78. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  79. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +29 -0
  80. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +35 -3
  81. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +183 -87
  82. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  83. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +39 -69
  84. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -65
  85. data/lib/active_record/connection_adapters.rb +65 -0
  86. data/lib/active_record/connection_handling.rb +74 -37
  87. data/lib/active_record/core.rb +132 -51
  88. data/lib/active_record/counter_cache.rb +19 -10
  89. data/lib/active_record/database_configurations/connection_url_resolver.rb +9 -2
  90. data/lib/active_record/database_configurations/database_config.rb +23 -4
  91. data/lib/active_record/database_configurations/hash_config.rb +46 -34
  92. data/lib/active_record/database_configurations/url_config.rb +20 -1
  93. data/lib/active_record/database_configurations.rb +1 -1
  94. data/lib/active_record/delegated_type.rb +41 -17
  95. data/lib/active_record/dynamic_matchers.rb +2 -2
  96. data/lib/active_record/encryption/config.rb +3 -1
  97. data/lib/active_record/encryption/encryptable_record.rb +7 -7
  98. data/lib/active_record/encryption/encrypted_attribute_type.rb +33 -4
  99. data/lib/active_record/encryption/encryptor.rb +28 -6
  100. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  101. data/lib/active_record/encryption/key_provider.rb +1 -1
  102. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  103. data/lib/active_record/encryption/message_serializer.rb +4 -0
  104. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  105. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  106. data/lib/active_record/encryption/scheme.rb +8 -1
  107. data/lib/active_record/enum.rb +20 -16
  108. data/lib/active_record/errors.rb +54 -20
  109. data/lib/active_record/explain.rb +13 -24
  110. data/lib/active_record/fixtures.rb +37 -33
  111. data/lib/active_record/future_result.rb +21 -13
  112. data/lib/active_record/gem_version.rb +4 -4
  113. data/lib/active_record/inheritance.rb +4 -2
  114. data/lib/active_record/insert_all.rb +19 -16
  115. data/lib/active_record/integration.rb +4 -1
  116. data/lib/active_record/internal_metadata.rb +48 -34
  117. data/lib/active_record/locking/optimistic.rb +8 -7
  118. data/lib/active_record/log_subscriber.rb +5 -32
  119. data/lib/active_record/message_pack.rb +1 -1
  120. data/lib/active_record/migration/command_recorder.rb +33 -14
  121. data/lib/active_record/migration/compatibility.rb +8 -3
  122. data/lib/active_record/migration/default_strategy.rb +4 -5
  123. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  124. data/lib/active_record/migration.rb +104 -98
  125. data/lib/active_record/model_schema.rb +32 -70
  126. data/lib/active_record/nested_attributes.rb +15 -9
  127. data/lib/active_record/normalization.rb +3 -7
  128. data/lib/active_record/persistence.rb +127 -451
  129. data/lib/active_record/query_cache.rb +19 -8
  130. data/lib/active_record/query_logs.rb +104 -37
  131. data/lib/active_record/query_logs_formatter.rb +17 -28
  132. data/lib/active_record/querying.rb +24 -12
  133. data/lib/active_record/railtie.rb +26 -68
  134. data/lib/active_record/railties/controller_runtime.rb +13 -4
  135. data/lib/active_record/railties/databases.rake +43 -61
  136. data/lib/active_record/reflection.rb +112 -53
  137. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  138. data/lib/active_record/relation/batches.rb +138 -72
  139. data/lib/active_record/relation/calculations.rb +122 -82
  140. data/lib/active_record/relation/delegation.rb +30 -22
  141. data/lib/active_record/relation/finder_methods.rb +32 -18
  142. data/lib/active_record/relation/merger.rb +12 -14
  143. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  144. data/lib/active_record/relation/predicate_builder/association_query_value.rb +10 -2
  145. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  146. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  147. data/lib/active_record/relation/predicate_builder.rb +16 -3
  148. data/lib/active_record/relation/query_attribute.rb +1 -1
  149. data/lib/active_record/relation/query_methods.rb +317 -101
  150. data/lib/active_record/relation/spawn_methods.rb +3 -19
  151. data/lib/active_record/relation/where_clause.rb +7 -19
  152. data/lib/active_record/relation.rb +561 -119
  153. data/lib/active_record/result.rb +95 -46
  154. data/lib/active_record/runtime_registry.rb +39 -0
  155. data/lib/active_record/sanitization.rb +31 -25
  156. data/lib/active_record/schema.rb +8 -6
  157. data/lib/active_record/schema_dumper.rb +53 -20
  158. data/lib/active_record/schema_migration.rb +31 -14
  159. data/lib/active_record/scoping/named.rb +6 -2
  160. data/lib/active_record/signed_id.rb +24 -4
  161. data/lib/active_record/statement_cache.rb +19 -19
  162. data/lib/active_record/store.rb +7 -3
  163. data/lib/active_record/table_metadata.rb +2 -13
  164. data/lib/active_record/tasks/database_tasks.rb +87 -58
  165. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -3
  166. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  167. data/lib/active_record/tasks/sqlite_database_tasks.rb +4 -3
  168. data/lib/active_record/test_fixtures.rb +98 -89
  169. data/lib/active_record/testing/query_assertions.rb +121 -0
  170. data/lib/active_record/timestamp.rb +2 -2
  171. data/lib/active_record/token_for.rb +22 -12
  172. data/lib/active_record/touch_later.rb +1 -1
  173. data/lib/active_record/transaction.rb +132 -0
  174. data/lib/active_record/transactions.rb +72 -17
  175. data/lib/active_record/translation.rb +0 -2
  176. data/lib/active_record/type/serialized.rb +1 -3
  177. data/lib/active_record/type_caster/connection.rb +4 -4
  178. data/lib/active_record/validations/associated.rb +9 -3
  179. data/lib/active_record/validations/uniqueness.rb +23 -18
  180. data/lib/active_record/validations.rb +4 -1
  181. data/lib/active_record.rb +138 -57
  182. data/lib/arel/alias_predication.rb +1 -1
  183. data/lib/arel/collectors/bind.rb +4 -2
  184. data/lib/arel/collectors/composite.rb +7 -0
  185. data/lib/arel/collectors/sql_string.rb +2 -2
  186. data/lib/arel/collectors/substitute_binds.rb +3 -3
  187. data/lib/arel/nodes/binary.rb +1 -7
  188. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  189. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  190. data/lib/arel/nodes/node.rb +5 -4
  191. data/lib/arel/nodes/sql_literal.rb +8 -1
  192. data/lib/arel/nodes.rb +2 -2
  193. data/lib/arel/predications.rb +1 -1
  194. data/lib/arel/select_manager.rb +1 -1
  195. data/lib/arel/table.rb +3 -7
  196. data/lib/arel/tree_manager.rb +3 -2
  197. data/lib/arel/update_manager.rb +2 -1
  198. data/lib/arel/visitors/dot.rb +1 -0
  199. data/lib/arel/visitors/mysql.rb +9 -4
  200. data/lib/arel/visitors/postgresql.rb +1 -12
  201. data/lib/arel/visitors/sqlite.rb +25 -0
  202. data/lib/arel/visitors/to_sql.rb +29 -16
  203. data/lib/arel.rb +7 -3
  204. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  205. metadata +18 -16
  206. data/lib/active_record/relation/record_fetch_warning.rb +0 -49
@@ -11,20 +11,13 @@ require "active_record/connection_adapters/sqlite3/schema_definitions"
11
11
  require "active_record/connection_adapters/sqlite3/schema_dumper"
12
12
  require "active_record/connection_adapters/sqlite3/schema_statements"
13
13
 
14
- gem "sqlite3", ">= 1.4"
14
+ gem "sqlite3", ">= 2.1"
15
15
  require "sqlite3"
16
16
 
17
- module ActiveRecord
18
- module ConnectionHandling # :nodoc:
19
- def sqlite3_adapter_class
20
- ConnectionAdapters::SQLite3Adapter
21
- end
22
-
23
- def sqlite3_connection(config)
24
- sqlite3_adapter_class.new(config)
25
- end
26
- end
17
+ # Suppress the warning that SQLite3 issues when open writable connections are carried across fork()
18
+ SQLite3::ForkSafety.suppress_warnings!
27
19
 
20
+ module ActiveRecord
28
21
  module ConnectionAdapters # :nodoc:
29
22
  # = Active Record SQLite3 Adapter
30
23
  #
@@ -55,7 +48,7 @@ module ActiveRecord
55
48
  args << "-header" if options[:header]
56
49
  args << File.expand_path(config.database, Rails.respond_to?(:root) ? Rails.root : nil)
57
50
 
58
- find_cmd_and_exec("sqlite3", *args)
51
+ find_cmd_and_exec(ActiveRecord.database_cli[:sqlite], *args)
59
52
  end
60
53
  end
61
54
 
@@ -88,6 +81,15 @@ module ActiveRecord
88
81
  json: { name: "json" },
89
82
  }
90
83
 
84
+ DEFAULT_PRAGMAS = {
85
+ "foreign_keys" => true,
86
+ "journal_mode" => :wal,
87
+ "synchronous" => :normal,
88
+ "mmap_size" => 134217728, # 128 megabytes
89
+ "journal_size_limit" => 67108864, # 64 megabytes
90
+ "cache_size" => 2000
91
+ }
92
+
91
93
  class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
92
94
  alias reset clear
93
95
 
@@ -113,20 +115,21 @@ module ActiveRecord
113
115
  dirname = File.dirname(@config[:database])
114
116
  unless File.directory?(dirname)
115
117
  begin
116
- Dir.mkdir(dirname)
117
- rescue Errno::ENOENT => error
118
- if error.message.include?("No such file or directory")
119
- raise ActiveRecord::NoDatabaseError.new(connection_pool: @pool)
120
- else
121
- raise
122
- end
118
+ FileUtils.mkdir_p(dirname)
119
+ rescue SystemCallError
120
+ raise ActiveRecord::NoDatabaseError.new(connection_pool: @pool)
123
121
  end
124
122
  end
125
123
  end
126
124
 
125
+ @last_affected_rows = nil
126
+ @previous_read_uncommitted = nil
127
127
  @config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)
128
- @connection_parameters = @config.merge(database: @config[:database].to_s, results_as_hash: true)
129
- @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
128
+ @connection_parameters = @config.merge(
129
+ database: @config[:database].to_s,
130
+ results_as_hash: true,
131
+ default_transaction_mode: :immediate,
132
+ )
130
133
  end
131
134
 
132
135
  def database_exists?
@@ -196,12 +199,19 @@ module ActiveRecord
196
199
  !@memory_database
197
200
  end
198
201
 
199
- def active?
200
- @raw_connection && !@raw_connection.closed?
202
+ def supports_virtual_columns?
203
+ database_version >= "3.31.0"
204
+ end
205
+
206
+ def connected?
207
+ !(@raw_connection.nil? || @raw_connection.closed?)
201
208
  end
202
209
 
203
- def return_value_after_insert?(column) # :nodoc:
204
- column.auto_populated?
210
+ def active?
211
+ if connected?
212
+ verified!
213
+ true
214
+ end
205
215
  end
206
216
 
207
217
  alias :reset! :reconnect!
@@ -236,6 +246,10 @@ module ActiveRecord
236
246
  true
237
247
  end
238
248
 
249
+ def supports_deferrable_constraints?
250
+ true
251
+ end
252
+
239
253
  # REFERENTIAL INTEGRITY ====================================
240
254
 
241
255
  def disable_referential_integrity # :nodoc:
@@ -258,7 +272,7 @@ module ActiveRecord
258
272
 
259
273
  unless result.blank?
260
274
  tables = result.map { |row| row["table"] }
261
- raise ActiveRecord::StatementInvalid.new("Foreign key violations found: #{tables.join(", ")}", sql: sql)
275
+ raise ActiveRecord::StatementInvalid.new("Foreign key violations found: #{tables.join(", ")}", sql: sql, connection_pool: @pool)
262
276
  end
263
277
  end
264
278
 
@@ -277,6 +291,38 @@ module ActiveRecord
277
291
  exec_query "DROP INDEX #{quote_column_name(index_name)}"
278
292
  end
279
293
 
294
+ VIRTUAL_TABLE_REGEX = /USING\s+(\w+)\s*\((.+)\)/i
295
+
296
+ # Returns a list of defined virtual tables
297
+ def virtual_tables
298
+ query = <<~SQL
299
+ SELECT name, sql FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL %';
300
+ SQL
301
+
302
+ exec_query(query, "SCHEMA").cast_values.each_with_object({}) do |row, memo|
303
+ table_name, sql = row[0], row[1]
304
+ _, module_name, arguments = sql.match(VIRTUAL_TABLE_REGEX).to_a
305
+ memo[table_name] = [module_name, arguments]
306
+ end.to_a
307
+ end
308
+
309
+ # Creates a virtual table
310
+ #
311
+ # Example:
312
+ # create_virtual_table :emails, :fts5, ['sender', 'title',' body']
313
+ def create_virtual_table(table_name, module_name, values)
314
+ exec_query "CREATE VIRTUAL TABLE IF NOT EXISTS #{table_name} USING #{module_name} (#{values.join(", ")})"
315
+ end
316
+
317
+ # Drops a virtual table
318
+ #
319
+ # Although this command ignores +module_name+ and +values+,
320
+ # it can be helpful to provide these in a migration's +change+ method so it can be reverted.
321
+ # In that case, +module_name+, +values+ and +options+ will be used by #create_virtual_table.
322
+ def drop_virtual_table(table_name, module_name, values, **options)
323
+ drop_table(table_name)
324
+ end
325
+
280
326
  # Renames a table.
281
327
  #
282
328
  # Example:
@@ -290,6 +336,7 @@ module ActiveRecord
290
336
  end
291
337
 
292
338
  def add_column(table_name, column_name, type, **options) # :nodoc:
339
+ type = type.to_sym
293
340
  if invalid_alter_table_type?(type, options)
294
341
  alter_table(table_name) do |definition|
295
342
  definition.column(column_name, type, **options)
@@ -365,15 +412,31 @@ module ActiveRecord
365
412
  end
366
413
  alias :add_belongs_to :add_reference
367
414
 
415
+ FK_REGEX = /.*FOREIGN KEY\s+\("([^"]+)"\)\s+REFERENCES\s+"(\w+)"\s+\("(\w+)"\)/
416
+ DEFERRABLE_REGEX = /DEFERRABLE INITIALLY (\w+)/
368
417
  def foreign_keys(table_name)
369
418
  # SQLite returns 1 row for each column of composite foreign keys.
370
419
  fk_info = internal_exec_query("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA")
420
+ # Deferred or immediate foreign keys can only be seen in the CREATE TABLE sql
421
+ fk_defs = table_structure_sql(table_name)
422
+ .select do |column_string|
423
+ column_string.start_with?("CONSTRAINT") &&
424
+ column_string.include?("FOREIGN KEY")
425
+ end
426
+ .to_h do |fk_string|
427
+ _, from, table, to = fk_string.match(FK_REGEX).to_a
428
+ _, mode = fk_string.match(DEFERRABLE_REGEX).to_a
429
+ deferred = mode&.downcase&.to_sym || false
430
+ [[table, from, to], deferred]
431
+ end
432
+
371
433
  grouped_fk = fk_info.group_by { |row| row["id"] }.values.each { |group| group.sort_by! { |row| row["seq"] } }
372
434
  grouped_fk.map do |group|
373
435
  row = group.first
374
436
  options = {
375
437
  on_delete: extract_foreign_key_action(row["on_delete"]),
376
- on_update: extract_foreign_key_action(row["on_update"])
438
+ on_update: extract_foreign_key_action(row["on_update"]),
439
+ deferrable: fk_defs[[row["table"], row["from"], row["to"]]]
377
440
  }
378
441
 
379
442
  if group.one?
@@ -410,10 +473,6 @@ module ActiveRecord
410
473
  @config.fetch(:flags, 0).anybits?(::SQLite3::Constants::Open::SHAREDCACHE)
411
474
  end
412
475
 
413
- def use_insert_returning?
414
- @use_insert_returning
415
- end
416
-
417
476
  def get_database_version # :nodoc:
418
477
  SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)", "SCHEMA"))
419
478
  end
@@ -454,8 +513,8 @@ module ActiveRecord
454
513
  end
455
514
 
456
515
  def table_structure(table_name)
457
- structure = internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
458
- raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
516
+ structure = table_info(table_name)
517
+ raise ActiveRecord::StatementInvalid.new("Could not find table '#{table_name}'", connection_pool: @pool) if structure.empty?
459
518
  table_structure_with_collation(table_name, structure)
460
519
  end
461
520
  alias column_definitions table_structure
@@ -494,8 +553,9 @@ module ActiveRecord
494
553
  # See: https://www.sqlite.org/lang_altertable.html
495
554
  # SQLite has an additional restriction on the ALTER TABLE statement
496
555
  def invalid_alter_table_type?(type, options)
497
- type.to_sym == :primary_key || options[:primary_key] ||
498
- options[:null] == false && options[:default].nil?
556
+ type == :primary_key || options[:primary_key] ||
557
+ options[:null] == false && options[:default].nil? ||
558
+ (type == :virtual && options[:stored])
499
559
  end
500
560
 
501
561
  def alter_table(
@@ -551,12 +611,6 @@ module ActiveRecord
551
611
  options[:rename][column.name.to_sym] ||
552
612
  column.name) : column.name
553
613
 
554
- if column.has_default?
555
- type = lookup_cast_type_from_column(column)
556
- default = type.deserialize(column.default)
557
- default = -> { column.default_function } if default.nil?
558
- end
559
-
560
614
  column_options = {
561
615
  limit: column.limit,
562
616
  precision: column.precision,
@@ -566,19 +620,31 @@ module ActiveRecord
566
620
  primary_key: column_name == from_primary_key
567
621
  }
568
622
 
569
- unless column.auto_increment?
570
- column_options[:default] = default
623
+ if column.virtual?
624
+ column_options[:as] = column.default_function
625
+ column_options[:stored] = column.virtual_stored?
626
+ column_options[:type] = column.type
627
+ elsif column.has_default?
628
+ type = lookup_cast_type_from_column(column)
629
+ default = type.deserialize(column.default)
630
+ default = -> { column.default_function } if default.nil?
631
+
632
+ unless column.auto_increment?
633
+ column_options[:default] = default
634
+ end
571
635
  end
572
636
 
573
- column_type = column.bigint? ? :bigint : column.type
637
+ column_type = column.virtual? ? :virtual : (column.bigint? ? :bigint : column.type)
574
638
  @definition.column(column_name, column_type, **column_options)
575
639
  end
576
640
 
577
641
  yield @definition if block_given?
578
642
  end
579
643
  copy_table_indexes(from, to, options[:rename] || {})
644
+
645
+ columns_to_copy = @definition.columns.reject { |col| col.options.key?(:as) }.map(&:name)
580
646
  copy_table_contents(from, to,
581
- @definition.columns.map(&:name),
647
+ columns_to_copy,
582
648
  options[:rename] || {})
583
649
  end
584
650
 
@@ -636,6 +702,8 @@ module ActiveRecord
636
702
  InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
637
703
  elsif exception.message.match?(/called on a closed database/i)
638
704
  ConnectionNotEstablished.new(exception, connection_pool: @pool)
705
+ elsif exception.is_a?(::SQLite3::BusyException)
706
+ StatementTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
639
707
  else
640
708
  super
641
709
  end
@@ -643,32 +711,22 @@ module ActiveRecord
643
711
 
644
712
  COLLATE_REGEX = /.*"(\w+)".*collate\s+"(\w+)".*/i
645
713
  PRIMARY_KEY_AUTOINCREMENT_REGEX = /.*"(\w+)".+PRIMARY KEY AUTOINCREMENT/i
714
+ GENERATED_ALWAYS_AS_REGEX = /.*"(\w+)".+GENERATED ALWAYS AS \((.+)\) (?:STORED|VIRTUAL)/i
646
715
 
647
716
  def table_structure_with_collation(table_name, basic_structure)
648
717
  collation_hash = {}
649
718
  auto_increments = {}
650
- sql = <<~SQL
651
- SELECT sql FROM
652
- (SELECT * FROM sqlite_master UNION ALL
653
- SELECT * FROM sqlite_temp_master)
654
- WHERE type = 'table' AND name = #{quote(table_name)}
655
- SQL
656
-
657
- # Result will have following sample string
658
- # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
659
- # "password_digest" varchar COLLATE "NOCASE");
660
- result = query_value(sql, "SCHEMA")
719
+ generated_columns = {}
661
720
 
662
- if result
663
- # Splitting with left parentheses and discarding the first part will return all
664
- # columns separated with comma(,).
665
- columns_string = result.split("(", 2).last
721
+ column_strings = table_structure_sql(table_name, basic_structure.map { |column| column["name"] })
666
722
 
667
- columns_string.split(",").each do |column_string|
723
+ if column_strings.any?
724
+ column_strings.each do |column_string|
668
725
  # This regex will match the column name and collation type and will save
669
726
  # the value in $1 and $2 respectively.
670
727
  collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string
671
728
  auto_increments[$1] = true if PRIMARY_KEY_AUTOINCREMENT_REGEX =~ column_string
729
+ generated_columns[$1] = $2 if GENERATED_ALWAYS_AS_REGEX =~ column_string
672
730
  end
673
731
 
674
732
  basic_structure.map do |column|
@@ -682,6 +740,10 @@ module ActiveRecord
682
740
  column["auto_increment"] = true
683
741
  end
684
742
 
743
+ if generated_columns.has_key?(column_name)
744
+ column["dflt_value"] = generated_columns[column_name]
745
+ end
746
+
685
747
  column
686
748
  end
687
749
  else
@@ -689,6 +751,50 @@ module ActiveRecord
689
751
  end
690
752
  end
691
753
 
754
+ UNQUOTED_OPEN_PARENS_REGEX = /\((?![^'"]*['"][^'"]*$)/
755
+ FINAL_CLOSE_PARENS_REGEX = /\);*\z/
756
+
757
+ def table_structure_sql(table_name, column_names = nil)
758
+ unless column_names
759
+ column_info = table_info(table_name)
760
+ column_names = column_info.map { |column| column["name"] }
761
+ end
762
+
763
+ sql = <<~SQL
764
+ SELECT sql FROM
765
+ (SELECT * FROM sqlite_master UNION ALL
766
+ SELECT * FROM sqlite_temp_master)
767
+ WHERE type = 'table' AND name = #{quote(table_name)}
768
+ SQL
769
+
770
+ # Result will have following sample string
771
+ # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
772
+ # "password_digest" varchar COLLATE "NOCASE",
773
+ # "o_id" integer,
774
+ # CONSTRAINT "fk_rails_78146ddd2e" FOREIGN KEY ("o_id") REFERENCES "os" ("id"));
775
+ result = query_value(sql, "SCHEMA")
776
+
777
+ return [] unless result
778
+
779
+ # Splitting with left parentheses and discarding the first part will return all
780
+ # columns separated with comma(,).
781
+ result.partition(UNQUOTED_OPEN_PARENS_REGEX)
782
+ .last
783
+ .sub(FINAL_CLOSE_PARENS_REGEX, "")
784
+ # column definitions can have a comma in them, so split on commas followed
785
+ # by a space and a column name in quotes or followed by the keyword CONSTRAINT
786
+ .split(/,(?=\s(?:CONSTRAINT|"(?:#{Regexp.union(column_names).source})"))/i)
787
+ .map(&:strip)
788
+ end
789
+
790
+ def table_info(table_name)
791
+ if supports_virtual_columns?
792
+ internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
793
+ else
794
+ internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
795
+ end
796
+ end
797
+
692
798
  def arel_visitor
693
799
  Arel::Visitors::SQLite.new(self)
694
800
  end
@@ -715,37 +821,27 @@ module ActiveRecord
715
821
  if @config[:timeout] && @config[:retries]
716
822
  raise ArgumentError, "Cannot specify both timeout and retries arguments"
717
823
  elsif @config[:timeout]
718
- @raw_connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout]))
824
+ timeout = self.class.type_cast_config_to_integer(@config[:timeout])
825
+ raise TypeError, "timeout must be integer, not #{timeout}" unless timeout.is_a?(Integer)
826
+ @raw_connection.busy_handler_timeout = timeout
719
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
720
831
  retries = self.class.type_cast_config_to_integer(@config[:retries])
721
- raw_connection.busy_handler do |count|
722
- count <= retries
723
- end
832
+ raw_connection.busy_handler { |count| count <= retries }
724
833
  end
725
834
 
726
- # Enforce foreign key constraints
727
- # https://www.sqlite.org/pragma.html#pragma_foreign_keys
728
- # https://www.sqlite.org/foreignkeys.html
729
- raw_execute("PRAGMA foreign_keys = ON", "SCHEMA")
730
- unless @memory_database
731
- # Journal mode WAL allows for greater concurrency (many readers + one writer)
732
- # https://www.sqlite.org/pragma.html#pragma_journal_mode
733
- raw_execute("PRAGMA journal_mode = WAL", "SCHEMA")
734
- # Set more relaxed level of database durability
735
- # 2 = "FULL" (sync on every write), 1 = "NORMAL" (sync every 1000 written pages) and 0 = "NONE"
736
- # https://www.sqlite.org/pragma.html#pragma_synchronous
737
- raw_execute("PRAGMA synchronous = NORMAL", "SCHEMA")
738
- # Set the global memory map so all processes can share some data
739
- # https://www.sqlite.org/pragma.html#pragma_mmap_size
740
- # https://www.sqlite.org/mmap.html
741
- raw_execute("PRAGMA mmap_size = #{128.megabytes}", "SCHEMA")
835
+ super
836
+
837
+ pragmas = @config.fetch(:pragmas, {}).stringify_keys
838
+ DEFAULT_PRAGMAS.merge(pragmas).each do |pragma, value|
839
+ if ::SQLite3::Pragmas.method_defined?("#{pragma}=")
840
+ @raw_connection.public_send("#{pragma}=", value)
841
+ else
842
+ warn "Unknown SQLite pragma: #{pragma}"
843
+ end
742
844
  end
743
- # Impose a limit on the WAL file to prevent unlimited growth
744
- # https://www.sqlite.org/pragma.html#pragma_journal_size_limit
745
- raw_execute("PRAGMA journal_size_limit = #{64.megabytes}", "SCHEMA")
746
- # Set the local connection cache to 2000 pages
747
- # https://www.sqlite.org/pragma.html#pragma_cache_size
748
- raw_execute("PRAGMA cache_size = 2000", "SCHEMA")
749
845
  end
750
846
  end
751
847
  ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)
@@ -50,8 +50,10 @@ module ActiveRecord
50
50
  end
51
51
 
52
52
  def delete(key)
53
- dealloc cache[key]
54
- cache.delete(key)
53
+ if stmt = cache.delete(key)
54
+ dealloc(stmt)
55
+ end
56
+ stmt
55
57
  end
56
58
 
57
59
  private
@@ -4,93 +4,63 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module Trilogy
6
6
  module DatabaseStatements
7
- def select_all(*, **) # :nodoc:
8
- result = super
9
- with_raw_connection do |conn|
10
- conn.next_result while conn.more_results_exist?
11
- end
12
- result
13
- end
14
-
15
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) # :nodoc:
16
- sql = transform_query(sql)
17
- check_if_write_query(sql)
18
- mark_transaction_written_if_write(sql)
19
-
20
- result = raw_execute(sql, name, async: async)
21
- ActiveRecord::Result.new(result.fields, result.to_a)
22
- end
23
-
24
7
  def exec_insert(sql, name, binds, pk = nil, sequence_name = nil, returning: nil) # :nodoc:
25
- sql = transform_query(sql)
26
- check_if_write_query(sql)
27
- mark_transaction_written_if_write(sql)
28
-
29
- raw_execute(to_sql(sql, binds), name)
8
+ sql, _binds = sql_for_insert(sql, pk, binds, returning)
9
+ internal_execute(sql, name)
30
10
  end
31
11
 
32
- def exec_delete(sql, name = nil, binds = []) # :nodoc:
33
- sql = transform_query(sql)
34
- check_if_write_query(sql)
35
- mark_transaction_written_if_write(sql)
36
-
37
- result = raw_execute(to_sql(sql, binds), name)
38
- result.affected_rows
39
- end
40
-
41
- alias :exec_update :exec_delete # :nodoc:
42
-
43
12
  private
44
- def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
45
- log(sql, name, async: async) do
46
- with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
47
- sync_timezone_changes(conn)
48
- result = conn.query(sql)
49
- verified!
50
- handle_warnings(sql)
51
- result
52
- end
13
+ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
14
+ reset_multi_statement = if batch && !@config[:multi_statement]
15
+ raw_connection.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_ON)
16
+ true
53
17
  end
54
- end
55
18
 
56
- def last_inserted_id(result)
57
- result.last_insert_id
58
- end
59
-
60
- def sync_timezone_changes(conn)
61
- # Sync any changes since connection last established.
19
+ # Make sure we carry over any changes to ActiveRecord.default_timezone that have been
20
+ # made since we established the connection
62
21
  if default_timezone == :local
63
- conn.query_flags |= ::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
22
+ raw_connection.query_flags |= ::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
64
23
  else
65
- conn.query_flags &= ~::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
24
+ raw_connection.query_flags &= ~::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
25
+ end
26
+
27
+ result = raw_connection.query(sql)
28
+ while raw_connection.more_results_exist?
29
+ raw_connection.next_result
30
+ end
31
+ verified!
32
+ handle_warnings(sql)
33
+ notification_payload[:row_count] = result.count
34
+ result
35
+ ensure
36
+ if reset_multi_statement && active?
37
+ raw_connection.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF)
66
38
  end
67
39
  end
68
40
 
69
- def execute_batch(statements, name = nil)
70
- statements = statements.map { |sql| transform_query(sql) }
71
- combine_multi_statements(statements).each do |statement|
72
- with_raw_connection do |conn|
73
- raw_execute(statement, name)
74
- conn.next_result while conn.more_results_exist?
75
- end
41
+ def cast_result(result)
42
+ if result.fields.empty?
43
+ ActiveRecord::Result.empty
44
+ else
45
+ ActiveRecord::Result.new(result.fields, result.rows)
76
46
  end
77
47
  end
78
48
 
79
- def multi_statements_enabled?
80
- !!@config[:multi_statement]
49
+ def affected_rows(result)
50
+ result.affected_rows
81
51
  end
82
52
 
83
- def with_multi_statements
84
- if multi_statements_enabled?
85
- return yield
53
+ def last_inserted_id(result)
54
+ if supports_insert_returning?
55
+ super
56
+ else
57
+ result.last_insert_id
86
58
  end
59
+ end
87
60
 
88
- with_raw_connection do |conn|
89
- conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_ON)
90
-
91
- yield
92
- ensure
93
- conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF)
61
+ def execute_batch(statements, name = nil, **kwargs)
62
+ combine_multi_statements(statements).each do |statement|
63
+ raw_execute(statement, name, batch: true, **kwargs)
94
64
  end
95
65
  end
96
66
  end