activerecord 7.1.5.1 → 7.2.2.1

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +645 -2329
  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 +15 -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 +7 -1
  15. data/lib/active_record/associations/collection_proxy.rb +14 -1
  16. data/lib/active_record/associations/errors.rb +265 -0
  17. data/lib/active_record/associations/has_many_association.rb +1 -1
  18. data/lib/active_record/associations/has_many_through_association.rb +7 -1
  19. data/lib/active_record/associations/join_dependency/join_association.rb +27 -25
  20. data/lib/active_record/associations/nested_error.rb +47 -0
  21. data/lib/active_record/associations/preloader/association.rb +2 -1
  22. data/lib/active_record/associations/preloader/branch.rb +7 -1
  23. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  24. data/lib/active_record/associations/singular_association.rb +6 -0
  25. data/lib/active_record/associations/through_association.rb +1 -1
  26. data/lib/active_record/associations.rb +59 -292
  27. data/lib/active_record/attribute_assignment.rb +0 -2
  28. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  29. data/lib/active_record/attribute_methods/primary_key.rb +23 -55
  30. data/lib/active_record/attribute_methods/read.rb +1 -13
  31. data/lib/active_record/attribute_methods/serialization.rb +4 -24
  32. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -6
  33. data/lib/active_record/attribute_methods.rb +54 -63
  34. data/lib/active_record/attributes.rb +61 -47
  35. data/lib/active_record/autosave_association.rb +12 -29
  36. data/lib/active_record/base.rb +2 -3
  37. data/lib/active_record/callbacks.rb +1 -1
  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 +270 -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 +189 -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 +15 -6
  46. data/lib/active_record/connection_adapters/abstract/transaction.rb +125 -62
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -44
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +40 -10
  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 +6 -0
  52. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +16 -15
  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/interval.rb +1 -1
  57. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  58. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  59. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -0
  60. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +15 -11
  61. data/lib/active_record/connection_adapters/postgresql_adapter.rb +29 -24
  62. data/lib/active_record/connection_adapters/schema_cache.rb +123 -128
  63. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  64. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +10 -6
  65. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +44 -46
  66. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  67. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  68. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  69. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +25 -2
  70. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +125 -75
  71. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +15 -15
  72. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -48
  73. data/lib/active_record/connection_adapters.rb +121 -0
  74. data/lib/active_record/connection_handling.rb +56 -41
  75. data/lib/active_record/core.rb +85 -37
  76. data/lib/active_record/counter_cache.rb +18 -9
  77. data/lib/active_record/database_configurations/connection_url_resolver.rb +7 -2
  78. data/lib/active_record/database_configurations/database_config.rb +19 -4
  79. data/lib/active_record/database_configurations/hash_config.rb +38 -34
  80. data/lib/active_record/database_configurations/url_config.rb +20 -1
  81. data/lib/active_record/database_configurations.rb +1 -1
  82. data/lib/active_record/delegated_type.rb +24 -0
  83. data/lib/active_record/dynamic_matchers.rb +2 -2
  84. data/lib/active_record/encryption/encryptable_record.rb +3 -3
  85. data/lib/active_record/encryption/encrypted_attribute_type.rb +24 -4
  86. data/lib/active_record/encryption/encryptor.rb +18 -3
  87. data/lib/active_record/encryption/key_provider.rb +1 -1
  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/enum.rb +18 -1
  93. data/lib/active_record/errors.rb +46 -20
  94. data/lib/active_record/explain.rb +13 -24
  95. data/lib/active_record/fixtures.rb +37 -31
  96. data/lib/active_record/future_result.rb +8 -4
  97. data/lib/active_record/gem_version.rb +2 -2
  98. data/lib/active_record/inheritance.rb +4 -2
  99. data/lib/active_record/insert_all.rb +18 -15
  100. data/lib/active_record/integration.rb +4 -1
  101. data/lib/active_record/internal_metadata.rb +48 -34
  102. data/lib/active_record/locking/optimistic.rb +7 -6
  103. data/lib/active_record/log_subscriber.rb +0 -21
  104. data/lib/active_record/message_pack.rb +1 -1
  105. data/lib/active_record/migration/command_recorder.rb +2 -3
  106. data/lib/active_record/migration/compatibility.rb +5 -3
  107. data/lib/active_record/migration/default_strategy.rb +4 -5
  108. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  109. data/lib/active_record/migration.rb +85 -76
  110. data/lib/active_record/model_schema.rb +31 -68
  111. data/lib/active_record/nested_attributes.rb +11 -3
  112. data/lib/active_record/normalization.rb +3 -7
  113. data/lib/active_record/persistence.rb +30 -352
  114. data/lib/active_record/query_cache.rb +19 -8
  115. data/lib/active_record/query_logs.rb +15 -0
  116. data/lib/active_record/querying.rb +21 -9
  117. data/lib/active_record/railtie.rb +37 -55
  118. data/lib/active_record/railties/controller_runtime.rb +13 -4
  119. data/lib/active_record/railties/databases.rake +40 -43
  120. data/lib/active_record/reflection.rb +98 -36
  121. data/lib/active_record/relation/batches/batch_enumerator.rb +15 -2
  122. data/lib/active_record/relation/batches.rb +14 -8
  123. data/lib/active_record/relation/calculations.rb +96 -63
  124. data/lib/active_record/relation/delegation.rb +8 -11
  125. data/lib/active_record/relation/finder_methods.rb +16 -2
  126. data/lib/active_record/relation/merger.rb +4 -6
  127. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  128. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -3
  129. data/lib/active_record/relation/predicate_builder.rb +3 -3
  130. data/lib/active_record/relation/query_methods.rb +224 -58
  131. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  132. data/lib/active_record/relation/spawn_methods.rb +2 -18
  133. data/lib/active_record/relation/where_clause.rb +7 -19
  134. data/lib/active_record/relation.rb +496 -72
  135. data/lib/active_record/result.rb +31 -44
  136. data/lib/active_record/runtime_registry.rb +39 -0
  137. data/lib/active_record/sanitization.rb +24 -19
  138. data/lib/active_record/schema.rb +8 -6
  139. data/lib/active_record/schema_dumper.rb +19 -9
  140. data/lib/active_record/schema_migration.rb +30 -14
  141. data/lib/active_record/scoping/named.rb +1 -0
  142. data/lib/active_record/signed_id.rb +20 -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 +69 -41
  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 +86 -89
  150. data/lib/active_record/testing/query_assertions.rb +121 -0
  151. data/lib/active_record/timestamp.rb +2 -2
  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 +132 -0
  155. data/lib/active_record/transactions.rb +70 -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 +15 -10
  161. data/lib/active_record/validations.rb +4 -1
  162. data/lib/active_record.rb +148 -39
  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/sqlite.rb +25 -0
  182. data/lib/arel/visitors/to_sql.rb +29 -16
  183. data/lib/arel.rb +7 -3
  184. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  185. metadata +16 -10
@@ -21,7 +21,7 @@ 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) # :nodoc:
24
+ def internal_exec_query(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
25
25
  sql = transform_query(sql)
26
26
  check_if_write_query(sql)
27
27
 
@@ -29,7 +29,7 @@ module ActiveRecord
29
29
 
30
30
  type_casted_binds = type_casted_binds(binds)
31
31
 
32
- log(sql, name, binds, type_casted_binds, async: async) do
32
+ log(sql, name, binds, type_casted_binds, async: async) do |notification_payload|
33
33
  with_raw_connection do |conn|
34
34
  # Don't cache statements if they are not prepared
35
35
  unless prepare
@@ -52,7 +52,9 @@ module ActiveRecord
52
52
  end
53
53
  verified!
54
54
 
55
- build_result(columns: cols, rows: records)
55
+ result = build_result(columns: cols, rows: records)
56
+ notification_payload[:row_count] = result.length
57
+ result
56
58
  end
57
59
  end
58
60
  end
@@ -104,7 +106,7 @@ module ActiveRecord
104
106
 
105
107
  # https://stackoverflow.com/questions/17574784
106
108
  # https://www.sqlite.org/lang_datefunc.html
107
- HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')").freeze # :nodoc:
109
+ HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')", retryable: true).freeze # :nodoc:
108
110
  private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP
109
111
 
110
112
  def high_precision_current_timestamp
@@ -113,10 +115,11 @@ module ActiveRecord
113
115
 
114
116
  private
115
117
  def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: false)
116
- log(sql, name, async: async) do
118
+ log(sql, name, async: async) do |notification_payload|
117
119
  with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
118
120
  result = conn.execute(sql)
119
121
  verified!
122
+ notification_payload[:row_count] = result.length
120
123
  result
121
124
  end
122
125
  end
@@ -136,10 +139,11 @@ module ActiveRecord
136
139
  check_if_write_query(sql)
137
140
  mark_transaction_written_if_write(sql)
138
141
 
139
- log(sql, name) do
142
+ log(sql, name) do |notification_payload|
140
143
  with_raw_connection do |conn|
141
144
  result = conn.execute_batch2(sql)
142
145
  verified!
146
+ notification_payload[:row_count] = result.length
143
147
  result
144
148
  end
145
149
  end
@@ -4,9 +4,52 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module SQLite3
6
6
  module Quoting # :nodoc:
7
+ extend ActiveSupport::Concern
8
+
7
9
  QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc:
8
10
  QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc:
9
11
 
12
+ module ClassMethods # :nodoc:
13
+ def column_name_matcher
14
+ /
15
+ \A
16
+ (
17
+ (?:
18
+ # "table_name"."column_name" | function(one or no argument)
19
+ ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\))
20
+ )
21
+ (?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
22
+ )
23
+ (?:\s*,\s*\g<1>)*
24
+ \z
25
+ /ix
26
+ end
27
+
28
+ def column_name_with_order_matcher
29
+ /
30
+ \A
31
+ (
32
+ (?:
33
+ # "table_name"."column_name" | function(one or no argument)
34
+ ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\))
35
+ )
36
+ (?:\s+COLLATE\s+(?:\w+|"\w+"))?
37
+ (?:\s+ASC|\s+DESC)?
38
+ )
39
+ (?:\s*,\s*\g<1>)*
40
+ \z
41
+ /ix
42
+ end
43
+
44
+ def quote_column_name(name)
45
+ QUOTED_COLUMN_NAMES[name] ||= %Q("#{name.to_s.gsub('"', '""')}").freeze
46
+ end
47
+
48
+ def quote_table_name(name)
49
+ QUOTED_TABLE_NAMES[name] ||= %Q("#{name.to_s.gsub('"', '""').gsub(".", "\".\"")}").freeze
50
+ end
51
+ end
52
+
10
53
  def quote_string(s)
11
54
  ::SQLite3::Database.quote(s)
12
55
  end
@@ -15,14 +58,6 @@ module ActiveRecord
15
58
  quote_column_name(attr)
16
59
  end
17
60
 
18
- def quote_table_name(name)
19
- QUOTED_TABLE_NAMES[name] ||= super.gsub(".", "\".\"").freeze
20
- end
21
-
22
- def quote_column_name(name)
23
- QUOTED_COLUMN_NAMES[name] ||= %Q("#{super.gsub('"', '""')}")
24
- end
25
-
26
61
  def quoted_time(value)
27
62
  value = value.change(year: 2000, month: 1, day: 1)
28
63
  quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ")
@@ -63,7 +98,7 @@ module ActiveRecord
63
98
 
64
99
  def type_cast(value) # :nodoc:
65
100
  case value
66
- when BigDecimal
101
+ when BigDecimal, Rational
67
102
  value.to_f
68
103
  when String
69
104
  if value.encoding == Encoding::ASCII_8BIT
@@ -75,43 +110,6 @@ module ActiveRecord
75
110
  super
76
111
  end
77
112
  end
78
-
79
- def column_name_matcher
80
- COLUMN_NAME
81
- end
82
-
83
- def column_name_with_order_matcher
84
- COLUMN_NAME_WITH_ORDER
85
- end
86
-
87
- COLUMN_NAME = /
88
- \A
89
- (
90
- (?:
91
- # "table_name"."column_name" | function(one or no argument)
92
- ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\))
93
- )
94
- (?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
95
- )
96
- (?:\s*,\s*\g<1>)*
97
- \z
98
- /ix
99
-
100
- COLUMN_NAME_WITH_ORDER = /
101
- \A
102
- (
103
- (?:
104
- # "table_name"."column_name" | function(one or no argument)
105
- ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\))
106
- )
107
- (?:\s+COLLATE\s+(?:\w+|"\w+"))?
108
- (?:\s+ASC|\s+DESC)?
109
- )
110
- (?:\s*,\s*\g<1>)*
111
- \z
112
- /ix
113
-
114
- private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
115
113
  end
116
114
  end
117
115
  end
@@ -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,8 @@ 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 = table_info(table_name)
476
+ raise ActiveRecord::StatementInvalid.new("Could not find table '#{table_name}'", connection_pool: @pool) if structure.empty?
459
477
  table_structure_with_collation(table_name, structure)
460
478
  end
461
479
  alias column_definitions table_structure
@@ -494,8 +512,9 @@ module ActiveRecord
494
512
  # See: https://www.sqlite.org/lang_altertable.html
495
513
  # SQLite has an additional restriction on the ALTER TABLE statement
496
514
  def invalid_alter_table_type?(type, options)
497
- type.to_sym == :primary_key || options[:primary_key] ||
498
- options[:null] == false && options[:default].nil?
515
+ type == :primary_key || options[:primary_key] ||
516
+ options[:null] == false && options[:default].nil? ||
517
+ (type == :virtual && options[:stored])
499
518
  end
500
519
 
501
520
  def alter_table(
@@ -551,12 +570,6 @@ module ActiveRecord
551
570
  options[:rename][column.name.to_sym] ||
552
571
  column.name) : column.name
553
572
 
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
573
  column_options = {
561
574
  limit: column.limit,
562
575
  precision: column.precision,
@@ -566,19 +579,31 @@ module ActiveRecord
566
579
  primary_key: column_name == from_primary_key
567
580
  }
568
581
 
569
- unless column.auto_increment?
570
- column_options[:default] = default
582
+ if column.virtual?
583
+ column_options[:as] = column.default_function
584
+ column_options[:stored] = column.virtual_stored?
585
+ column_options[:type] = column.type
586
+ elsif column.has_default?
587
+ type = lookup_cast_type_from_column(column)
588
+ default = type.deserialize(column.default)
589
+ default = -> { column.default_function } if default.nil?
590
+
591
+ unless column.auto_increment?
592
+ column_options[:default] = default
593
+ end
571
594
  end
572
595
 
573
- column_type = column.bigint? ? :bigint : column.type
596
+ column_type = column.virtual? ? :virtual : (column.bigint? ? :bigint : column.type)
574
597
  @definition.column(column_name, column_type, **column_options)
575
598
  end
576
599
 
577
600
  yield @definition if block_given?
578
601
  end
579
602
  copy_table_indexes(from, to, options[:rename] || {})
603
+
604
+ columns_to_copy = @definition.columns.reject { |col| col.options.key?(:as) }.map(&:name)
580
605
  copy_table_contents(from, to,
581
- @definition.columns.map(&:name),
606
+ columns_to_copy,
582
607
  options[:rename] || {})
583
608
  end
584
609
 
@@ -643,32 +668,22 @@ module ActiveRecord
643
668
 
644
669
  COLLATE_REGEX = /.*"(\w+)".*collate\s+"(\w+)".*/i
645
670
  PRIMARY_KEY_AUTOINCREMENT_REGEX = /.*"(\w+)".+PRIMARY KEY AUTOINCREMENT/i
671
+ GENERATED_ALWAYS_AS_REGEX = /.*"(\w+)".+GENERATED ALWAYS AS \((.+)\) (?:STORED|VIRTUAL)/i
646
672
 
647
673
  def table_structure_with_collation(table_name, basic_structure)
648
674
  collation_hash = {}
649
675
  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
676
+ generated_columns = {}
656
677
 
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")
678
+ column_strings = table_structure_sql(table_name, basic_structure.map { |column| column["name"] })
661
679
 
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
666
-
667
- columns_string.split(",").each do |column_string|
680
+ if column_strings.any?
681
+ column_strings.each do |column_string|
668
682
  # This regex will match the column name and collation type and will save
669
683
  # the value in $1 and $2 respectively.
670
684
  collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string
671
685
  auto_increments[$1] = true if PRIMARY_KEY_AUTOINCREMENT_REGEX =~ column_string
686
+ generated_columns[$1] = $2 if GENERATED_ALWAYS_AS_REGEX =~ column_string
672
687
  end
673
688
 
674
689
  basic_structure.map do |column|
@@ -682,6 +697,10 @@ module ActiveRecord
682
697
  column["auto_increment"] = true
683
698
  end
684
699
 
700
+ if generated_columns.has_key?(column_name)
701
+ column["dflt_value"] = generated_columns[column_name]
702
+ end
703
+
685
704
  column
686
705
  end
687
706
  else
@@ -689,6 +708,50 @@ module ActiveRecord
689
708
  end
690
709
  end
691
710
 
711
+ UNQUOTED_OPEN_PARENS_REGEX = /\((?![^'"]*['"][^'"]*$)/
712
+ FINAL_CLOSE_PARENS_REGEX = /\);*\z/
713
+
714
+ def table_structure_sql(table_name, column_names = nil)
715
+ unless column_names
716
+ column_info = table_info(table_name)
717
+ column_names = column_info.map { |column| column["name"] }
718
+ end
719
+
720
+ sql = <<~SQL
721
+ SELECT sql FROM
722
+ (SELECT * FROM sqlite_master UNION ALL
723
+ SELECT * FROM sqlite_temp_master)
724
+ WHERE type = 'table' AND name = #{quote(table_name)}
725
+ SQL
726
+
727
+ # Result will have following sample string
728
+ # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
729
+ # "password_digest" varchar COLLATE "NOCASE",
730
+ # "o_id" integer,
731
+ # CONSTRAINT "fk_rails_78146ddd2e" FOREIGN KEY ("o_id") REFERENCES "os" ("id"));
732
+ result = query_value(sql, "SCHEMA")
733
+
734
+ return [] unless result
735
+
736
+ # Splitting with left parentheses and discarding the first part will return all
737
+ # columns separated with comma(,).
738
+ result.partition(UNQUOTED_OPEN_PARENS_REGEX)
739
+ .last
740
+ .sub(FINAL_CLOSE_PARENS_REGEX, "")
741
+ # column definitions can have a comma in them, so split on commas followed
742
+ # by a space and a column name in quotes or followed by the keyword CONSTRAINT
743
+ .split(/,(?=\s(?:CONSTRAINT|"(?:#{Regexp.union(column_names).source})"))/i)
744
+ .map(&:strip)
745
+ end
746
+
747
+ def table_info(table_name)
748
+ if supports_virtual_columns?
749
+ internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
750
+ else
751
+ internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
752
+ end
753
+ end
754
+
692
755
  def arel_visitor
693
756
  Arel::Visitors::SQLite.new(self)
694
757
  end
@@ -723,29 +786,16 @@ module ActiveRecord
723
786
  end
724
787
  end
725
788
 
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")
789
+ super
790
+
791
+ pragmas = @config.fetch(:pragmas, {}).stringify_keys
792
+ DEFAULT_PRAGMAS.merge(pragmas).each do |pragma, value|
793
+ if ::SQLite3::Pragmas.method_defined?("#{pragma}=")
794
+ @raw_connection.public_send("#{pragma}=", value)
795
+ else
796
+ warn "Unknown SQLite pragma: #{pragma}"
797
+ end
742
798
  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
799
  end
750
800
  end
751
801
  ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)