Skip to content

Commit ea07a25

Browse files
committed
Add support for SQLite
Rails 8 ships with production-grade SQLite, which means active_record_doctor should support it, too. This commit adds a test suite for SQLite3. This required making a few changes: 1. Making the database name used by the test suite depend on the adapter as SQLite uses an in-memory database. 2. Dedicated require_* methods for skipping test cases that are relevant to the current database under test. 3. Splitting a few test cases into SQLite-specific code and code for other databases. Additionally, short_primary_key is effectively disabled in SQLite.
1 parent 4ee897c commit ea07a25

14 files changed

+196
-113
lines changed

Rakefile

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ RuboCop::RakeTask.new
2424
require "rake/testtask"
2525

2626
namespace :test do
27-
["postgresql", "mysql2"].each do |adapter|
27+
["postgresql", "mysql2", "sqlite3"].each do |adapter|
2828
Rake::TestTask.new(adapter) do |t|
29-
t.deps = ["set_#{adapter}_env"]
29+
t.deps = ["prepare_#{adapter}"]
3030
t.libs = ["lib", "test"]
3131
t.ruby_opts = ["-rsetup"]
3232
t.pattern = "test/**/*_test.rb"
@@ -36,10 +36,12 @@ namespace :test do
3636
t.warning = false
3737
end
3838

39-
task("set_#{adapter}_env") { ENV["DATABASE_ADAPTER"] = adapter }
39+
task :"prepare_#{adapter}" do
40+
ENV["DATABASE_ADAPTER"] = adapter
41+
end
4042
end
4143
end
4244

43-
task test: ["test:postgresql", "test:mysql2"]
45+
task test: ["test:postgresql", "test:mysql2", "test:sqlite3"]
4446

4547
task default: :test

active_record_doctor.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ Gem::Specification.new do |s|
2828
s.add_development_dependency "railties", ACTIVE_RECORD_SPEC
2929
s.add_development_dependency "rake", "~> 13.2.1"
3030
s.add_development_dependency "rubocop", "~> 1.68.0"
31+
s.add_development_dependency "sqlite3", "~> 2.2.0"
3132
s.add_development_dependency "transient_record", "~> 2.0.0"
3233
end

lib/active_record_doctor/detectors/short_primary_key_type.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ def message(table:, column:)
2020
end
2121

2222
def detect
23+
return if ActiveRecordDoctor::Utils.sqlite?
24+
2325
each_table(except: config(:ignore_tables)) do |table|
2426
column = primary_key(table)
2527
next if column.nil?

lib/active_record_doctor/utils.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ def mysql?(connection = ActiveRecord::Base.connection)
1111
connection.adapter_name == "Mysql2"
1212
end
1313

14+
def sqlite?(connection = ActiveRecord::Base.connection)
15+
connection.adapter_name == "SQLite"
16+
end
17+
1418
def expression_indexes_unsupported?(connection = ActiveRecord::Base.connection)
1519
# Active Record is unable to correctly parse expression indexes for MySQL.
1620
mysql?(connection) && ActiveRecord::VERSION::STRING < "7.1"

test/active_record_doctor/detectors/extraneous_indexes_test.rb

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def test_index_on_primary_key_is_duplicate
1212
end
1313

1414
def test_partial_index_on_primary_key
15-
skip("MySQL doesn't support partial indexes") if mysql?
15+
require_partial_indexes!
1616

1717
Context.create_table(:users) do |t|
1818
t.boolean :admin
@@ -92,7 +92,7 @@ def test_unique_index_with_fewer_columns
9292
end
9393

9494
def test_expression_index_not_covered_by_multicolumn_index
95-
skip("Expression indexes are not supported") if ActiveRecordDoctor::Utils.expression_indexes_unsupported?
95+
require_expression_indexes!
9696

9797
Context.create_table(:users) do |t|
9898
t.string :first_name
@@ -105,7 +105,7 @@ def test_expression_index_not_covered_by_multicolumn_index
105105
end
106106

107107
def test_unique_expression_index_not_covered_by_unique_multicolumn_index
108-
skip("Expression indexes are not supported") if ActiveRecordDoctor::Utils.expression_indexes_unsupported?
108+
require_expression_indexes!
109109

110110
Context.create_table(:users) do |t|
111111
t.string :first_name
@@ -118,14 +118,16 @@ def test_unique_expression_index_not_covered_by_unique_multicolumn_index
118118
end
119119

120120
def test_not_covered_by_different_index_type
121+
require_additional_index_types!
122+
121123
Context.create_table(:users) do |t|
122124
t.string :first_name
123125
t.string :last_name
124126
t.index [:last_name, :first_name], using: :btree
125127

126128
if mysql?
127129
t.index :last_name, type: :fulltext
128-
else
130+
elsif postgresql?
129131
t.index :last_name, using: :hash
130132
end
131133
end
@@ -134,7 +136,7 @@ def test_not_covered_by_different_index_type
134136
end
135137

136138
def test_not_covered_by_partial_index
137-
skip("MySQL doesn't support partial indexes") if mysql?
139+
require_partial_indexes!
138140

139141
Context.create_table(:users) do |t|
140142
t.string :first_name
@@ -148,7 +150,7 @@ def test_not_covered_by_partial_index
148150
end
149151

150152
def test_not_covered_with_different_opclasses
151-
skip("MySQL doesn't support operator classes") if mysql?
153+
require_operator_classes!
152154

153155
Context.create_table(:users) do |t|
154156
t.string :first_name
@@ -161,7 +163,7 @@ def test_not_covered_with_different_opclasses
161163
end
162164

163165
def test_single_column_covered_by_multi_column_on_materialized_view_is_duplicate
164-
skip("Only PostgreSQL supports materialized views") unless postgresql?
166+
require_materialized_views!
165167

166168
begin
167169
Context.create_table(:users) do |t|
@@ -188,8 +190,7 @@ def test_single_column_covered_by_multi_column_on_materialized_view_is_duplicate
188190
end
189191

190192
def test_include_index_covered_by_other_non_include_index
191-
skip("ActiveRecord < 7.1 doesn't support include indexes") if ActiveRecord::VERSION::STRING < "7.1"
192-
skip("Only PostgreSQL supports include indexes") unless postgresql?
193+
require_non_key_index_columns!
193194

194195
Context.create_table(:users) do |t|
195196
t.string :first_name
@@ -204,8 +205,7 @@ def test_include_index_covered_by_other_non_include_index
204205
end
205206

206207
def test_include_index_covered_by_other_include_index
207-
skip("ActiveRecord < 7.1 doesn't support include indexes") if ActiveRecord::VERSION::STRING < "7.1"
208-
skip("Only PostgreSQL supports include indexes") unless postgresql?
208+
require_non_key_index_columns!
209209

210210
Context.create_table(:users) do |t|
211211
t.string :first_name
@@ -221,8 +221,7 @@ def test_include_index_covered_by_other_include_index
221221
end
222222

223223
def test_include_index_not_covered_by_other_index
224-
skip("ActiveRecord < 7.1 doesn't support include indexes") if ActiveRecord::VERSION::STRING < "7.1"
225-
skip("Only PostgreSQL supports include indexes") unless postgresql?
224+
require_non_key_index_columns!
226225

227226
Context.create_table(:users) do |t|
228227
t.string :first_name

test/active_record_doctor/detectors/incorrect_length_validation_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def test_validation_and_limit_different_is_error
2626
end
2727

2828
def test_validation_and_no_limit_is_error
29-
skip("MySQL always sets a limit on text columns") if mysql?
29+
require_arbitrary_long_text_columns!
3030

3131
Context.create_table(:users) do |t|
3232
t.string :email
@@ -51,7 +51,7 @@ def test_no_validation_and_limit_is_error
5151
end
5252

5353
def test_no_validation_and_no_limit_is_ok
54-
skip("MySQL always sets a limit on text columns") if mysql?
54+
require_arbitrary_long_text_columns!
5555

5656
Context.create_table(:users) do |t|
5757
t.string :email

test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22

33
class ActiveRecordDoctor::Detectors::MismatchedForeignKeyTypeTest < Minitest::Test
44
def test_mismatched_foreign_key_type_is_reported
5-
# MySQL does not allow foreign keys to have different type than paired primary keys
6-
return if mysql?
5+
require_foreign_keys_of_different_type!
76

8-
Context.create_table(:companies, id: :bigint)
7+
Context.create_table(:companies, id: :smallint)
98
Context.create_table(:users) do |t|
109
t.references :company, foreign_key: true, type: :integer
1110
end
1211

13-
assert_problems(<<~OUTPUT)
14-
users.company_id is a foreign key of type integer and references companies.id of type bigint - foreign keys should be of the same type as the referenced column
15-
OUTPUT
12+
if sqlite?
13+
assert_problems(<<~OUTPUT)
14+
users.company_id is a foreign key of type INTEGER and references companies.id of type smallint - foreign keys should be of the same type as the referenced column
15+
OUTPUT
16+
else
17+
assert_problems(<<~OUTPUT)
18+
users.company_id is a foreign key of type integer and references companies.id of type smallint - foreign keys should be of the same type as the referenced column
19+
OUTPUT
20+
end
1621
end
1722

1823
def test_matched_foreign_key_type_is_not_reported
@@ -25,8 +30,7 @@ def test_matched_foreign_key_type_is_not_reported
2530
end
2631

2732
def test_mismatched_foreign_key_with_non_primary_key_type_is_reported
28-
# MySQL does not allow foreign keys to have different type than paired primary keys
29-
return if mysql?
33+
require_foreign_keys_of_different_type!
3034

3135
Context.create_table(:companies, id: :bigint) do |t|
3236
t.string :code
@@ -37,9 +41,15 @@ def test_mismatched_foreign_key_with_non_primary_key_type_is_reported
3741
t.foreign_key :companies, table: :companies, column: :code, primary_key: :code
3842
end
3943

40-
assert_problems(<<~OUTPUT)
41-
users.code is a foreign key of type text and references companies.code of type character varying - foreign keys should be of the same type as the referenced column
42-
OUTPUT
44+
if sqlite?
45+
assert_problems(<<~OUTPUT)
46+
users.code is a foreign key of type TEXT and references companies.code of type varchar - foreign keys should be of the same type as the referenced column
47+
OUTPUT
48+
else
49+
assert_problems(<<~OUTPUT)
50+
users.code is a foreign key of type text and references companies.code of type character varying - foreign keys should be of the same type as the referenced column
51+
OUTPUT
52+
end
4353
end
4454

4555
def test_matched_foreign_key_with_non_primary_key_type_is_not_reported
@@ -59,8 +69,7 @@ def test_matched_foreign_key_with_non_primary_key_type_is_not_reported
5969
end
6070

6171
def test_config_ignore_tables
62-
# MySQL does not allow foreign keys to have different type than paired primary keys
63-
return if mysql?
72+
require_foreign_keys_of_different_type!
6473

6574
Context.create_table(:companies, id: :bigint)
6675
Context.create_table(:users) do |t|
@@ -78,8 +87,7 @@ def test_config_ignore_tables
7887
end
7988

8089
def test_global_ignore_tables
81-
# MySQL does not allow foreign keys to have different type than paired primary keys
82-
return if mysql?
90+
require_foreign_keys_of_different_type!
8391

8492
Context.create_table(:companies, id: :bigint)
8593
Context.create_table(:users) do |t|
@@ -96,8 +104,7 @@ def test_global_ignore_tables
96104
end
97105

98106
def test_config_ignore_columns
99-
# MySQL does not allow foreign keys to have different type than paired primary keys
100-
return if mysql?
107+
require_foreign_keys_of_different_type!
101108

102109
Context.create_table(:companies, id: :bigint)
103110
Context.create_table(:users) do |t|

test/active_record_doctor/detectors/missing_non_null_constraint_test.rb

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_optional_columns_with_required_polymorphic_association_are_disallowed
4040
end
4141

4242
def test_optional_foreign_keys_with_required_with_if_association_are_disallowed
43-
skip("ActiveRecord < 7.1 doesn't support optimized association presence validation") if ActiveRecord::VERSION::STRING < "7.1"
43+
require_optimized_association_presence_validations!
4444

4545
begin
4646
previous = ActiveRecord.belongs_to_required_validates_foreign_key
@@ -228,37 +228,25 @@ def test_custom_sti_column_is_ignored
228228
end
229229

230230
def test_not_null_check_constraint
231-
skip unless postgresql?
232-
233231
Context.create_table(:users) do |t|
234-
t.string :email
232+
if !sqlite?
233+
t.string :email
234+
end
235235
end.define_model do
236236
validates :email, presence: true
237237
end
238238

239-
ActiveRecord::Base.connection.execute(<<-SQL)
240-
ALTER TABLE users ADD CONSTRAINT email_not_null CHECK (email IS NOT NULL)
241-
SQL
242-
243-
refute_problems
244-
end
245-
246-
def test_not_null_check_constraint_not_valid
247-
skip unless postgresql?
248-
249-
Context.create_table(:users) do |t|
250-
t.string :email
251-
end.define_model do
252-
validates :email, presence: true
239+
if sqlite?
240+
ActiveRecord::Base.connection.execute(<<-SQL)
241+
ALTER TABLE users ADD COLUMN email VARCHAR CONSTRAINT email_not_null CHECK (email IS NOT NULL)
242+
SQL
243+
else
244+
ActiveRecord::Base.connection.execute(<<-SQL)
245+
ALTER TABLE users ADD CONSTRAINT email_not_null CHECK (email IS NOT NULL)
246+
SQL
253247
end
254248

255-
ActiveRecord::Base.connection.execute(<<-SQL)
256-
ALTER TABLE users ADD CONSTRAINT email_not_null CHECK (email IS NOT NULL) NOT VALID
257-
SQL
258-
259-
assert_problems(<<~OUTPUT)
260-
add `NOT NULL` to users.email - models validates its presence but it's not non-NULL in the database
261-
OUTPUT
249+
refute_problems
262250
end
263251

264252
def test_config_ignore_tables

test/active_record_doctor/detectors/missing_presence_validation_test.rb

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -146,35 +146,27 @@ def test_models_with_non_existent_tables_are_skipped
146146
end
147147

148148
def test_not_null_check_constraint
149-
skip unless postgresql?
150-
151149
Context.create_table(:users) do |t|
152-
t.string :name
150+
if !sqlite?
151+
t.string :name
152+
end
153153
end.define_model
154154

155-
ActiveRecord::Base.connection.execute(<<-SQL)
156-
ALTER TABLE users ADD CONSTRAINT name_not_null CHECK (name IS NOT NULL)
157-
SQL
155+
if sqlite?
156+
ActiveRecord::Base.connection.execute(<<-SQL)
157+
ALTER TABLE users ADD COLUMN name VARCHAR CONSTRAINT name_not_null CHECK (name IS NOT NULL)
158+
SQL
159+
else
160+
ActiveRecord::Base.connection.execute(<<-SQL)
161+
ALTER TABLE users ADD CONSTRAINT name_not_null CHECK (name IS NOT NULL)
162+
SQL
163+
end
158164

159165
assert_problems(<<~OUTPUT)
160166
add a `presence` validator to Context::User.name - it's NOT NULL but lacks a validator
161167
OUTPUT
162168
end
163169

164-
def test_not_null_check_constraint_not_valid
165-
skip unless postgresql?
166-
167-
Context.create_table(:users) do |t|
168-
t.string :name
169-
end.define_model
170-
171-
ActiveRecord::Base.connection.execute(<<-SQL)
172-
ALTER TABLE users ADD CONSTRAINT name_not_null CHECK (name IS NOT NULL) NOT VALID
173-
SQL
174-
175-
refute_problems
176-
end
177-
178170
def test_abstract_class
179171
Context.define_model(:ApplicationRecord) do
180172
self.abstract_class = true

0 commit comments

Comments
 (0)