activerecord 7.1.3.2 → 7.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +507 -2123
  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 +18 -11
  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 +4 -2
  15. data/lib/active_record/associations/collection_proxy.rb +14 -1
  16. data/lib/active_record/associations/has_many_association.rb +3 -3
  17. data/lib/active_record/associations/has_one_association.rb +2 -2
  18. data/lib/active_record/associations/join_dependency/join_association.rb +27 -25
  19. data/lib/active_record/associations/join_dependency.rb +5 -7
  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 +34 -11
  27. data/lib/active_record/attribute_assignment.rb +1 -11
  28. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  29. data/lib/active_record/attribute_methods/dirty.rb +1 -1
  30. data/lib/active_record/attribute_methods/primary_key.rb +23 -55
  31. data/lib/active_record/attribute_methods/read.rb +1 -13
  32. data/lib/active_record/attribute_methods/serialization.rb +4 -24
  33. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -6
  34. data/lib/active_record/attribute_methods.rb +87 -58
  35. data/lib/active_record/attributes.rb +55 -42
  36. data/lib/active_record/autosave_association.rb +14 -30
  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 -58
  41. data/lib/active_record/connection_adapters/abstract/database_statements.rb +35 -18
  42. data/lib/active_record/connection_adapters/abstract/query_cache.rb +160 -75
  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 +22 -9
  46. data/lib/active_record/connection_adapters/abstract/transaction.rb +60 -57
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +32 -61
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +69 -19
  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 -0
  52. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +11 -5
  53. data/lib/active_record/connection_adapters/mysql2_adapter.rb +20 -32
  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 +6 -0
  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 +16 -12
  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 +109 -77
  72. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +12 -6
  73. data/lib/active_record/connection_adapters/trilogy_adapter.rb +32 -65
  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 +59 -38
  77. data/lib/active_record/counter_cache.rb +23 -10
  78. data/lib/active_record/database_configurations/connection_url_resolver.rb +7 -2
  79. data/lib/active_record/database_configurations/database_config.rb +15 -4
  80. data/lib/active_record/database_configurations/hash_config.rb +44 -36
  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 +30 -6
  84. data/lib/active_record/destroy_association_async_job.rb +1 -1
  85. data/lib/active_record/dynamic_matchers.rb +2 -2
  86. data/lib/active_record/encryption/encryptable_record.rb +2 -2
  87. data/lib/active_record/encryption/encrypted_attribute_type.rb +24 -4
  88. data/lib/active_record/encryption/encryptor.rb +17 -2
  89. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  90. data/lib/active_record/encryption/message_serializer.rb +4 -0
  91. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  92. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  93. data/lib/active_record/encryption/scheme.rb +8 -4
  94. data/lib/active_record/enum.rb +11 -2
  95. data/lib/active_record/errors.rb +16 -11
  96. data/lib/active_record/explain.rb +13 -24
  97. data/lib/active_record/fixtures.rb +37 -31
  98. data/lib/active_record/future_result.rb +17 -4
  99. data/lib/active_record/gem_version.rb +3 -3
  100. data/lib/active_record/inheritance.rb +4 -2
  101. data/lib/active_record/insert_all.rb +18 -15
  102. data/lib/active_record/integration.rb +4 -1
  103. data/lib/active_record/internal_metadata.rb +48 -34
  104. data/lib/active_record/locking/optimistic.rb +8 -7
  105. data/lib/active_record/log_subscriber.rb +0 -21
  106. data/lib/active_record/marshalling.rb +1 -1
  107. data/lib/active_record/message_pack.rb +2 -2
  108. data/lib/active_record/migration/command_recorder.rb +2 -3
  109. data/lib/active_record/migration/compatibility.rb +11 -3
  110. data/lib/active_record/migration/default_strategy.rb +4 -5
  111. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  112. data/lib/active_record/migration.rb +85 -76
  113. data/lib/active_record/model_schema.rb +34 -69
  114. data/lib/active_record/nested_attributes.rb +11 -3
  115. data/lib/active_record/normalization.rb +3 -7
  116. data/lib/active_record/persistence.rb +32 -354
  117. data/lib/active_record/query_cache.rb +18 -6
  118. data/lib/active_record/query_logs.rb +15 -0
  119. data/lib/active_record/query_logs_formatter.rb +1 -1
  120. data/lib/active_record/querying.rb +21 -9
  121. data/lib/active_record/railtie.rb +52 -64
  122. data/lib/active_record/railties/controller_runtime.rb +13 -4
  123. data/lib/active_record/railties/databases.rake +41 -44
  124. data/lib/active_record/reflection.rb +98 -37
  125. data/lib/active_record/relation/batches/batch_enumerator.rb +15 -2
  126. data/lib/active_record/relation/batches.rb +3 -3
  127. data/lib/active_record/relation/calculations.rb +94 -61
  128. data/lib/active_record/relation/delegation.rb +8 -11
  129. data/lib/active_record/relation/finder_methods.rb +16 -2
  130. data/lib/active_record/relation/merger.rb +4 -6
  131. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  132. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +6 -1
  133. data/lib/active_record/relation/predicate_builder.rb +3 -3
  134. data/lib/active_record/relation/query_methods.rb +196 -43
  135. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  136. data/lib/active_record/relation/spawn_methods.rb +2 -18
  137. data/lib/active_record/relation/where_clause.rb +7 -19
  138. data/lib/active_record/relation.rb +500 -66
  139. data/lib/active_record/result.rb +32 -45
  140. data/lib/active_record/runtime_registry.rb +39 -0
  141. data/lib/active_record/sanitization.rb +24 -19
  142. data/lib/active_record/schema.rb +8 -6
  143. data/lib/active_record/schema_dumper.rb +19 -9
  144. data/lib/active_record/schema_migration.rb +30 -14
  145. data/lib/active_record/signed_id.rb +11 -1
  146. data/lib/active_record/statement_cache.rb +7 -7
  147. data/lib/active_record/table_metadata.rb +1 -10
  148. data/lib/active_record/tasks/database_tasks.rb +70 -42
  149. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  150. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  151. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -1
  152. data/lib/active_record/test_fixtures.rb +82 -91
  153. data/lib/active_record/testing/query_assertions.rb +121 -0
  154. data/lib/active_record/timestamp.rb +4 -2
  155. data/lib/active_record/token_for.rb +22 -12
  156. data/lib/active_record/touch_later.rb +1 -1
  157. data/lib/active_record/transaction.rb +68 -0
  158. data/lib/active_record/transactions.rb +43 -14
  159. data/lib/active_record/translation.rb +0 -2
  160. data/lib/active_record/type/serialized.rb +1 -3
  161. data/lib/active_record/type_caster/connection.rb +4 -4
  162. data/lib/active_record/validations/associated.rb +9 -3
  163. data/lib/active_record/validations/uniqueness.rb +14 -10
  164. data/lib/active_record/validations.rb +4 -1
  165. data/lib/active_record.rb +149 -40
  166. data/lib/arel/alias_predication.rb +1 -1
  167. data/lib/arel/collectors/bind.rb +2 -0
  168. data/lib/arel/collectors/composite.rb +7 -0
  169. data/lib/arel/collectors/sql_string.rb +1 -1
  170. data/lib/arel/collectors/substitute_binds.rb +1 -1
  171. data/lib/arel/nodes/binary.rb +0 -6
  172. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  173. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  174. data/lib/arel/nodes/node.rb +4 -3
  175. data/lib/arel/nodes/sql_literal.rb +7 -0
  176. data/lib/arel/nodes.rb +2 -2
  177. data/lib/arel/predications.rb +1 -1
  178. data/lib/arel/select_manager.rb +1 -1
  179. data/lib/arel/tree_manager.rb +8 -3
  180. data/lib/arel/update_manager.rb +2 -1
  181. data/lib/arel/visitors/dot.rb +1 -0
  182. data/lib/arel/visitors/mysql.rb +9 -4
  183. data/lib/arel/visitors/postgresql.rb +1 -12
  184. data/lib/arel/visitors/to_sql.rb +31 -17
  185. data/lib/arel.rb +7 -3
  186. metadata +17 -12
@@ -6,10 +6,11 @@ module ActiveRecord
6
6
  class Column < ConnectionAdapters::Column # :nodoc:
7
7
  attr_reader :rowid
8
8
 
9
- def initialize(*, auto_increment: nil, rowid: false, **)
9
+ def initialize(*, auto_increment: nil, rowid: false, generated_type: nil, **)
10
10
  super
11
11
  @auto_increment = auto_increment
12
12
  @rowid = rowid
13
+ @generated_type = generated_type
13
14
  end
14
15
 
15
16
  def auto_increment?
@@ -20,6 +21,18 @@ module ActiveRecord
20
21
  auto_increment? || rowid
21
22
  end
22
23
 
24
+ def virtual?
25
+ !@generated_type.nil?
26
+ end
27
+
28
+ def virtual_stored?
29
+ virtual? && @generated_type == :stored
30
+ end
31
+
32
+ def has_default?
33
+ super && !virtual?
34
+ end
35
+
23
36
  def init_with(coder)
24
37
  @auto_increment = coder["auto_increment"]
25
38
  super
@@ -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
@@ -11,20 +11,10 @@ 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", ">= 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
 
@@ -286,10 +287,11 @@ module ActiveRecord
286
287
  schema_cache.clear_data_source_cache!(table_name.to_s)
287
288
  schema_cache.clear_data_source_cache!(new_name.to_s)
288
289
  exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
289
- rename_table_indexes(table_name, new_name)
290
+ rename_table_indexes(table_name, new_name, **options)
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)