active_record_doctor 1.8.0 → 1.10.0
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.
- checksums.yaml +4 -4
- data/README.md +316 -54
- data/lib/active_record_doctor/config/default.rb +76 -0
- data/lib/active_record_doctor/config/loader.rb +137 -0
- data/lib/active_record_doctor/config.rb +14 -0
- data/lib/active_record_doctor/detectors/base.rb +142 -21
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +59 -48
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +31 -23
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +102 -35
- data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +63 -0
- data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +45 -0
- data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +32 -23
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +41 -28
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +29 -23
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +92 -32
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +45 -0
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +17 -20
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +43 -18
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +31 -20
- data/lib/active_record_doctor/detectors.rb +12 -4
- data/lib/active_record_doctor/errors.rb +226 -0
- data/lib/active_record_doctor/help.rb +39 -0
- data/lib/active_record_doctor/rake/task.rb +78 -0
- data/lib/active_record_doctor/runner.rb +41 -0
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +8 -3
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +34 -21
- data/lib/tasks/active_record_doctor.rake +9 -18
- data/test/active_record_doctor/config/loader_test.rb +120 -0
- data/test/active_record_doctor/config_test.rb +116 -0
- data/test/active_record_doctor/detectors/disable_test.rb +30 -0
- data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +165 -8
- data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +48 -5
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +288 -12
- data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +105 -0
- data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +82 -0
- data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +50 -4
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +172 -24
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +111 -14
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +223 -10
- data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +72 -0
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +34 -21
- data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +118 -8
- data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +56 -4
- data/test/active_record_doctor/runner_test.rb +42 -0
- data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +131 -0
- data/test/model_factory.rb +73 -23
- data/test/setup.rb +65 -71
- metadata +43 -7
- data/lib/active_record_doctor/printers/io_printer.rb +0 -133
- data/lib/active_record_doctor/task.rb +0 -28
- data/test/active_record_doctor/printers/io_printer_test.rb +0 -33
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveRecordDoctor::Detectors::ShortPrimaryKeyTypeTest < Minitest::Test
|
4
|
+
def setup
|
5
|
+
@connection = ActiveRecord::Base.connection
|
6
|
+
@connection.enable_extension("uuid-ossp") if postgresql?
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def teardown
|
11
|
+
@connection.disable_extension("uuid-ossp") if postgresql?
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_short_integer_primary_key_is_reported
|
16
|
+
create_table(:companies, id: :int)
|
17
|
+
|
18
|
+
# In Rails 4.2 and MySQL primary key is not created due to a bug
|
19
|
+
if mysql? && ActiveRecord::VERSION::STRING < "5.0"
|
20
|
+
@connection.execute("ALTER TABLE companies ADD PRIMARY KEY(id)")
|
21
|
+
end
|
22
|
+
|
23
|
+
assert_problems(<<~OUTPUT)
|
24
|
+
change the type of companies.id to bigint
|
25
|
+
OUTPUT
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_long_integer_primary_key_is_not_reported
|
29
|
+
create_table(:companies, id: :bigint)
|
30
|
+
refute_problems
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_uuid_primary_key_is_not_reported
|
34
|
+
skip unless postgresql?
|
35
|
+
|
36
|
+
create_table(:companies, id: :uuid)
|
37
|
+
refute_problems
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_no_primary_key_is_not_reported
|
41
|
+
create_table(:companies, id: false) do |t|
|
42
|
+
t.string :name, null: false
|
43
|
+
end
|
44
|
+
|
45
|
+
refute_problems
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_config_ignore_tables
|
49
|
+
create_table(:companies, id: :integer)
|
50
|
+
|
51
|
+
config_file(<<-CONFIG)
|
52
|
+
ActiveRecordDoctor.configure do |config|
|
53
|
+
config.detector :short_primary_key_type,
|
54
|
+
ignore_tables: ["companies"]
|
55
|
+
end
|
56
|
+
CONFIG
|
57
|
+
|
58
|
+
refute_problems
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_global_ignore_tables
|
62
|
+
create_table(:companies, id: :integer)
|
63
|
+
|
64
|
+
config_file(<<-CONFIG)
|
65
|
+
ActiveRecordDoctor.configure do |config|
|
66
|
+
config.global :ignore_tables, ["companies"]
|
67
|
+
end
|
68
|
+
CONFIG
|
69
|
+
|
70
|
+
refute_problems
|
71
|
+
end
|
72
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class ActiveRecordDoctor::Detectors::UndefinedTableReferencesTest < Minitest::Test
|
4
|
-
def
|
4
|
+
def test_model_backed_by_table
|
5
5
|
create_table(:users) do
|
6
6
|
end.create_model do
|
7
7
|
end
|
@@ -9,31 +9,19 @@ class ActiveRecordDoctor::Detectors::UndefinedTableReferencesTest < Minitest::Te
|
|
9
9
|
refute_problems
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
create_model(:
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
Rails 4.2 + PostgreSQL. It seems this is NOT your setup. Therefore, such models
|
19
|
-
will be erroneously reported below as not having their underlying tables/views.
|
20
|
-
Consider upgrading Rails or disabling this task temporarily.
|
21
|
-
The following models reference undefined tables:
|
22
|
-
ModelFactory::Models::User (the table users is undefined)
|
23
|
-
OUTPUT
|
24
|
-
else
|
25
|
-
assert_problems(<<OUTPUT)
|
26
|
-
The following models reference undefined tables:
|
27
|
-
ModelFactory::Models::User (the table users is undefined)
|
28
|
-
OUTPUT
|
29
|
-
end
|
12
|
+
def test_model_backed_by_non_existent_table
|
13
|
+
create_model(:User)
|
14
|
+
|
15
|
+
assert_problems(<<~OUTPUT)
|
16
|
+
ModelFactory::Models::User references a non-existent table or view named users
|
17
|
+
OUTPUT
|
30
18
|
end
|
31
19
|
|
32
|
-
def
|
20
|
+
def test_model_backed_by_view
|
33
21
|
# We replace the underlying table with a view. The view doesn't have to be
|
34
22
|
# backed by an actual table - it can simply return a predefined tuple.
|
35
23
|
ActiveRecord::Base.connection.execute("CREATE VIEW users AS SELECT 1")
|
36
|
-
create_model(:
|
24
|
+
create_model(:User)
|
37
25
|
|
38
26
|
begin
|
39
27
|
refute_problems
|
@@ -41,4 +29,29 @@ OUTPUT
|
|
41
29
|
ActiveRecord::Base.connection.execute("DROP VIEW users")
|
42
30
|
end
|
43
31
|
end
|
32
|
+
|
33
|
+
def test_config_ignore_tables
|
34
|
+
create_model(:User)
|
35
|
+
|
36
|
+
config_file(<<-CONFIG)
|
37
|
+
ActiveRecordDoctor.configure do |config|
|
38
|
+
config.detector :undefined_table_references,
|
39
|
+
ignore_models: ["ModelFactory::Models::User"]
|
40
|
+
end
|
41
|
+
CONFIG
|
42
|
+
|
43
|
+
refute_problems
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_global_ignore_tables
|
47
|
+
create_model(:User)
|
48
|
+
|
49
|
+
config_file(<<-CONFIG)
|
50
|
+
ActiveRecordDoctor.configure do |config|
|
51
|
+
config.global :ignore_models, ["ModelFactory::Models::User"]
|
52
|
+
end
|
53
|
+
CONFIG
|
54
|
+
|
55
|
+
refute_problems
|
56
|
+
end
|
44
57
|
end
|
@@ -11,6 +11,9 @@ class ActiveRecordDoctor::Detectors::UnindexedDeletedAtTest < Minitest::Test
|
|
11
11
|
t.index [:first_name, :last_name],
|
12
12
|
name: "index_profiles_on_first_name_and_last_name",
|
13
13
|
where: "deleted_at IS NULL"
|
14
|
+
t.index [:last_name],
|
15
|
+
name: "index_deleted_profiles_on_last_name",
|
16
|
+
where: "deleted_at IS NOT NULL"
|
14
17
|
end
|
15
18
|
|
16
19
|
refute_problems
|
@@ -27,10 +30,9 @@ class ActiveRecordDoctor::Detectors::UnindexedDeletedAtTest < Minitest::Test
|
|
27
30
|
name: "index_profiles_on_first_name_and_last_name"
|
28
31
|
end
|
29
32
|
|
30
|
-
assert_problems(
|
31
|
-
|
32
|
-
|
33
|
-
OUTPUT
|
33
|
+
assert_problems(<<~OUTPUT)
|
34
|
+
consider adding `WHERE deleted_at IS NULL` or `WHERE deleted_at IS NOT NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
|
35
|
+
OUTPUT
|
34
36
|
end
|
35
37
|
|
36
38
|
def test_indexed_discarded_at_is_not_reported
|
@@ -43,6 +45,9 @@ OUTPUT
|
|
43
45
|
t.index [:first_name, :last_name],
|
44
46
|
name: "index_profiles_on_first_name_and_last_name",
|
45
47
|
where: "discarded_at IS NULL"
|
48
|
+
t.index [:last_name],
|
49
|
+
name: "index_discarded_profiles_on_last_name",
|
50
|
+
where: "discarded_at IS NOT NULL"
|
46
51
|
end
|
47
52
|
|
48
53
|
refute_problems
|
@@ -59,9 +64,114 @@ OUTPUT
|
|
59
64
|
name: "index_profiles_on_first_name_and_last_name"
|
60
65
|
end
|
61
66
|
|
62
|
-
assert_problems(
|
63
|
-
|
64
|
-
|
65
|
-
|
67
|
+
assert_problems(<<~OUTPUT)
|
68
|
+
consider adding `WHERE discarded_at IS NULL` or `WHERE discarded_at IS NOT NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
|
69
|
+
OUTPUT
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_config_ignore_tables
|
73
|
+
skip("MySQL doesn't support partial indexes") if mysql?
|
74
|
+
|
75
|
+
create_table(:users) do |t|
|
76
|
+
t.string :first_name
|
77
|
+
t.string :last_name
|
78
|
+
t.datetime :discarded_at
|
79
|
+
t.index [:first_name, :last_name],
|
80
|
+
name: "index_profiles_on_first_name_and_last_name"
|
81
|
+
end
|
82
|
+
|
83
|
+
config_file(<<-CONFIG)
|
84
|
+
ActiveRecordDoctor.configure do |config|
|
85
|
+
config.detector :unindexed_deleted_at,
|
86
|
+
ignore_tables: ["users"]
|
87
|
+
end
|
88
|
+
CONFIG
|
89
|
+
|
90
|
+
refute_problems
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_global_ignore_tables
|
94
|
+
skip("MySQL doesn't support partial indexes") if mysql?
|
95
|
+
|
96
|
+
create_table(:users) do |t|
|
97
|
+
t.string :first_name
|
98
|
+
t.string :last_name
|
99
|
+
t.datetime :discarded_at
|
100
|
+
t.index [:first_name, :last_name],
|
101
|
+
name: "index_profiles_on_first_name_and_last_name"
|
102
|
+
end
|
103
|
+
|
104
|
+
config_file(<<-CONFIG)
|
105
|
+
ActiveRecordDoctor.configure do |config|
|
106
|
+
config.global :ignore_tables, ["users"]
|
107
|
+
end
|
108
|
+
CONFIG
|
109
|
+
|
110
|
+
refute_problems
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_config_ignore_columns
|
114
|
+
skip("MySQL doesn't support partial indexes") if mysql?
|
115
|
+
|
116
|
+
create_table(:users) do |t|
|
117
|
+
t.string :first_name
|
118
|
+
t.string :last_name
|
119
|
+
t.datetime :discarded_at
|
120
|
+
t.index [:first_name, :last_name],
|
121
|
+
name: "index_profiles_on_first_name_and_last_name"
|
122
|
+
end
|
123
|
+
|
124
|
+
config_file(<<-CONFIG)
|
125
|
+
ActiveRecordDoctor.configure do |config|
|
126
|
+
config.detector :unindexed_deleted_at,
|
127
|
+
ignore_columns: ["users.discarded_at"]
|
128
|
+
end
|
129
|
+
CONFIG
|
130
|
+
|
131
|
+
refute_problems
|
132
|
+
end
|
133
|
+
|
134
|
+
def test_config_ignore_indexes
|
135
|
+
skip("MySQL doesn't support partial indexes") if mysql?
|
136
|
+
|
137
|
+
create_table(:users) do |t|
|
138
|
+
t.string :first_name
|
139
|
+
t.string :last_name
|
140
|
+
t.datetime :discarded_at
|
141
|
+
t.index [:first_name, :last_name],
|
142
|
+
name: "index_profiles_on_first_name_and_last_name"
|
143
|
+
end
|
144
|
+
|
145
|
+
config_file(<<-CONFIG)
|
146
|
+
ActiveRecordDoctor.configure do |config|
|
147
|
+
config.detector :unindexed_deleted_at,
|
148
|
+
ignore_indexes: ["index_profiles_on_first_name_and_last_name"]
|
149
|
+
end
|
150
|
+
CONFIG
|
151
|
+
|
152
|
+
refute_problems
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_config_column_names
|
156
|
+
skip("MySQL doesn't support partial indexes") if mysql?
|
157
|
+
|
158
|
+
create_table(:users) do |t|
|
159
|
+
t.string :first_name
|
160
|
+
t.string :last_name
|
161
|
+
t.datetime :obliverated_at
|
162
|
+
t.index [:first_name, :last_name],
|
163
|
+
name: "index_profiles_on_first_name_and_last_name"
|
164
|
+
end
|
165
|
+
|
166
|
+
config_file(<<-CONFIG)
|
167
|
+
ActiveRecordDoctor.configure do |config|
|
168
|
+
config.detector :unindexed_deleted_at,
|
169
|
+
column_names: ["obliverated_at"]
|
170
|
+
end
|
171
|
+
CONFIG
|
172
|
+
|
173
|
+
assert_problems(<<~OUTPUT)
|
174
|
+
consider adding `WHERE obliverated_at IS NULL` or `WHERE obliverated_at IS NOT NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
|
175
|
+
OUTPUT
|
66
176
|
end
|
67
177
|
end
|
@@ -9,10 +9,9 @@ class ActiveRecordDoctor::Detectors::UnindexedForeignKeysTest < Minitest::Test
|
|
9
9
|
t.references :company, foreign_key: true, index: false
|
10
10
|
end
|
11
11
|
|
12
|
-
assert_problems(
|
13
|
-
|
14
|
-
|
15
|
-
OUTPUT
|
12
|
+
assert_problems(<<~OUTPUT)
|
13
|
+
add an index on users.company_id - foreign keys are often used in database lookups and should be indexed for performance reasons
|
14
|
+
OUTPUT
|
16
15
|
end
|
17
16
|
|
18
17
|
def test_indexed_foreign_key_is_not_reported
|
@@ -23,4 +22,57 @@ OUTPUT
|
|
23
22
|
|
24
23
|
refute_problems
|
25
24
|
end
|
25
|
+
|
26
|
+
def test_config_ignore_tables
|
27
|
+
skip("MySQL always indexes foreign keys") if mysql?
|
28
|
+
|
29
|
+
create_table(:companies)
|
30
|
+
create_table(:users) do |t|
|
31
|
+
t.references :company, foreign_key: true, index: false
|
32
|
+
end
|
33
|
+
|
34
|
+
config_file(<<-CONFIG)
|
35
|
+
ActiveRecordDoctor.configure do |config|
|
36
|
+
config.detector :unindexed_foreign_keys,
|
37
|
+
ignore_tables: ["users"]
|
38
|
+
end
|
39
|
+
CONFIG
|
40
|
+
|
41
|
+
refute_problems
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_global_ignore_tables
|
45
|
+
skip("MySQL always indexes foreign keys") if mysql?
|
46
|
+
|
47
|
+
create_table(:companies)
|
48
|
+
create_table(:users) do |t|
|
49
|
+
t.references :company, foreign_key: true, index: false
|
50
|
+
end
|
51
|
+
|
52
|
+
config_file(<<-CONFIG)
|
53
|
+
ActiveRecordDoctor.configure do |config|
|
54
|
+
config.global :ignore_tables, ["users"]
|
55
|
+
end
|
56
|
+
CONFIG
|
57
|
+
|
58
|
+
refute_problems
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_config_ignore_columns
|
62
|
+
skip("MySQL always indexes foreign keys") if mysql?
|
63
|
+
|
64
|
+
create_table(:companies)
|
65
|
+
create_table(:users) do |t|
|
66
|
+
t.references :company, foreign_key: true, index: false
|
67
|
+
end
|
68
|
+
|
69
|
+
config_file(<<-CONFIG)
|
70
|
+
ActiveRecordDoctor.configure do |config|
|
71
|
+
config.detector :unindexed_foreign_keys,
|
72
|
+
ignore_columns: ["users.company_id"]
|
73
|
+
end
|
74
|
+
CONFIG
|
75
|
+
|
76
|
+
refute_problems
|
77
|
+
end
|
26
78
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveRecordDoctor::RunnerTest < Minitest::Test
|
4
|
+
def test_run_one_raises_on_unknown_detectors
|
5
|
+
io = StringIO.new
|
6
|
+
runner = ActiveRecordDoctor::Runner.new(load_config, io)
|
7
|
+
|
8
|
+
assert_raises(KeyError) do
|
9
|
+
runner.run_one(:performance_issues)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_run_all_returns_true_when_no_errors
|
14
|
+
io = StringIO.new
|
15
|
+
runner = ActiveRecordDoctor::Runner.new(load_config, io)
|
16
|
+
|
17
|
+
assert(runner.run_all)
|
18
|
+
assert(io.string.blank?)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_run_all_returns_false_when_errors
|
22
|
+
# Create a model without its underlying table to trigger an error.
|
23
|
+
create_model(:User)
|
24
|
+
|
25
|
+
io = StringIO.new
|
26
|
+
runner = ActiveRecordDoctor::Runner.new(load_config, io)
|
27
|
+
|
28
|
+
refute(runner.run_all)
|
29
|
+
refute(io.string.blank?)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_help_prints_help
|
33
|
+
ActiveRecordDoctor.detectors.each do |name, _|
|
34
|
+
io = StringIO.new
|
35
|
+
runner = ActiveRecordDoctor::Runner.new(load_config, io)
|
36
|
+
|
37
|
+
runner.help(name)
|
38
|
+
|
39
|
+
refute(io.string.blank?, "expected help for #{name}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
|
5
|
+
require "generators/active_record_doctor/add_indexes/add_indexes_generator"
|
6
|
+
|
7
|
+
class ActiveRecordDoctor::AddIndexesGeneratorTest < Minitest::Test
|
8
|
+
TIMESTAMP = Time.new(2021, 2, 1, 13, 15, 30)
|
9
|
+
|
10
|
+
def test_create_migrations
|
11
|
+
create_table(:users) do |t|
|
12
|
+
t.integer :organization_id, null: false
|
13
|
+
t.integer :account_id, null: false
|
14
|
+
end
|
15
|
+
create_table(:organizations) do |t|
|
16
|
+
t.integer :owner_id
|
17
|
+
end
|
18
|
+
|
19
|
+
Dir.mktmpdir do |dir|
|
20
|
+
Dir.chdir(dir)
|
21
|
+
|
22
|
+
path = File.join(dir, "indexes.txt")
|
23
|
+
File.write(path, <<~INDEXES)
|
24
|
+
add an index on users.organization_id - foreign keys are often used in database lookups and should be indexed for performance reasons
|
25
|
+
add an index on users.account_id - foreign keys are often used in database lookups and should be indexed for performance reasons
|
26
|
+
add an index on organizations.owner_id - foreign keys are often used in database lookups and should be indexed for performance reasons
|
27
|
+
INDEXES
|
28
|
+
|
29
|
+
capture_io do
|
30
|
+
Time.stub(:now, TIMESTAMP) do
|
31
|
+
ActiveRecordDoctor::AddIndexesGenerator.start([path])
|
32
|
+
|
33
|
+
load(File.join("db", "migrate", "20210201131530_index_foreign_keys_in_users.rb"))
|
34
|
+
IndexForeignKeysInUsers.migrate(:up)
|
35
|
+
|
36
|
+
load(File.join("db", "migrate", "20210201131531_index_foreign_keys_in_organizations.rb"))
|
37
|
+
IndexForeignKeysInOrganizations.migrate(:up)
|
38
|
+
|
39
|
+
::Object.send(:remove_const, :IndexForeignKeysInUsers)
|
40
|
+
::Object.send(:remove_const, :IndexForeignKeysInOrganizations)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
assert_indexes([
|
45
|
+
["users", ["organization_id"]],
|
46
|
+
["users", ["account_id"]],
|
47
|
+
["organizations", ["owner_id"]]
|
48
|
+
])
|
49
|
+
|
50
|
+
assert_equal(4, Dir.entries("./db/migrate").size)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_create_migrations_raises_when_malformed_inpout
|
55
|
+
Tempfile.create do |file|
|
56
|
+
file.write(<<~INDEXES)
|
57
|
+
add an index on users. - foreign keys are often used in database lookups and should be indexed for performance reasons
|
58
|
+
INDEXES
|
59
|
+
file.flush
|
60
|
+
|
61
|
+
assert_raises(RuntimeError) do
|
62
|
+
capture_io do
|
63
|
+
ActiveRecordDoctor::AddIndexesGenerator.start([file.path])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_create_migrations_skips_blank_lines
|
70
|
+
Dir.mktmpdir do |dir|
|
71
|
+
Dir.chdir(dir)
|
72
|
+
|
73
|
+
path = File.join(dir, "indexes.txt")
|
74
|
+
File.write(path, "\n")
|
75
|
+
|
76
|
+
capture_io do
|
77
|
+
Time.stub(:now, TIMESTAMP) do
|
78
|
+
# Not raising an exception is considered success.
|
79
|
+
ActiveRecordDoctor::AddIndexesGenerator.start([path])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_create_migrations_truncates_long_index_names
|
86
|
+
# Both the table and column names must be quite long. Otherwise, the
|
87
|
+
# we might reach table or column name length limits and fail to generate an
|
88
|
+
# index name that's long enough.
|
89
|
+
create_table(:organizations_migrated_from_legacy_app) do |t|
|
90
|
+
t.integer :legacy_owner_id_compatible_with_v1_to_v8
|
91
|
+
end
|
92
|
+
|
93
|
+
Dir.mktmpdir do |dir|
|
94
|
+
Dir.chdir(dir)
|
95
|
+
|
96
|
+
path = File.join(dir, "indexes.txt")
|
97
|
+
File.write(path, <<~INDEXES)
|
98
|
+
add an index on organizations_migrated_from_legacy_app.legacy_owner_id_compatible_with_v1_to_v8 - foreign keys are often used in database lookups and should be indexed for performance reasons
|
99
|
+
INDEXES
|
100
|
+
|
101
|
+
capture_io do
|
102
|
+
Time.stub(:now, TIMESTAMP) do
|
103
|
+
# If no exceptions are raised then we consider this to be a success.
|
104
|
+
ActiveRecordDoctor::AddIndexesGenerator.start([path])
|
105
|
+
|
106
|
+
load(File.join(
|
107
|
+
"db",
|
108
|
+
"migrate",
|
109
|
+
"20210201131530_index_foreign_keys_in_organizations_migrated_from_legacy_app.rb"
|
110
|
+
))
|
111
|
+
::IndexForeignKeysInOrganizationsMigratedFromLegacyApp.migrate(:up)
|
112
|
+
|
113
|
+
::Object.send(:remove_const, :IndexForeignKeysInOrganizationsMigratedFromLegacyApp)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def assert_indexes(expected_indexes)
|
122
|
+
actual_indexes =
|
123
|
+
ActiveRecord::Base.connection.tables.map do |table|
|
124
|
+
ActiveRecord::Base.connection.indexes(table).map do |index|
|
125
|
+
[index.table, index.columns]
|
126
|
+
end
|
127
|
+
end.flatten(1)
|
128
|
+
|
129
|
+
assert_equal(expected_indexes.sort, actual_indexes.sort)
|
130
|
+
end
|
131
|
+
end
|
data/test/model_factory.rb
CHANGED
@@ -1,7 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ModelFactory
|
4
|
-
def
|
4
|
+
def create_table(*args, &block)
|
5
|
+
ModelFactory.create_table(*args, &block)
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_model(*args, &block)
|
9
|
+
ModelFactory.create_model(*args, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def cleanup_models
|
13
|
+
ModelFactory.cleanup_models
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.cleanup_models
|
5
17
|
delete_models
|
6
18
|
drop_all_tables
|
7
19
|
GC.start
|
@@ -10,27 +22,53 @@ module ModelFactory
|
|
10
22
|
def self.drop_all_tables
|
11
23
|
connection = ActiveRecord::Base.connection
|
12
24
|
loop do
|
13
|
-
before = connection.
|
25
|
+
before = connection.data_sources.size
|
14
26
|
break if before.zero?
|
15
27
|
|
16
|
-
|
17
|
-
|
28
|
+
try_drop_all_tables_and_views(connection)
|
29
|
+
remaining_data_sources = connection.data_sources
|
30
|
+
after = remaining_data_sources.size
|
18
31
|
|
32
|
+
# rubocop:disable Style/Next
|
19
33
|
if before == after
|
20
|
-
raise(
|
34
|
+
raise(<<~ERROR)
|
35
|
+
Cannot delete temporary tables - most likely due to failing constraints. Remaining tables and views:
|
36
|
+
|
37
|
+
#{remaining_data_sources.join("\n")}
|
38
|
+
ERROR
|
39
|
+
end
|
40
|
+
# rubocop:enable Style/Next
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.try_drop_all_tables_and_views(connection)
|
45
|
+
connection.data_sources.each do |table_name|
|
46
|
+
try_drop_table(connection, table_name) || try_drop_view(connection, table_name)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.try_drop_table(connection, table_name)
|
51
|
+
ActiveRecord::Migration.suppress_messages do
|
52
|
+
begin
|
53
|
+
connection.drop_table(table_name, force: :cascade)
|
54
|
+
true
|
55
|
+
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::StatementInvalid
|
56
|
+
# The table cannot be dropped due to foreign key constraints so
|
57
|
+
# we'll try to drop it on another attempt.
|
58
|
+
false
|
21
59
|
end
|
22
60
|
end
|
23
61
|
end
|
24
62
|
|
25
|
-
def self.
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
63
|
+
def self.try_drop_view(connection, view_name)
|
64
|
+
ActiveRecord::Migration.suppress_messages do
|
65
|
+
begin
|
66
|
+
connection.execute("DROP VIEW #{view_name}")
|
67
|
+
true
|
68
|
+
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::StatementInvalid
|
69
|
+
# The table cannot be dropped due to foreign key constraints so
|
70
|
+
# we'll try to drop it on another attempt.
|
71
|
+
false
|
34
72
|
end
|
35
73
|
end
|
36
74
|
end
|
@@ -39,23 +77,35 @@ module ModelFactory
|
|
39
77
|
Models.empty
|
40
78
|
end
|
41
79
|
|
42
|
-
def self.create_table(table_name, &block)
|
80
|
+
def self.create_table(table_name, options = {}, &block)
|
43
81
|
table_name = table_name.to_sym
|
44
82
|
ActiveRecord::Migration.suppress_messages do
|
45
|
-
ActiveRecord::Migration.create_table(table_name, &block)
|
83
|
+
ActiveRecord::Migration.create_table(table_name, **options, &block)
|
46
84
|
end
|
47
|
-
|
48
85
|
# Return a proxy object allowing the caller to chain #create_model
|
49
86
|
# right after creating a table so that it can be followed by the model
|
50
87
|
# definition.
|
51
88
|
ModelDefinitionProxy.new(table_name)
|
52
89
|
end
|
53
90
|
|
54
|
-
def self.create_model(
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
91
|
+
def self.create_model(model_name, base_class = ActiveRecord::Base, &block)
|
92
|
+
model_name = model_name.to_sym
|
93
|
+
|
94
|
+
# Normally, when a class is defined via `class MyClass < MySuperclass` the
|
95
|
+
# .name class method returns the name of the class when called from within
|
96
|
+
# the class body. However, anonymous classes defined via Class.new DO NOT
|
97
|
+
# HAVE NAMES. They're assigned names when they're assigned to a constant.
|
98
|
+
# If we evaluated the class body, passed via block here, in the class
|
99
|
+
# definition below then some macros would break
|
100
|
+
# (e.g. has_and_belongs_to_many) due to nil name.
|
101
|
+
#
|
102
|
+
# We solve the problem by defining an empty model class first, assigning to
|
103
|
+
# a constant to ensure a name is assigned, and then reopening the class to
|
104
|
+
# give it a non-trivial body.
|
105
|
+
klass = Class.new(base_class)
|
106
|
+
Models.const_set(model_name, klass)
|
107
|
+
|
108
|
+
klass.class_eval(&block) if block_given?
|
59
109
|
end
|
60
110
|
|
61
111
|
class ModelDefinitionProxy
|
@@ -64,7 +114,7 @@ module ModelFactory
|
|
64
114
|
end
|
65
115
|
|
66
116
|
def create_model(&block)
|
67
|
-
ModelFactory.create_model(@table_name, &block)
|
117
|
+
ModelFactory.create_model(@table_name.to_s.classify, &block)
|
68
118
|
end
|
69
119
|
end
|
70
120
|
|