active_record_doctor 1.10.0 → 1.12.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 +15 -15
- data/lib/active_record_doctor/detectors/base.rb +194 -53
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +36 -34
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -5
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +87 -37
- data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +7 -10
- data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +16 -9
- data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +2 -4
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +13 -11
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +14 -7
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +70 -35
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +4 -4
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +2 -2
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +5 -13
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +35 -11
- data/lib/active_record_doctor/logger/dummy.rb +11 -0
- data/lib/active_record_doctor/logger/hierarchical.rb +22 -0
- data/lib/active_record_doctor/logger.rb +6 -0
- data/lib/active_record_doctor/rake/task.rb +10 -1
- data/lib/active_record_doctor/runner.rb +8 -3
- data/lib/active_record_doctor/utils.rb +21 -0
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +5 -0
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +14 -14
- data/test/active_record_doctor/detectors/disable_test.rb +1 -1
- data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +59 -6
- data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +7 -7
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +175 -57
- data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +16 -14
- data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +35 -1
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +46 -23
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +55 -27
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +216 -47
- data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +5 -0
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
- data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +39 -1
- data/test/active_record_doctor/runner_test.rb +18 -19
- data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +16 -6
- data/test/setup.rb +10 -6
- metadata +23 -7
- data/test/model_factory.rb +0 -128
@@ -8,6 +8,10 @@ class ActiveRecordDoctor::AddIndexesGeneratorTest < Minitest::Test
|
|
8
8
|
TIMESTAMP = Time.new(2021, 2, 1, 13, 15, 30)
|
9
9
|
|
10
10
|
def test_create_migrations
|
11
|
+
create_table(:notes) do |t|
|
12
|
+
t.integer :notable_id, null: false
|
13
|
+
t.string :notable_type, null: false
|
14
|
+
end
|
11
15
|
create_table(:users) do |t|
|
12
16
|
t.integer :organization_id, null: false
|
13
17
|
t.integer :account_id, null: false
|
@@ -21,9 +25,10 @@ class ActiveRecordDoctor::AddIndexesGeneratorTest < Minitest::Test
|
|
21
25
|
|
22
26
|
path = File.join(dir, "indexes.txt")
|
23
27
|
File.write(path, <<~INDEXES)
|
24
|
-
add an index on users
|
25
|
-
add an index on users
|
26
|
-
add an index on organizations
|
28
|
+
add an index on users(organization_id) - foreign keys are often used in database lookups and should be indexed for performance reasons
|
29
|
+
add an index on users(account_id) - foreign keys are often used in database lookups and should be indexed for performance reasons
|
30
|
+
add an index on organizations(owner_id) - foreign keys are often used in database lookups and should be indexed for performance reasons
|
31
|
+
add an index on notes(notable_type, notable_id) - foreign keys are often used in database lookups and should be indexed for performance reasons
|
27
32
|
INDEXES
|
28
33
|
|
29
34
|
capture_io do
|
@@ -36,25 +41,30 @@ class ActiveRecordDoctor::AddIndexesGeneratorTest < Minitest::Test
|
|
36
41
|
load(File.join("db", "migrate", "20210201131531_index_foreign_keys_in_organizations.rb"))
|
37
42
|
IndexForeignKeysInOrganizations.migrate(:up)
|
38
43
|
|
44
|
+
load(File.join("db", "migrate", "20210201131532_index_foreign_keys_in_notes.rb"))
|
45
|
+
IndexForeignKeysInNotes.migrate(:up)
|
46
|
+
|
39
47
|
::Object.send(:remove_const, :IndexForeignKeysInUsers)
|
40
48
|
::Object.send(:remove_const, :IndexForeignKeysInOrganizations)
|
49
|
+
::Object.send(:remove_const, :IndexForeignKeysInNotes)
|
41
50
|
end
|
42
51
|
end
|
43
52
|
|
44
53
|
assert_indexes([
|
54
|
+
["notes", ["notable_type", "notable_id"]],
|
45
55
|
["users", ["organization_id"]],
|
46
56
|
["users", ["account_id"]],
|
47
57
|
["organizations", ["owner_id"]]
|
48
58
|
])
|
49
59
|
|
50
|
-
assert_equal(
|
60
|
+
assert_equal(5, Dir.entries("./db/migrate").size)
|
51
61
|
end
|
52
62
|
end
|
53
63
|
|
54
64
|
def test_create_migrations_raises_when_malformed_inpout
|
55
65
|
Tempfile.create do |file|
|
56
66
|
file.write(<<~INDEXES)
|
57
|
-
add an index on users
|
67
|
+
add an index on users() - foreign keys are often used in database lookups and should be indexed for performance reasons
|
58
68
|
INDEXES
|
59
69
|
file.flush
|
60
70
|
|
@@ -95,7 +105,7 @@ class ActiveRecordDoctor::AddIndexesGeneratorTest < Minitest::Test
|
|
95
105
|
|
96
106
|
path = File.join(dir, "indexes.txt")
|
97
107
|
File.write(path, <<~INDEXES)
|
98
|
-
add an index on organizations_migrated_from_legacy_app
|
108
|
+
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
109
|
INDEXES
|
100
110
|
|
101
111
|
capture_io do
|
data/test/setup.rb
CHANGED
@@ -33,7 +33,7 @@ require "active_record_doctor"
|
|
33
33
|
require "minitest"
|
34
34
|
require "minitest/autorun"
|
35
35
|
require "minitest/fork_executor"
|
36
|
-
|
36
|
+
require "transient_record"
|
37
37
|
|
38
38
|
# Filter out Minitest backtrace while allowing backtrace from other libraries
|
39
39
|
# to be shown.
|
@@ -44,11 +44,11 @@ Minitest.parallel_executor = Minitest::ForkExecutor.new
|
|
44
44
|
|
45
45
|
# Prepare the test class.
|
46
46
|
class Minitest::Test
|
47
|
-
include
|
47
|
+
include TransientRecord
|
48
48
|
|
49
49
|
def setup
|
50
50
|
# Delete remnants (models and tables) of previous test case runs.
|
51
|
-
|
51
|
+
TransientRecord.cleanup
|
52
52
|
end
|
53
53
|
|
54
54
|
def teardown
|
@@ -61,7 +61,7 @@ class Minitest::Test
|
|
61
61
|
|
62
62
|
# Ensure all remnants of previous test runs, most likely in form of tables,
|
63
63
|
# are removed.
|
64
|
-
|
64
|
+
TransientRecord.cleanup
|
65
65
|
end
|
66
66
|
|
67
67
|
private
|
@@ -93,7 +93,11 @@ class Minitest::Test
|
|
93
93
|
|
94
94
|
def run_detector
|
95
95
|
io = StringIO.new
|
96
|
-
runner = ActiveRecordDoctor::Runner.new(
|
96
|
+
runner = ActiveRecordDoctor::Runner.new(
|
97
|
+
config: load_config,
|
98
|
+
logger: ActiveRecordDoctor::Logger::Dummy.new,
|
99
|
+
io: io
|
100
|
+
)
|
97
101
|
success = runner.run_one(detector_name)
|
98
102
|
[success, io.string]
|
99
103
|
end
|
@@ -115,6 +119,6 @@ class Minitest::Test
|
|
115
119
|
end
|
116
120
|
|
117
121
|
def sort_lines(string)
|
118
|
-
string.split("\n").sort
|
122
|
+
string.split("\n").sort
|
119
123
|
end
|
120
124
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record_doctor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg Navis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-07-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 1.
|
61
|
+
version: 1.3.0
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 1.
|
68
|
+
version: 1.3.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: railties
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: 12.3.3
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: transient_record
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.0.0.rc1
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 1.0.0.rc1
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: rubocop
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -138,10 +152,14 @@ files:
|
|
138
152
|
- lib/active_record_doctor/detectors/unindexed_foreign_keys.rb
|
139
153
|
- lib/active_record_doctor/errors.rb
|
140
154
|
- lib/active_record_doctor/help.rb
|
155
|
+
- lib/active_record_doctor/logger.rb
|
156
|
+
- lib/active_record_doctor/logger/dummy.rb
|
157
|
+
- lib/active_record_doctor/logger/hierarchical.rb
|
141
158
|
- lib/active_record_doctor/printers.rb
|
142
159
|
- lib/active_record_doctor/railtie.rb
|
143
160
|
- lib/active_record_doctor/rake/task.rb
|
144
161
|
- lib/active_record_doctor/runner.rb
|
162
|
+
- lib/active_record_doctor/utils.rb
|
145
163
|
- lib/active_record_doctor/version.rb
|
146
164
|
- lib/generators/active_record_doctor/add_indexes/USAGE
|
147
165
|
- lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb
|
@@ -164,7 +182,6 @@ files:
|
|
164
182
|
- test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb
|
165
183
|
- test/active_record_doctor/runner_test.rb
|
166
184
|
- test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb
|
167
|
-
- test/model_factory.rb
|
168
185
|
- test/setup.rb
|
169
186
|
homepage: https://github.com/gregnavis/active_record_doctor
|
170
187
|
licenses:
|
@@ -185,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
185
202
|
- !ruby/object:Gem::Version
|
186
203
|
version: '0'
|
187
204
|
requirements: []
|
188
|
-
rubygems_version: 3.
|
205
|
+
rubygems_version: 3.4.10
|
189
206
|
signing_key:
|
190
207
|
specification_version: 4
|
191
208
|
summary: Identify database issues before they hit production.
|
@@ -208,5 +225,4 @@ test_files:
|
|
208
225
|
- test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb
|
209
226
|
- test/active_record_doctor/runner_test.rb
|
210
227
|
- test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb
|
211
|
-
- test/model_factory.rb
|
212
228
|
- test/setup.rb
|
data/test/model_factory.rb
DELETED
@@ -1,128 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ModelFactory
|
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
|
17
|
-
delete_models
|
18
|
-
drop_all_tables
|
19
|
-
GC.start
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.drop_all_tables
|
23
|
-
connection = ActiveRecord::Base.connection
|
24
|
-
loop do
|
25
|
-
before = connection.data_sources.size
|
26
|
-
break if before.zero?
|
27
|
-
|
28
|
-
try_drop_all_tables_and_views(connection)
|
29
|
-
remaining_data_sources = connection.data_sources
|
30
|
-
after = remaining_data_sources.size
|
31
|
-
|
32
|
-
# rubocop:disable Style/Next
|
33
|
-
if before == after
|
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
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
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
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def self.delete_models
|
77
|
-
Models.empty
|
78
|
-
end
|
79
|
-
|
80
|
-
def self.create_table(table_name, options = {}, &block)
|
81
|
-
table_name = table_name.to_sym
|
82
|
-
ActiveRecord::Migration.suppress_messages do
|
83
|
-
ActiveRecord::Migration.create_table(table_name, **options, &block)
|
84
|
-
end
|
85
|
-
# Return a proxy object allowing the caller to chain #create_model
|
86
|
-
# right after creating a table so that it can be followed by the model
|
87
|
-
# definition.
|
88
|
-
ModelDefinitionProxy.new(table_name)
|
89
|
-
end
|
90
|
-
|
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?
|
109
|
-
end
|
110
|
-
|
111
|
-
class ModelDefinitionProxy
|
112
|
-
def initialize(table_name)
|
113
|
-
@table_name = table_name
|
114
|
-
end
|
115
|
-
|
116
|
-
def create_model(&block)
|
117
|
-
ModelFactory.create_model(@table_name.to_s.classify, &block)
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
module Models
|
122
|
-
def self.empty
|
123
|
-
constants.each do |name|
|
124
|
-
remove_const(name)
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|