activerecord 7.2.0 → 8.0.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +189 -745
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/associations/association.rb +25 -5
  5. data/lib/active_record/associations/builder/association.rb +7 -6
  6. data/lib/active_record/associations/collection_association.rb +10 -8
  7. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  8. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  9. data/lib/active_record/associations/join_dependency/join_association.rb +3 -2
  10. data/lib/active_record/associations/join_dependency.rb +5 -5
  11. data/lib/active_record/associations/preloader/association.rb +2 -2
  12. data/lib/active_record/associations/singular_association.rb +8 -3
  13. data/lib/active_record/associations.rb +34 -4
  14. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  15. data/lib/active_record/attribute_assignment.rb +9 -1
  16. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -0
  17. data/lib/active_record/attributes.rb +6 -5
  18. data/lib/active_record/autosave_association.rb +69 -27
  19. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
  20. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  21. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +23 -44
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  24. data/lib/active_record/connection_adapters/abstract/query_cache.rb +53 -18
  25. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  26. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  27. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +26 -5
  28. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  29. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -25
  30. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +20 -38
  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 +44 -46
  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/cidr.rb +1 -1
  38. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  39. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -1
  40. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +50 -6
  41. data/lib/active_record/connection_adapters/postgresql_adapter.rb +38 -90
  42. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  43. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
  44. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  45. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  46. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -1
  47. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -12
  48. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  49. data/lib/active_record/connection_adapters/trilogy_adapter.rb +0 -17
  50. data/lib/active_record/connection_handling.rb +22 -0
  51. data/lib/active_record/core.rb +16 -9
  52. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -1
  53. data/lib/active_record/encryption/config.rb +3 -1
  54. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  55. data/lib/active_record/encryption/encrypted_attribute_type.rb +12 -3
  56. data/lib/active_record/encryption/encryptor.rb +16 -9
  57. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  58. data/lib/active_record/encryption/key_provider.rb +1 -1
  59. data/lib/active_record/encryption/scheme.rb +8 -1
  60. data/lib/active_record/encryption.rb +2 -0
  61. data/lib/active_record/enum.rb +8 -0
  62. data/lib/active_record/errors.rb +13 -5
  63. data/lib/active_record/fixtures.rb +0 -1
  64. data/lib/active_record/future_result.rb +14 -10
  65. data/lib/active_record/gem_version.rb +3 -3
  66. data/lib/active_record/insert_all.rb +1 -1
  67. data/lib/active_record/migration/command_recorder.rb +22 -5
  68. data/lib/active_record/migration/compatibility.rb +5 -2
  69. data/lib/active_record/migration.rb +35 -33
  70. data/lib/active_record/model_schema.rb +6 -3
  71. data/lib/active_record/nested_attributes.rb +11 -2
  72. data/lib/active_record/persistence.rb +128 -130
  73. data/lib/active_record/query_logs.rb +97 -39
  74. data/lib/active_record/query_logs_formatter.rb +17 -28
  75. data/lib/active_record/querying.rb +6 -6
  76. data/lib/active_record/railtie.rb +8 -14
  77. data/lib/active_record/reflection.rb +19 -10
  78. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  79. data/lib/active_record/relation/batches.rb +135 -75
  80. data/lib/active_record/relation/calculations.rb +24 -19
  81. data/lib/active_record/relation/delegation.rb +25 -14
  82. data/lib/active_record/relation/finder_methods.rb +18 -18
  83. data/lib/active_record/relation/merger.rb +8 -8
  84. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  85. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  86. data/lib/active_record/relation/predicate_builder.rb +6 -1
  87. data/lib/active_record/relation/query_methods.rb +58 -37
  88. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  89. data/lib/active_record/relation/spawn_methods.rb +1 -1
  90. data/lib/active_record/relation.rb +72 -61
  91. data/lib/active_record/result.rb +68 -7
  92. data/lib/active_record/sanitization.rb +7 -6
  93. data/lib/active_record/schema_dumper.rb +5 -0
  94. data/lib/active_record/schema_migration.rb +2 -1
  95. data/lib/active_record/scoping/named.rb +6 -2
  96. data/lib/active_record/statement_cache.rb +12 -12
  97. data/lib/active_record/store.rb +7 -3
  98. data/lib/active_record/tasks/database_tasks.rb +36 -16
  99. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  100. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  101. data/lib/active_record/test_fixtures.rb +12 -0
  102. data/lib/active_record/token_for.rb +1 -1
  103. data/lib/active_record/validations/uniqueness.rb +9 -8
  104. data/lib/active_record.rb +15 -0
  105. data/lib/arel/collectors/bind.rb +1 -1
  106. metadata +14 -14
@@ -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", 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
@@ -687,6 +725,8 @@ module ActiveRecord
687
725
  end
688
726
 
689
727
  basic_structure.map do |column|
728
+ column = column.to_h
729
+
690
730
  column_name = column["name"]
691
731
 
692
732
  if collation_hash.has_key? column_name
@@ -778,12 +818,15 @@ module ActiveRecord
778
818
  if @config[:timeout] && @config[:retries]
779
819
  raise ArgumentError, "Cannot specify both timeout and retries arguments"
780
820
  elsif @config[:timeout]
781
- @raw_connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout]))
821
+ timeout = self.class.type_cast_config_to_integer(@config[:timeout])
822
+ raise TypeError, "timeout must be integer, not #{timeout}" unless timeout.is_a?(Integer)
823
+ @raw_connection.busy_handler_timeout = timeout
782
824
  elsif @config[:retries]
825
+ ActiveRecord.deprecator.warn(<<~MSG)
826
+ The retries option is deprecated and will be removed in Rails 8.1. Use timeout instead.
827
+ MSG
783
828
  retries = self.class.type_cast_config_to_integer(@config[:retries])
784
- raw_connection.busy_handler do |count|
785
- count <= retries
786
- end
829
+ raw_connection.busy_handler { |count| count <= retries }
787
830
  end
788
831
 
789
832
  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
@@ -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