activerecord 7.2.2 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +239 -878
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/association_relation.rb +1 -0
  5. data/lib/active_record/associations/association.rb +34 -10
  6. data/lib/active_record/associations/builder/association.rb +7 -6
  7. data/lib/active_record/associations/collection_association.rb +10 -8
  8. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  9. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  10. data/lib/active_record/associations/preloader/association.rb +2 -2
  11. data/lib/active_record/associations/singular_association.rb +8 -3
  12. data/lib/active_record/associations.rb +34 -4
  13. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  14. data/lib/active_record/attribute_methods/primary_key.rb +2 -7
  15. data/lib/active_record/attribute_methods/time_zone_conversion.rb +2 -12
  16. data/lib/active_record/attribute_methods.rb +1 -1
  17. data/lib/active_record/autosave_association.rb +69 -27
  18. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
  19. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  20. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  21. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +0 -9
  22. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  23. data/lib/active_record/connection_adapters/abstract/query_cache.rb +8 -2
  24. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  25. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -5
  26. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +7 -2
  27. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +33 -6
  28. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  29. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -26
  30. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +21 -39
  31. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  32. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  33. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +43 -45
  34. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +42 -98
  35. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -8
  36. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
  37. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  38. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  39. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  40. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -11
  41. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +6 -12
  42. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +2 -1
  43. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +59 -16
  44. data/lib/active_record/connection_adapters/postgresql_adapter.rb +45 -95
  45. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  46. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
  47. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  48. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  49. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -1
  50. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -12
  51. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  52. data/lib/active_record/connection_adapters/trilogy_adapter.rb +0 -17
  53. data/lib/active_record/connection_adapters.rb +0 -56
  54. data/lib/active_record/connection_handling.rb +22 -0
  55. data/lib/active_record/core.rb +18 -14
  56. data/lib/active_record/database_configurations/database_config.rb +4 -0
  57. data/lib/active_record/database_configurations/hash_config.rb +8 -0
  58. data/lib/active_record/encryption/config.rb +3 -1
  59. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  60. data/lib/active_record/encryption/encrypted_attribute_type.rb +10 -1
  61. data/lib/active_record/encryption/encryptor.rb +15 -8
  62. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  63. data/lib/active_record/encryption/scheme.rb +8 -1
  64. data/lib/active_record/enum.rb +9 -22
  65. data/lib/active_record/errors.rb +13 -5
  66. data/lib/active_record/fixtures.rb +0 -2
  67. data/lib/active_record/future_result.rb +14 -10
  68. data/lib/active_record/gem_version.rb +3 -3
  69. data/lib/active_record/insert_all.rb +1 -1
  70. data/lib/active_record/locking/optimistic.rb +1 -1
  71. data/lib/active_record/log_subscriber.rb +5 -11
  72. data/lib/active_record/migration/command_recorder.rb +27 -10
  73. data/lib/active_record/migration/compatibility.rb +5 -2
  74. data/lib/active_record/migration.rb +35 -38
  75. data/lib/active_record/model_schema.rb +3 -4
  76. data/lib/active_record/nested_attributes.rb +4 -6
  77. data/lib/active_record/persistence.rb +128 -130
  78. data/lib/active_record/query_logs.rb +102 -50
  79. data/lib/active_record/query_logs_formatter.rb +17 -28
  80. data/lib/active_record/querying.rb +8 -8
  81. data/lib/active_record/railtie.rb +2 -26
  82. data/lib/active_record/railties/databases.rake +2 -17
  83. data/lib/active_record/reflection.rb +18 -21
  84. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  85. data/lib/active_record/relation/batches.rb +132 -72
  86. data/lib/active_record/relation/calculations.rb +40 -39
  87. data/lib/active_record/relation/delegation.rb +25 -14
  88. data/lib/active_record/relation/finder_methods.rb +18 -18
  89. data/lib/active_record/relation/merger.rb +8 -8
  90. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  91. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  92. data/lib/active_record/relation/predicate_builder.rb +13 -0
  93. data/lib/active_record/relation/query_methods.rb +115 -65
  94. data/lib/active_record/relation/spawn_methods.rb +1 -1
  95. data/lib/active_record/relation.rb +79 -61
  96. data/lib/active_record/result.rb +66 -4
  97. data/lib/active_record/sanitization.rb +7 -6
  98. data/lib/active_record/schema_dumper.rb +5 -0
  99. data/lib/active_record/schema_migration.rb +2 -1
  100. data/lib/active_record/scoping/named.rb +5 -2
  101. data/lib/active_record/statement_cache.rb +12 -12
  102. data/lib/active_record/store.rb +7 -3
  103. data/lib/active_record/table_metadata.rb +1 -3
  104. data/lib/active_record/tasks/database_tasks.rb +48 -47
  105. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  106. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  107. data/lib/active_record/test_fixtures.rb +12 -0
  108. data/lib/active_record/token_for.rb +1 -1
  109. data/lib/active_record/validations/uniqueness.rb +8 -8
  110. data/lib/active_record.rb +15 -45
  111. data/lib/arel/collectors/bind.rb +1 -1
  112. data/lib/arel/table.rb +3 -7
  113. metadata +11 -12
  114. data/lib/active_record/relation/record_fetch_warning.rb +0 -52
@@ -21,87 +21,24 @@ module ActiveRecord
21
21
  SQLite3::ExplainPrettyPrinter.new.pp(result)
22
22
  end
23
23
 
24
- def internal_exec_query(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
25
- sql = transform_query(sql)
26
- check_if_write_query(sql)
27
-
28
- mark_transaction_written_if_write(sql)
29
-
30
- type_casted_binds = type_casted_binds(binds)
31
-
32
- log(sql, name, binds, type_casted_binds, async: async) do |notification_payload|
33
- with_raw_connection do |conn|
34
- # Don't cache statements if they are not prepared
35
- unless prepare
36
- stmt = conn.prepare(sql)
37
- begin
38
- cols = stmt.columns
39
- unless without_prepared_statement?(binds)
40
- stmt.bind_params(type_casted_binds)
41
- end
42
- records = stmt.to_a
43
- ensure
44
- stmt.close
45
- end
46
- else
47
- stmt = @statements[sql] ||= conn.prepare(sql)
48
- cols = stmt.columns
49
- stmt.reset!
50
- stmt.bind_params(type_casted_binds)
51
- records = stmt.to_a
52
- end
53
- verified!
54
-
55
- result = build_result(columns: cols, rows: records)
56
- notification_payload[:row_count] = result.length
57
- result
58
- end
59
- end
60
- end
61
-
62
- def exec_delete(sql, name = "SQL", binds = []) # :nodoc:
63
- internal_exec_query(sql, name, binds)
64
- @raw_connection.changes
24
+ def begin_deferred_transaction(isolation = nil) # :nodoc:
25
+ internal_begin_transaction(:deferred, isolation)
65
26
  end
66
- alias :exec_update :exec_delete
67
27
 
68
28
  def begin_isolated_db_transaction(isolation) # :nodoc:
69
- raise TransactionIsolationError, "SQLite3 only supports the `read_uncommitted` transaction isolation level" if isolation != :read_uncommitted
70
- raise StandardError, "You need to enable the shared-cache mode in SQLite mode before attempting to change the transaction isolation level" unless shared_cache?
71
-
72
- with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
73
- ActiveSupport::IsolatedExecutionState[:active_record_read_uncommitted] = conn.get_first_value("PRAGMA read_uncommitted")
74
- conn.read_uncommitted = true
75
- begin_db_transaction
76
- end
29
+ internal_begin_transaction(:deferred, isolation)
77
30
  end
78
31
 
79
32
  def begin_db_transaction # :nodoc:
80
- log("begin transaction", "TRANSACTION") do
81
- with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
82
- result = conn.transaction
83
- verified!
84
- result
85
- end
86
- end
33
+ internal_begin_transaction(:immediate, nil)
87
34
  end
88
35
 
89
36
  def commit_db_transaction # :nodoc:
90
- log("commit transaction", "TRANSACTION") do
91
- with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
92
- conn.commit
93
- end
94
- end
95
- reset_read_uncommitted
37
+ internal_execute("COMMIT TRANSACTION", "TRANSACTION", allow_retry: true, materialize_transactions: false)
96
38
  end
97
39
 
98
40
  def exec_rollback_db_transaction # :nodoc:
99
- log("rollback transaction", "TRANSACTION") do
100
- with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
101
- conn.rollback
102
- end
103
- end
104
- reset_read_uncommitted
41
+ internal_execute("ROLLBACK TRANSACTION", "TRANSACTION", allow_retry: true, materialize_transactions: false)
105
42
  end
106
43
 
107
44
  # https://stackoverflow.com/questions/17574784
@@ -113,47 +50,82 @@ module ActiveRecord
113
50
  HIGH_PRECISION_CURRENT_TIMESTAMP
114
51
  end
115
52
 
53
+ def execute(...) # :nodoc:
54
+ # SQLite3Adapter was refactored to use ActiveRecord::Result internally
55
+ # but for backward compatibility we have to keep returning arrays of hashes here
56
+ super&.to_a
57
+ end
58
+
59
+ def reset_isolation_level # :nodoc:
60
+ internal_execute("PRAGMA read_uncommitted=#{@previous_read_uncommitted}", "TRANSACTION", allow_retry: true, materialize_transactions: false)
61
+ @previous_read_uncommitted = nil
62
+ end
63
+
116
64
  private
117
- def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: false)
118
- log(sql, name, async: async) do |notification_payload|
119
- with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
120
- result = conn.execute(sql)
121
- verified!
122
- notification_payload[:row_count] = result.length
123
- result
124
- end
65
+ def internal_begin_transaction(mode, isolation)
66
+ if isolation
67
+ raise TransactionIsolationError, "SQLite3 only supports the `read_uncommitted` transaction isolation level" if isolation != :read_uncommitted
68
+ raise StandardError, "You need to enable the shared-cache mode in SQLite mode before attempting to change the transaction isolation level" unless shared_cache?
69
+ end
70
+
71
+ internal_execute("BEGIN #{mode} TRANSACTION", "TRANSACTION", allow_retry: true, materialize_transactions: false)
72
+ if isolation
73
+ @previous_read_uncommitted = query_value("PRAGMA read_uncommitted")
74
+ internal_execute("PRAGMA read_uncommitted=ON", "TRANSACTION", allow_retry: true, materialize_transactions: false)
125
75
  end
126
76
  end
127
77
 
128
- def reset_read_uncommitted
129
- read_uncommitted = ActiveSupport::IsolatedExecutionState[:active_record_read_uncommitted]
130
- return unless read_uncommitted
78
+ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
79
+ if batch
80
+ 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
89
+ else
90
+ ActiveRecord::Result.new(stmt.columns, stmt.to_a)
91
+ end
92
+ else
93
+ # Don't cache statements if they are not prepared.
94
+ stmt = raw_connection.prepare(sql)
95
+ begin
96
+ unless binds.nil? || binds.empty?
97
+ stmt.bind_params(type_casted_binds)
98
+ end
99
+ result = if stmt.column_count.zero? # No return
100
+ stmt.step
101
+ ActiveRecord::Result.empty
102
+ else
103
+ ActiveRecord::Result.new(stmt.columns, stmt.to_a)
104
+ end
105
+ ensure
106
+ stmt.close
107
+ end
108
+ end
109
+ @last_affected_rows = raw_connection.changes
110
+ verified!
131
111
 
132
- @raw_connection&.read_uncommitted = read_uncommitted
112
+ notification_payload[:row_count] = result&.length || 0
113
+ result
133
114
  end
134
115
 
135
- def execute_batch(statements, name = nil)
136
- statements = statements.map { |sql| transform_query(sql) }
137
- sql = combine_multi_statements(statements)
138
-
139
- check_if_write_query(sql)
140
- mark_transaction_written_if_write(sql)
116
+ 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.
119
+ result
120
+ end
141
121
 
142
- log(sql, name) do |notification_payload|
143
- with_raw_connection do |conn|
144
- result = conn.execute_batch2(sql)
145
- verified!
146
- notification_payload[:row_count] = result.length
147
- result
148
- end
149
- end
122
+ def affected_rows(result)
123
+ @last_affected_rows
150
124
  end
151
125
 
152
- def build_fixture_statements(fixture_set)
153
- fixture_set.flat_map do |table_name, fixtures|
154
- next if fixtures.empty?
155
- fixtures.map { |fixture| build_fixture_sql([fixture], table_name) }
156
- end.compact
126
+ def execute_batch(statements, name = nil, **kwargs)
127
+ sql = combine_multi_statements(statements)
128
+ raw_execute(sql, name, batch: true, **kwargs)
157
129
  end
158
130
 
159
131
  def build_truncate_statement(table_name)
@@ -163,6 +135,10 @@ module ActiveRecord
163
135
  def returning_column_values(result)
164
136
  result.rows.first
165
137
  end
138
+
139
+ def default_insert_value(column)
140
+ column.default
141
+ end
166
142
  end
167
143
  end
168
144
  end
@@ -5,12 +5,6 @@ module ActiveRecord
5
5
  module SQLite3
6
6
  class SchemaCreation < SchemaCreation # :nodoc:
7
7
  private
8
- def visit_AddForeignKey(o)
9
- super.dup.tap do |sql|
10
- sql << " DEFERRABLE INITIALLY #{o.options[:deferrable].to_s.upcase}" if o.deferrable
11
- end
12
- end
13
-
14
8
  def visit_ForeignKeyDefinition(o)
15
9
  super.dup.tap do |sql|
16
10
  sql << " DEFERRABLE INITIALLY #{o.deferrable.to_s.upcase}" if o.deferrable
@@ -5,6 +5,19 @@ module ActiveRecord
5
5
  module SQLite3
6
6
  class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
7
7
  private
8
+ def virtual_tables(stream)
9
+ virtual_tables = @connection.virtual_tables
10
+ if virtual_tables.any?
11
+ stream.puts
12
+ stream.puts " # Virtual tables defined in this database."
13
+ stream.puts " # Note that virtual tables may not work with other database engines. Be careful if changing database."
14
+ virtual_tables.sort.each do |table_name, options|
15
+ module_name, arguments = options
16
+ stream.puts " create_virtual_table #{table_name.inspect}, #{module_name.inspect}, #{arguments.split(", ").inspect}"
17
+ end
18
+ end
19
+ end
20
+
8
21
  def default_primary_key?(column)
9
22
  schema_type(column) == :integer
10
23
  end
@@ -82,6 +82,10 @@ module ActiveRecord
82
82
  alter_table(from_table, foreign_keys)
83
83
  end
84
84
 
85
+ def virtual_table_exists?(table_name)
86
+ query_values(data_source_sql(table_name, type: "VIRTUAL TABLE"), "SCHEMA").any?
87
+ end
88
+
85
89
  def check_constraints(table_name)
86
90
  table_sql = query_value(<<-SQL, "SCHEMA")
87
91
  SELECT sql
@@ -176,7 +180,8 @@ module ActiveRecord
176
180
  scope = quoted_scope(name, type: type)
177
181
  scope[:type] ||= "'table','view'"
178
182
 
179
- sql = +"SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'"
183
+ sql = +"SELECT name FROM pragma_table_list WHERE schema <> 'temp'"
184
+ sql << " AND name NOT IN ('sqlite_sequence', 'sqlite_schema')"
180
185
  sql << " AND name = #{scope[:name]}" if scope[:name]
181
186
  sql << " AND type IN (#{scope[:type]})"
182
187
  sql
@@ -189,6 +194,8 @@ module ActiveRecord
189
194
  "'table'"
190
195
  when "VIEW"
191
196
  "'view'"
197
+ when "VIRTUAL TABLE"
198
+ "'virtual'"
192
199
  end
193
200
  scope = {}
194
201
  scope[:name] = quote(name) if name
@@ -11,9 +11,12 @@ 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
+ # Suppress the warning that SQLite3 issues when open writable connections are carried across fork()
18
+ SQLite3::ForkSafety.suppress_warnings!
19
+
17
20
  module ActiveRecord
18
21
  module ConnectionAdapters # :nodoc:
19
22
  # = Active Record SQLite3 Adapter
@@ -45,7 +48,7 @@ module ActiveRecord
45
48
  args << "-header" if options[:header]
46
49
  args << File.expand_path(config.database, Rails.respond_to?(:root) ? Rails.root : nil)
47
50
 
48
- find_cmd_and_exec("sqlite3", *args)
51
+ find_cmd_and_exec(ActiveRecord.database_cli[:sqlite], *args)
49
52
  end
50
53
  end
51
54
 
@@ -119,9 +122,14 @@ module ActiveRecord
119
122
  end
120
123
  end
121
124
 
125
+ @last_affected_rows = nil
126
+ @previous_read_uncommitted = nil
122
127
  @config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)
123
- @connection_parameters = @config.merge(database: @config[:database].to_s, results_as_hash: true)
124
- @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
+ )
125
133
  end
126
134
 
127
135
  def database_exists?
@@ -278,6 +286,38 @@ module ActiveRecord
278
286
  exec_query "DROP INDEX #{quote_column_name(index_name)}"
279
287
  end
280
288
 
289
+ VIRTUAL_TABLE_REGEX = /USING\s+(\w+)\s*\((.+)\)/i
290
+
291
+ # Returns a list of defined virtual tables
292
+ def virtual_tables
293
+ query = <<~SQL
294
+ SELECT name, sql FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL %';
295
+ SQL
296
+
297
+ exec_query(query, "SCHEMA").cast_values.each_with_object({}) do |row, memo|
298
+ table_name, sql = row[0], row[1]
299
+ _, module_name, arguments = sql.match(VIRTUAL_TABLE_REGEX).to_a
300
+ memo[table_name] = [module_name, arguments]
301
+ end.to_a
302
+ end
303
+
304
+ # Creates a virtual table
305
+ #
306
+ # Example:
307
+ # create_virtual_table :emails, :fts5, ['sender', 'title',' body']
308
+ def create_virtual_table(table_name, module_name, values)
309
+ exec_query "CREATE VIRTUAL TABLE IF NOT EXISTS #{table_name} USING #{module_name} (#{values.join(", ")})"
310
+ end
311
+
312
+ # Drops a virtual table
313
+ #
314
+ # Although this command ignores +module_name+ and +values+,
315
+ # it can be helpful to provide these in a migration's +change+ method so it can be reverted.
316
+ # In that case, +module_name+, +values+ and +options+ will be used by #create_virtual_table.
317
+ def drop_virtual_table(table_name, module_name, values, **options)
318
+ drop_table(table_name)
319
+ end
320
+
281
321
  # Renames a table.
282
322
  #
283
323
  # Example:
@@ -428,10 +468,6 @@ module ActiveRecord
428
468
  @config.fetch(:flags, 0).anybits?(::SQLite3::Constants::Open::SHAREDCACHE)
429
469
  end
430
470
 
431
- def use_insert_returning?
432
- @use_insert_returning
433
- end
434
-
435
471
  def get_database_version # :nodoc:
436
472
  SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)", "SCHEMA"))
437
473
  end
@@ -661,6 +697,8 @@ module ActiveRecord
661
697
  InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
662
698
  elsif exception.message.match?(/called on a closed database/i)
663
699
  ConnectionNotEstablished.new(exception, connection_pool: @pool)
700
+ elsif exception.is_a?(::SQLite3::BusyException)
701
+ StatementTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
664
702
  else
665
703
  super
666
704
  end
@@ -778,12 +816,15 @@ module ActiveRecord
778
816
  if @config[:timeout] && @config[:retries]
779
817
  raise ArgumentError, "Cannot specify both timeout and retries arguments"
780
818
  elsif @config[:timeout]
781
- @raw_connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout]))
819
+ timeout = self.class.type_cast_config_to_integer(@config[:timeout])
820
+ raise TypeError, "timeout must be integer, not #{timeout}" unless timeout.is_a?(Integer)
821
+ @raw_connection.busy_handler_timeout = timeout
782
822
  elsif @config[:retries]
823
+ ActiveRecord.deprecator.warn(<<~MSG)
824
+ The retries option is deprecated and will be removed in Rails 8.1. Use timeout instead.
825
+ MSG
783
826
  retries = self.class.type_cast_config_to_integer(@config[:retries])
784
- raw_connection.busy_handler do |count|
785
- count <= retries
786
- end
827
+ raw_connection.busy_handler { |count| count <= retries }
787
828
  end
788
829
 
789
830
  super
@@ -4,93 +4,63 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module Trilogy
6
6
  module DatabaseStatements
7
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
8
- sql = transform_query(sql)
9
- check_if_write_query(sql)
10
- mark_transaction_written_if_write(sql)
11
-
12
- result = raw_execute(sql, name, async: async, allow_retry: allow_retry)
13
- ActiveRecord::Result.new(result.fields, result.to_a)
14
- end
15
-
16
7
  def exec_insert(sql, name, binds, pk = nil, sequence_name = nil, returning: nil) # :nodoc:
17
- sql = transform_query(sql)
18
- check_if_write_query(sql)
19
- mark_transaction_written_if_write(sql)
20
-
21
8
  sql, _binds = sql_for_insert(sql, pk, binds, returning)
22
- raw_execute(sql, name)
23
- end
24
-
25
- def exec_delete(sql, name = nil, binds = []) # :nodoc:
26
- sql = transform_query(sql)
27
- check_if_write_query(sql)
28
- mark_transaction_written_if_write(sql)
29
-
30
- result = raw_execute(to_sql(sql, binds), name)
31
- result.affected_rows
9
+ internal_execute(sql, name)
32
10
  end
33
11
 
34
- alias :exec_update :exec_delete # :nodoc:
35
-
36
12
  private
37
- def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
38
- log(sql, name, async: async) do |notification_payload|
39
- with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
40
- sync_timezone_changes(conn)
41
- result = conn.query(sql)
42
- while conn.more_results_exist?
43
- conn.next_result
44
- end
45
- verified!
46
- handle_warnings(sql)
47
- notification_payload[:row_count] = result.count
48
- result
49
- 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
50
17
  end
51
- end
52
18
 
53
- def last_inserted_id(result)
54
- if supports_insert_returning?
55
- super
19
+ # Make sure we carry over any changes to ActiveRecord.default_timezone that have been
20
+ # made since we established the connection
21
+ if default_timezone == :local
22
+ raw_connection.query_flags |= ::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
56
23
  else
57
- result.last_insert_id
24
+ raw_connection.query_flags &= ~::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
58
25
  end
59
- end
60
26
 
61
- def sync_timezone_changes(conn)
62
- # Sync any changes since connection last established.
63
- if default_timezone == :local
64
- conn.query_flags |= ::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
65
- else
66
- conn.query_flags &= ~::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
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)
67
38
  end
68
39
  end
69
40
 
70
- def execute_batch(statements, name = nil)
71
- statements = statements.map { |sql| transform_query(sql) }
72
- combine_multi_statements(statements).each do |statement|
73
- with_raw_connection do |conn|
74
- raw_execute(statement, name)
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) if active?
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
@@ -149,23 +149,6 @@ module ActiveRecord
149
149
  TYPE_MAP.lookup(type).is_a?(Type::String) || TYPE_MAP.lookup(type).is_a?(Type::Text)
150
150
  end
151
151
 
152
- def each_hash(result)
153
- return to_enum(:each_hash, result) unless block_given?
154
-
155
- keys = result.fields.map(&:to_sym)
156
- result.rows.each do |row|
157
- hash = {}
158
- idx = 0
159
- row.each do |value|
160
- hash[keys[idx]] = value
161
- idx += 1
162
- end
163
- yield hash
164
- end
165
-
166
- nil
167
- end
168
-
169
152
  def error_number(exception)
170
153
  exception.error_code if exception.respond_to?(:error_code)
171
154
  end
@@ -31,62 +31,6 @@ module ActiveRecord
31
31
  class_name, path_to_adapter = @adapters[adapter_name.to_s]
32
32
 
33
33
  unless class_name
34
- # To provide better error messages for adapters expecting the pre-7.2 adapter registration API, we attempt
35
- # to load the adapter file from the old location which was required by convention, and then raise an error
36
- # describing how to upgrade the adapter to the new API.
37
- legacy_adapter_path = "active_record/connection_adapters/#{adapter_name}_adapter"
38
- legacy_adapter_connection_method_name = "#{adapter_name}_connection".to_sym
39
-
40
- begin
41
- require legacy_adapter_path
42
- # If we reach here it means we found the found a file that may be the legacy adapter and should raise.
43
- if ActiveRecord::ConnectionHandling.method_defined?(legacy_adapter_connection_method_name)
44
- # If we find the connection method then we care certain it is a legacy adapter.
45
- deprecation_message = <<~MSG.squish
46
- Database configuration specifies '#{adapter_name}' adapter but that adapter has not been registered.
47
- Rails 7.2 has changed the way Active Record database adapters are loaded. The adapter needs to be
48
- updated to register itself rather than being loaded by convention.
49
- Ensure that the adapter in the Gemfile is at the latest version. If it is, then the adapter may need to
50
- be modified.
51
- See:
52
- https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-c-register
53
- MSG
54
-
55
- exception_message = <<~MSG.squish
56
- Database configuration specifies '#{adapter_name}' adapter but that adapter has not been registered.
57
- Ensure that the adapter in the Gemfile is at the latest version. If it is, then the adapter may need to
58
- be modified.
59
- MSG
60
- else
61
- # If we do not find the connection method we are much less certain it is a legacy adapter. Even though the
62
- # file exists in the location defined by convenntion, it does not necessarily mean that file is supposed
63
- # to define the adapter the legacy way. So raise an error that explains both possibilities.
64
- deprecation_message = <<~MSG.squish
65
- Database configuration specifies nonexistent '#{adapter_name}' adapter.
66
- Available adapters are: #{@adapters.keys.sort.join(", ")}.
67
- Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary
68
- adapter gem to your Gemfile if it's not in the list of available adapters.
69
- Rails 7.2 has changed the way Active Record database adapters are loaded. Ensure that the adapter in
70
- the Gemfile is at the latest version. If it is up to date, the adapter may need to be modified.
71
- See:
72
- https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-c-register
73
- MSG
74
-
75
- exception_message = <<~MSG.squish
76
- Database configuration specifies nonexistent '#{adapter_name}' adapter.
77
- Available adapters are: #{@adapters.keys.sort.join(", ")}.
78
- Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary
79
- adapter gem to your Gemfile and that it is at its latest version. If it is up to date, the adapter may
80
- need to be modified.
81
- MSG
82
- end
83
-
84
- ActiveRecord.deprecator.warn(deprecation_message)
85
- raise AdapterNotFound, exception_message
86
- rescue LoadError => error
87
- # The adapter was not found in the legacy location so fall through to the error handling for a missing adapter.
88
- end
89
-
90
34
  raise AdapterNotFound, <<~MSG.squish
91
35
  Database configuration specifies nonexistent '#{adapter_name}' adapter.
92
36
  Available adapters are: #{@adapters.keys.sort.join(", ")}.
@@ -87,6 +87,8 @@ module ActiveRecord
87
87
 
88
88
  connections = []
89
89
 
90
+ @shard_keys = shards.keys
91
+
90
92
  if shards.empty?
91
93
  shards[:default] = database
92
94
  end
@@ -175,6 +177,18 @@ module ActiveRecord
175
177
  connected_to_stack.pop
176
178
  end
177
179
 
180
+ # Passes the block to +connected_to+ for every +shard+ the
181
+ # model is configured to connect to (if any), and returns the
182
+ # results in an array.
183
+ #
184
+ # Optionally, +role+ and/or +prevent_writes+ can be passed which
185
+ # will be forwarded to each +connected_to+ call.
186
+ def connected_to_all_shards(role: nil, prevent_writes: false, &blk)
187
+ shard_keys.map do |shard|
188
+ connected_to(shard: shard, role: role, prevent_writes: prevent_writes, &blk)
189
+ end
190
+ end
191
+
178
192
  # Use a specified connection.
179
193
  #
180
194
  # This method is useful for ensuring that a specific connection is
@@ -359,6 +373,14 @@ module ActiveRecord
359
373
  connection_pool.schema_cache.clear!
360
374
  end
361
375
 
376
+ def shard_keys
377
+ connection_class_for_self.instance_variable_get(:@shard_keys) || []
378
+ end
379
+
380
+ def sharded?
381
+ shard_keys.any?
382
+ end
383
+
362
384
  private
363
385
  def resolve_config_for_connection(config_or_env)
364
386
  raise "Anonymous class is not allowed." unless name