activerecord 7.1.5 → 7.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (183) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +515 -2440
  3. data/README.rdoc +15 -15
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +1 -1
  6. data/lib/active_record/associations/alias_tracker.rb +25 -19
  7. data/lib/active_record/associations/association.rb +9 -8
  8. data/lib/active_record/associations/belongs_to_association.rb +14 -7
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/belongs_to.rb +1 -0
  11. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  12. data/lib/active_record/associations/builder/has_many.rb +3 -4
  13. data/lib/active_record/associations/builder/has_one.rb +3 -4
  14. data/lib/active_record/associations/collection_association.rb +6 -4
  15. data/lib/active_record/associations/collection_proxy.rb +14 -1
  16. data/lib/active_record/associations/has_many_association.rb +1 -1
  17. data/lib/active_record/associations/join_dependency/join_association.rb +29 -28
  18. data/lib/active_record/associations/join_dependency.rb +5 -5
  19. data/lib/active_record/associations/nested_error.rb +47 -0
  20. data/lib/active_record/associations/preloader/association.rb +2 -1
  21. data/lib/active_record/associations/preloader/branch.rb +7 -1
  22. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  23. data/lib/active_record/associations/singular_association.rb +6 -0
  24. data/lib/active_record/associations/through_association.rb +1 -1
  25. data/lib/active_record/associations.rb +33 -16
  26. data/lib/active_record/attribute_assignment.rb +1 -11
  27. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  28. data/lib/active_record/attribute_methods/dirty.rb +1 -1
  29. data/lib/active_record/attribute_methods/primary_key.rb +23 -55
  30. data/lib/active_record/attribute_methods/read.rb +4 -16
  31. data/lib/active_record/attribute_methods/serialization.rb +4 -24
  32. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -10
  33. data/lib/active_record/attribute_methods/write.rb +3 -3
  34. data/lib/active_record/attribute_methods.rb +60 -71
  35. data/lib/active_record/attributes.rb +55 -42
  36. data/lib/active_record/autosave_association.rb +13 -32
  37. data/lib/active_record/base.rb +2 -3
  38. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +24 -107
  39. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +1 -0
  40. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +248 -65
  41. data/lib/active_record/connection_adapters/abstract/database_statements.rb +34 -17
  42. data/lib/active_record/connection_adapters/abstract/query_cache.rb +159 -74
  43. data/lib/active_record/connection_adapters/abstract/quoting.rb +65 -91
  44. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  45. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +14 -5
  46. data/lib/active_record/connection_adapters/abstract/transaction.rb +60 -57
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +18 -46
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +32 -6
  49. data/lib/active_record/connection_adapters/mysql/database_statements.rb +9 -1
  50. data/lib/active_record/connection_adapters/mysql/quoting.rb +43 -48
  51. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +7 -1
  52. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +11 -5
  53. data/lib/active_record/connection_adapters/mysql2_adapter.rb +5 -23
  54. data/lib/active_record/connection_adapters/pool_config.rb +7 -6
  55. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +27 -4
  56. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +1 -1
  57. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  58. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  59. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  60. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -0
  61. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +15 -13
  62. data/lib/active_record/connection_adapters/postgresql_adapter.rb +26 -21
  63. data/lib/active_record/connection_adapters/schema_cache.rb +123 -128
  64. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  65. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +10 -6
  66. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +44 -46
  67. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  68. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  69. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  70. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +25 -2
  71. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +107 -75
  72. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +12 -6
  73. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -48
  74. data/lib/active_record/connection_adapters.rb +121 -0
  75. data/lib/active_record/connection_handling.rb +56 -41
  76. data/lib/active_record/core.rb +53 -37
  77. data/lib/active_record/counter_cache.rb +18 -9
  78. data/lib/active_record/database_configurations/connection_url_resolver.rb +8 -3
  79. data/lib/active_record/database_configurations/database_config.rb +15 -4
  80. data/lib/active_record/database_configurations/hash_config.rb +38 -34
  81. data/lib/active_record/database_configurations/url_config.rb +20 -1
  82. data/lib/active_record/database_configurations.rb +1 -1
  83. data/lib/active_record/delegated_type.rb +24 -0
  84. data/lib/active_record/dynamic_matchers.rb +2 -2
  85. data/lib/active_record/encryption/encryptable_record.rb +2 -2
  86. data/lib/active_record/encryption/encrypted_attribute_type.rb +22 -2
  87. data/lib/active_record/encryption/encryptor.rb +17 -2
  88. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  89. data/lib/active_record/encryption/message_serializer.rb +4 -0
  90. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  91. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  92. data/lib/active_record/encryption.rb +0 -2
  93. data/lib/active_record/enum.rb +10 -1
  94. data/lib/active_record/errors.rb +16 -11
  95. data/lib/active_record/explain.rb +13 -24
  96. data/lib/active_record/fixtures.rb +37 -31
  97. data/lib/active_record/future_result.rb +8 -4
  98. data/lib/active_record/gem_version.rb +3 -3
  99. data/lib/active_record/inheritance.rb +4 -2
  100. data/lib/active_record/insert_all.rb +18 -15
  101. data/lib/active_record/integration.rb +4 -1
  102. data/lib/active_record/internal_metadata.rb +48 -34
  103. data/lib/active_record/locking/optimistic.rb +7 -6
  104. data/lib/active_record/log_subscriber.rb +0 -21
  105. data/lib/active_record/marshalling.rb +1 -4
  106. data/lib/active_record/message_pack.rb +1 -1
  107. data/lib/active_record/migration/command_recorder.rb +2 -3
  108. data/lib/active_record/migration/compatibility.rb +5 -3
  109. data/lib/active_record/migration/default_strategy.rb +4 -5
  110. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  111. data/lib/active_record/migration.rb +85 -76
  112. data/lib/active_record/model_schema.rb +28 -68
  113. data/lib/active_record/nested_attributes.rb +13 -16
  114. data/lib/active_record/normalization.rb +3 -7
  115. data/lib/active_record/persistence.rb +30 -352
  116. data/lib/active_record/query_cache.rb +18 -6
  117. data/lib/active_record/query_logs.rb +15 -0
  118. data/lib/active_record/querying.rb +21 -9
  119. data/lib/active_record/railtie.rb +50 -62
  120. data/lib/active_record/railties/controller_runtime.rb +13 -4
  121. data/lib/active_record/railties/databases.rake +41 -44
  122. data/lib/active_record/reflection.rb +90 -35
  123. data/lib/active_record/relation/batches/batch_enumerator.rb +15 -2
  124. data/lib/active_record/relation/batches.rb +3 -3
  125. data/lib/active_record/relation/calculations.rb +94 -61
  126. data/lib/active_record/relation/delegation.rb +8 -11
  127. data/lib/active_record/relation/finder_methods.rb +16 -2
  128. data/lib/active_record/relation/merger.rb +4 -6
  129. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  130. data/lib/active_record/relation/predicate_builder.rb +3 -3
  131. data/lib/active_record/relation/query_methods.rb +196 -57
  132. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  133. data/lib/active_record/relation/spawn_methods.rb +2 -18
  134. data/lib/active_record/relation/where_clause.rb +7 -19
  135. data/lib/active_record/relation.rb +496 -72
  136. data/lib/active_record/result.rb +31 -44
  137. data/lib/active_record/runtime_registry.rb +39 -0
  138. data/lib/active_record/sanitization.rb +24 -19
  139. data/lib/active_record/schema.rb +8 -6
  140. data/lib/active_record/schema_dumper.rb +19 -9
  141. data/lib/active_record/schema_migration.rb +30 -14
  142. data/lib/active_record/signed_id.rb +11 -1
  143. data/lib/active_record/statement_cache.rb +7 -7
  144. data/lib/active_record/table_metadata.rb +1 -10
  145. data/lib/active_record/tasks/database_tasks.rb +76 -70
  146. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  147. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  148. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -1
  149. data/lib/active_record/test_fixtures.rb +81 -91
  150. data/lib/active_record/testing/query_assertions.rb +121 -0
  151. data/lib/active_record/timestamp.rb +1 -1
  152. data/lib/active_record/token_for.rb +22 -12
  153. data/lib/active_record/touch_later.rb +1 -1
  154. data/lib/active_record/transaction.rb +68 -0
  155. data/lib/active_record/transactions.rb +43 -14
  156. data/lib/active_record/translation.rb +0 -2
  157. data/lib/active_record/type/serialized.rb +1 -3
  158. data/lib/active_record/type_caster/connection.rb +4 -4
  159. data/lib/active_record/validations/associated.rb +9 -3
  160. data/lib/active_record/validations/uniqueness.rb +14 -10
  161. data/lib/active_record/validations.rb +4 -1
  162. data/lib/active_record.rb +149 -40
  163. data/lib/arel/alias_predication.rb +1 -1
  164. data/lib/arel/collectors/bind.rb +2 -0
  165. data/lib/arel/collectors/composite.rb +7 -0
  166. data/lib/arel/collectors/sql_string.rb +1 -1
  167. data/lib/arel/collectors/substitute_binds.rb +1 -1
  168. data/lib/arel/nodes/binary.rb +0 -6
  169. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  170. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  171. data/lib/arel/nodes/node.rb +4 -3
  172. data/lib/arel/nodes/sql_literal.rb +7 -0
  173. data/lib/arel/nodes.rb +2 -2
  174. data/lib/arel/predications.rb +1 -1
  175. data/lib/arel/select_manager.rb +1 -1
  176. data/lib/arel/tree_manager.rb +3 -2
  177. data/lib/arel/update_manager.rb +2 -1
  178. data/lib/arel/visitors/dot.rb +1 -0
  179. data/lib/arel/visitors/mysql.rb +9 -4
  180. data/lib/arel/visitors/postgresql.rb +1 -12
  181. data/lib/arel/visitors/to_sql.rb +29 -16
  182. data/lib/arel.rb +7 -3
  183. metadata +20 -15
@@ -5,6 +5,18 @@ 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
+ def visit_ForeignKeyDefinition(o)
15
+ super.dup.tap do |sql|
16
+ sql << " DEFERRABLE INITIALLY #{o.deferrable.to_s.upcase}" if o.deferrable
17
+ end
18
+ end
19
+
8
20
  def supports_index_using?
9
21
  false
10
22
  end
@@ -13,6 +25,16 @@ module ActiveRecord
13
25
  if options[:collation]
14
26
  sql << " COLLATE \"#{options[:collation]}\""
15
27
  end
28
+
29
+ if as = options[:as]
30
+ sql << " GENERATED ALWAYS AS (#{as})"
31
+
32
+ if options[:stored]
33
+ sql << " STORED"
34
+ else
35
+ sql << " VIRTUAL"
36
+ end
37
+ end
16
38
  super
17
39
  end
18
40
  end
@@ -16,10 +16,23 @@ module ActiveRecord
16
16
  end
17
17
  alias :belongs_to :references
18
18
 
19
+ def new_column_definition(name, type, **options) # :nodoc:
20
+ case type
21
+ when :virtual
22
+ type = options[:type]
23
+ end
24
+
25
+ super
26
+ end
27
+
19
28
  private
20
29
  def integer_like_primary_key_type(type, options)
21
30
  :primary_key
22
31
  end
32
+
33
+ def valid_column_definition_options
34
+ super + [:as, :type, :stored]
35
+ end
23
36
  end
24
37
  end
25
38
  end
@@ -12,6 +12,22 @@ module ActiveRecord
12
12
  def explicit_primary_key_default?(column)
13
13
  column.bigint?
14
14
  end
15
+
16
+ def prepare_column_options(column)
17
+ spec = super
18
+
19
+ if @connection.supports_virtual_columns? && column.virtual?
20
+ spec[:as] = extract_expression_for_virtual_column(column)
21
+ spec[:stored] = column.virtual_stored?
22
+ spec = { type: schema_type(column).inspect }.merge!(spec)
23
+ end
24
+
25
+ spec
26
+ end
27
+
28
+ def extract_expression_for_virtual_column(column)
29
+ column.default_function.inspect
30
+ end
15
31
  end
16
32
  end
17
33
  end
@@ -53,6 +53,8 @@ module ActiveRecord
53
53
  end
54
54
 
55
55
  def add_foreign_key(from_table, to_table, **options)
56
+ assert_valid_deferrable(options[:deferrable])
57
+
56
58
  alter_table(from_table) do |definition|
57
59
  to_table = strip_table_name_prefix_and_suffix(to_table)
58
60
  definition.foreign_key(to_table, **options)
@@ -137,7 +139,14 @@ module ActiveRecord
137
139
 
138
140
  type_metadata = fetch_type_metadata(field["type"])
139
141
  default_value = extract_value_from_default(default)
140
- default_function = extract_default_function(default_value, default)
142
+ generated_type = extract_generated_type(field)
143
+
144
+ if generated_type.present?
145
+ default_function = default
146
+ else
147
+ default_function = extract_default_function(default_value, default)
148
+ end
149
+
141
150
  rowid = is_column_the_rowid?(field, definitions)
142
151
 
143
152
  Column.new(
@@ -148,7 +157,8 @@ module ActiveRecord
148
157
  default_function,
149
158
  collation: field["collation"],
150
159
  auto_increment: field["auto_increment"],
151
- rowid: rowid
160
+ rowid: rowid,
161
+ generated_type: generated_type
152
162
  )
153
163
  end
154
164
 
@@ -185,6 +195,19 @@ module ActiveRecord
185
195
  scope[:type] = type if type
186
196
  scope
187
197
  end
198
+
199
+ def assert_valid_deferrable(deferrable)
200
+ return if !deferrable || %i(immediate deferred).include?(deferrable)
201
+
202
+ raise ArgumentError, "deferrable must be `:immediate` or `:deferred`, got: `#{deferrable.inspect}`"
203
+ end
204
+
205
+ def extract_generated_type(field)
206
+ case field["hidden"]
207
+ when 2 then :virtual
208
+ when 3 then :stored
209
+ end
210
+ end
188
211
  end
189
212
  end
190
213
  end
@@ -15,16 +15,6 @@ gem "sqlite3", ">= 1.4"
15
15
  require "sqlite3"
16
16
 
17
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
27
-
28
18
  module ConnectionAdapters # :nodoc:
29
19
  # = Active Record SQLite3 Adapter
30
20
  #
@@ -88,6 +78,15 @@ module ActiveRecord
88
78
  json: { name: "json" },
89
79
  }
90
80
 
81
+ DEFAULT_PRAGMAS = {
82
+ "foreign_keys" => true,
83
+ "journal_mode" => :wal,
84
+ "synchronous" => :normal,
85
+ "mmap_size" => 134217728, # 128 megabytes
86
+ "journal_size_limit" => 67108864, # 64 megabytes
87
+ "cache_size" => 2000
88
+ }
89
+
91
90
  class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
92
91
  alias reset clear
93
92
 
@@ -113,13 +112,9 @@ module ActiveRecord
113
112
  dirname = File.dirname(@config[:database])
114
113
  unless File.directory?(dirname)
115
114
  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
115
+ FileUtils.mkdir_p(dirname)
116
+ rescue SystemCallError
117
+ raise ActiveRecord::NoDatabaseError.new(connection_pool: @pool)
123
118
  end
124
119
  end
125
120
  end
@@ -196,14 +191,16 @@ module ActiveRecord
196
191
  !@memory_database
197
192
  end
198
193
 
199
- def active?
200
- @raw_connection && !@raw_connection.closed?
194
+ def supports_virtual_columns?
195
+ database_version >= "3.31.0"
201
196
  end
202
197
 
203
- def return_value_after_insert?(column) # :nodoc:
204
- column.auto_populated?
198
+ def connected?
199
+ !(@raw_connection.nil? || @raw_connection.closed?)
205
200
  end
206
201
 
202
+ alias_method :active?, :connected?
203
+
207
204
  alias :reset! :reconnect!
208
205
 
209
206
  # Disconnects from the database if already connected. Otherwise, this
@@ -236,6 +233,10 @@ module ActiveRecord
236
233
  true
237
234
  end
238
235
 
236
+ def supports_deferrable_constraints?
237
+ true
238
+ end
239
+
239
240
  # REFERENTIAL INTEGRITY ====================================
240
241
 
241
242
  def disable_referential_integrity # :nodoc:
@@ -258,7 +259,7 @@ module ActiveRecord
258
259
 
259
260
  unless result.blank?
260
261
  tables = result.map { |row| row["table"] }
261
- raise ActiveRecord::StatementInvalid.new("Foreign key violations found: #{tables.join(", ")}", sql: sql)
262
+ raise ActiveRecord::StatementInvalid.new("Foreign key violations found: #{tables.join(", ")}", sql: sql, connection_pool: @pool)
262
263
  end
263
264
  end
264
265
 
@@ -290,6 +291,7 @@ module ActiveRecord
290
291
  end
291
292
 
292
293
  def add_column(table_name, column_name, type, **options) # :nodoc:
294
+ type = type.to_sym
293
295
  if invalid_alter_table_type?(type, options)
294
296
  alter_table(table_name) do |definition|
295
297
  definition.column(column_name, type, **options)
@@ -365,15 +367,31 @@ module ActiveRecord
365
367
  end
366
368
  alias :add_belongs_to :add_reference
367
369
 
370
+ FK_REGEX = /.*FOREIGN KEY\s+\("(\w+)"\)\s+REFERENCES\s+"(\w+)"\s+\("(\w+)"\)/
371
+ DEFERRABLE_REGEX = /DEFERRABLE INITIALLY (\w+)/
368
372
  def foreign_keys(table_name)
369
373
  # SQLite returns 1 row for each column of composite foreign keys.
370
374
  fk_info = internal_exec_query("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA")
375
+ # Deferred or immediate foreign keys can only be seen in the CREATE TABLE sql
376
+ fk_defs = table_structure_sql(table_name)
377
+ .select do |column_string|
378
+ column_string.start_with?("CONSTRAINT") &&
379
+ column_string.include?("FOREIGN KEY")
380
+ end
381
+ .to_h do |fk_string|
382
+ _, from, table, to = fk_string.match(FK_REGEX).to_a
383
+ _, mode = fk_string.match(DEFERRABLE_REGEX).to_a
384
+ deferred = mode&.downcase&.to_sym || false
385
+ [[table, from, to], deferred]
386
+ end
387
+
371
388
  grouped_fk = fk_info.group_by { |row| row["id"] }.values.each { |group| group.sort_by! { |row| row["seq"] } }
372
389
  grouped_fk.map do |group|
373
390
  row = group.first
374
391
  options = {
375
392
  on_delete: extract_foreign_key_action(row["on_delete"]),
376
- on_update: extract_foreign_key_action(row["on_update"])
393
+ on_update: extract_foreign_key_action(row["on_update"]),
394
+ deferrable: fk_defs[[row["table"], row["from"], row["to"]]]
377
395
  }
378
396
 
379
397
  if group.one?
@@ -454,8 +472,12 @@ module ActiveRecord
454
472
  end
455
473
 
456
474
  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?
475
+ structure = if supports_virtual_columns?
476
+ internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
477
+ else
478
+ internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
479
+ end
480
+ raise ActiveRecord::StatementInvalid.new("Could not find table '#{table_name}'", connection_pool: @pool) if structure.empty?
459
481
  table_structure_with_collation(table_name, structure)
460
482
  end
461
483
  alias column_definitions table_structure
@@ -494,8 +516,9 @@ module ActiveRecord
494
516
  # See: https://www.sqlite.org/lang_altertable.html
495
517
  # SQLite has an additional restriction on the ALTER TABLE statement
496
518
  def invalid_alter_table_type?(type, options)
497
- type.to_sym == :primary_key || options[:primary_key] ||
498
- options[:null] == false && options[:default].nil?
519
+ type == :primary_key || options[:primary_key] ||
520
+ options[:null] == false && options[:default].nil? ||
521
+ (type == :virtual && options[:stored])
499
522
  end
500
523
 
501
524
  def alter_table(
@@ -551,12 +574,6 @@ module ActiveRecord
551
574
  options[:rename][column.name.to_sym] ||
552
575
  column.name) : column.name
553
576
 
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
577
  column_options = {
561
578
  limit: column.limit,
562
579
  precision: column.precision,
@@ -566,19 +583,31 @@ module ActiveRecord
566
583
  primary_key: column_name == from_primary_key
567
584
  }
568
585
 
569
- unless column.auto_increment?
570
- column_options[:default] = default
586
+ if column.virtual?
587
+ column_options[:as] = column.default_function
588
+ column_options[:stored] = column.virtual_stored?
589
+ column_options[:type] = column.type
590
+ elsif column.has_default?
591
+ type = lookup_cast_type_from_column(column)
592
+ default = type.deserialize(column.default)
593
+ default = -> { column.default_function } if default.nil?
594
+
595
+ unless column.auto_increment?
596
+ column_options[:default] = default
597
+ end
571
598
  end
572
599
 
573
- column_type = column.bigint? ? :bigint : column.type
600
+ column_type = column.virtual? ? :virtual : (column.bigint? ? :bigint : column.type)
574
601
  @definition.column(column_name, column_type, **column_options)
575
602
  end
576
603
 
577
604
  yield @definition if block_given?
578
605
  end
579
606
  copy_table_indexes(from, to, options[:rename] || {})
607
+
608
+ columns_to_copy = @definition.columns.reject { |col| col.options.key?(:as) }.map(&:name)
580
609
  copy_table_contents(from, to,
581
- @definition.columns.map(&:name),
610
+ columns_to_copy,
582
611
  options[:rename] || {})
583
612
  end
584
613
 
@@ -643,32 +672,22 @@ module ActiveRecord
643
672
 
644
673
  COLLATE_REGEX = /.*"(\w+)".*collate\s+"(\w+)".*/i
645
674
  PRIMARY_KEY_AUTOINCREMENT_REGEX = /.*"(\w+)".+PRIMARY KEY AUTOINCREMENT/i
675
+ GENERATED_ALWAYS_AS_REGEX = /.*"(\w+)".+GENERATED ALWAYS AS \((.+)\) (?:STORED|VIRTUAL)/i
646
676
 
647
677
  def table_structure_with_collation(table_name, basic_structure)
648
678
  collation_hash = {}
649
679
  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")
680
+ generated_columns = {}
661
681
 
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
682
+ column_strings = table_structure_sql(table_name)
666
683
 
667
- columns_string.split(",").each do |column_string|
684
+ if column_strings.any?
685
+ column_strings.each do |column_string|
668
686
  # This regex will match the column name and collation type and will save
669
687
  # the value in $1 and $2 respectively.
670
688
  collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string
671
689
  auto_increments[$1] = true if PRIMARY_KEY_AUTOINCREMENT_REGEX =~ column_string
690
+ generated_columns[$1] = $2 if GENERATED_ALWAYS_AS_REGEX =~ column_string
672
691
  end
673
692
 
674
693
  basic_structure.map do |column|
@@ -682,6 +701,10 @@ module ActiveRecord
682
701
  column["auto_increment"] = true
683
702
  end
684
703
 
704
+ if generated_columns.has_key?(column_name)
705
+ column["dflt_value"] = generated_columns[column_name]
706
+ end
707
+
685
708
  column
686
709
  end
687
710
  else
@@ -689,6 +712,28 @@ module ActiveRecord
689
712
  end
690
713
  end
691
714
 
715
+ def table_structure_sql(table_name)
716
+ sql = <<~SQL
717
+ SELECT sql FROM
718
+ (SELECT * FROM sqlite_master UNION ALL
719
+ SELECT * FROM sqlite_temp_master)
720
+ WHERE type = 'table' AND name = #{quote(table_name)}
721
+ SQL
722
+
723
+ # Result will have following sample string
724
+ # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
725
+ # "password_digest" varchar COLLATE "NOCASE");
726
+ result = query_value(sql, "SCHEMA")
727
+
728
+ return [] unless result
729
+
730
+ # Splitting with left parentheses and discarding the first part will return all
731
+ # columns separated with comma(,).
732
+ columns_string = result.split("(", 2).last
733
+
734
+ columns_string.split(",").map(&:strip)
735
+ end
736
+
692
737
  def arel_visitor
693
738
  Arel::Visitors::SQLite.new(self)
694
739
  end
@@ -723,29 +768,16 @@ module ActiveRecord
723
768
  end
724
769
  end
725
770
 
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")
771
+ super
772
+
773
+ pragmas = @config.fetch(:pragmas, {}).stringify_keys
774
+ DEFAULT_PRAGMAS.merge(pragmas).each do |pragma, value|
775
+ if ::SQLite3::Pragmas.method_defined?("#{pragma}=")
776
+ @raw_connection.public_send("#{pragma}=", value)
777
+ else
778
+ warn "Unknown SQLite pragma: #{pragma}"
779
+ end
742
780
  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
781
  end
750
782
  end
751
783
  ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)
@@ -12,12 +12,12 @@ module ActiveRecord
12
12
  result
13
13
  end
14
14
 
15
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) # :nodoc:
15
+ def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
16
16
  sql = transform_query(sql)
17
17
  check_if_write_query(sql)
18
18
  mark_transaction_written_if_write(sql)
19
19
 
20
- result = raw_execute(sql, name, async: async)
20
+ result = raw_execute(sql, name, async: async, allow_retry: allow_retry)
21
21
  ActiveRecord::Result.new(result.fields, result.to_a)
22
22
  end
23
23
 
@@ -26,7 +26,8 @@ module ActiveRecord
26
26
  check_if_write_query(sql)
27
27
  mark_transaction_written_if_write(sql)
28
28
 
29
- raw_execute(to_sql(sql, binds), name)
29
+ sql, _binds = sql_for_insert(sql, pk, binds, returning)
30
+ raw_execute(sql, name)
30
31
  end
31
32
 
32
33
  def exec_delete(sql, name = nil, binds = []) # :nodoc:
@@ -42,19 +43,24 @@ module ActiveRecord
42
43
 
43
44
  private
44
45
  def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
45
- log(sql, name, async: async) do
46
+ log(sql, name, async: async) do |notification_payload|
46
47
  with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
47
48
  sync_timezone_changes(conn)
48
49
  result = conn.query(sql)
49
50
  verified!
50
51
  handle_warnings(sql)
52
+ notification_payload[:row_count] = result.count
51
53
  result
52
54
  end
53
55
  end
54
56
  end
55
57
 
56
58
  def last_inserted_id(result)
57
- result.last_insert_id
59
+ if supports_insert_returning?
60
+ super
61
+ else
62
+ result.last_insert_id
63
+ end
58
64
  end
59
65
 
60
66
  def sync_timezone_changes(conn)
@@ -90,7 +96,7 @@ module ActiveRecord
90
96
 
91
97
  yield
92
98
  ensure
93
- conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF)
99
+ conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF) if active?
94
100
  end
95
101
  end
96
102
  end
@@ -2,44 +2,17 @@
2
2
 
3
3
  require "active_record/connection_adapters/abstract_mysql_adapter"
4
4
 
5
- gem "trilogy", "~> 2.4"
5
+ gem "trilogy", "~> 2.7"
6
6
  require "trilogy"
7
7
 
8
8
  require "active_record/connection_adapters/trilogy/database_statements"
9
9
 
10
10
  module ActiveRecord
11
- module ConnectionHandling # :nodoc:
12
- def trilogy_adapter_class
13
- ConnectionAdapters::TrilogyAdapter
14
- end
15
-
16
- # Establishes a connection to the database that's used by all Active Record objects.
17
- def trilogy_connection(config)
18
- configuration = config.dup
19
-
20
- # Set FOUND_ROWS capability on the connection so UPDATE queries returns number of rows
21
- # matched rather than number of rows updated.
22
- configuration[:found_rows] = true
23
-
24
- options = [
25
- configuration[:host],
26
- configuration[:port],
27
- configuration[:database],
28
- configuration[:username],
29
- configuration[:password],
30
- configuration[:socket],
31
- 0
32
- ]
33
-
34
- trilogy_adapter_class.new nil, logger, options, configuration
35
- end
36
- end
37
11
  module ConnectionAdapters
38
12
  class TrilogyAdapter < AbstractMysqlAdapter
39
13
  ER_BAD_DB_ERROR = 1049
40
14
  ER_DBACCESS_DENIED_ERROR = 1044
41
15
  ER_ACCESS_DENIED_ERROR = 1045
42
- ER_SERVER_SHUTDOWN = 1053
43
16
 
44
17
  ADAPTER_NAME = "Trilogy"
45
18
 
@@ -99,16 +72,22 @@ module ActiveRecord
99
72
  end
100
73
  end
101
74
 
102
- def initialize(...)
103
- super
75
+ def initialize(config, *)
76
+ config = config.dup
77
+
78
+ # Trilogy ignores `socket` if `host is set. We want the opposite to allow
79
+ # configuring UNIX domain sockets via `DATABASE_URL`.
80
+ config.delete(:host) if config[:socket]
104
81
 
105
- if @config[:prepared_statements]
82
+ # Set FOUND_ROWS capability on the connection so UPDATE queries returns number of rows
83
+ # matched rather than number of rows updated.
84
+ config[:found_rows] = true
85
+
86
+ if config[:prepared_statements]
106
87
  raise ArgumentError, "Trilogy currently doesn't support prepared statements. Remove `prepared_statements: true` from your database configuration."
107
88
  end
108
89
 
109
- # Trilogy ignore `socket` if `host is set. We want the opposite to allow
110
- # configuring UNIX domain sockets via `DATABASE_URL`.
111
- @config.delete(:host) if @config[:socket]
90
+ super
112
91
  end
113
92
 
114
93
  TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
@@ -137,14 +116,12 @@ module ActiveRecord
137
116
  true
138
117
  end
139
118
 
140
- def quote_string(string)
141
- with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
142
- conn.escape(string)
143
- end
119
+ def connected?
120
+ !(@raw_connection.nil? || @raw_connection.closed?)
144
121
  end
145
122
 
146
123
  def active?
147
- !(@raw_connection.nil? || @raw_connection.closed?) && @lock.synchronize { @raw_connection&.ping } || false
124
+ connected? && @lock.synchronize { @raw_connection&.ping } || false
148
125
  rescue ::Trilogy::Error
149
126
  false
150
127
  end
@@ -206,7 +183,7 @@ module ActiveRecord
206
183
  end
207
184
 
208
185
  def full_version
209
- schema_cache.database_version.full_version_string
186
+ database_version.full_version_string
210
187
  end
211
188
 
212
189
  def get_full_version
@@ -219,18 +196,12 @@ module ActiveRecord
219
196
  if exception.is_a?(::Trilogy::TimeoutError) && !exception.error_code
220
197
  return ActiveRecord::AdapterTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
221
198
  end
222
- error_code = exception.error_code if exception.respond_to?(:error_code)
223
-
224
- case error_code
225
- when ER_SERVER_SHUTDOWN
226
- return ConnectionFailed.new(message, connection_pool: @pool)
227
- end
228
199
 
229
200
  case exception
230
- when Errno::EPIPE, SocketError, IOError
201
+ when ::Trilogy::ConnectionClosed, ::Trilogy::EOFError
231
202
  return ConnectionFailed.new(message, connection_pool: @pool)
232
203
  when ::Trilogy::Error
233
- if /Connection reset by peer|TRILOGY_CLOSED_CONNECTION|TRILOGY_INVALID_SEQUENCE_ID|TRILOGY_UNEXPECTED_PACKET/.match?(exception.message)
204
+ if exception.is_a?(SystemCallError) || exception.message.include?("TRILOGY_INVALID_SEQUENCE_ID")
234
205
  return ConnectionFailed.new(message, connection_pool: @pool)
235
206
  end
236
207
  end