active_record_doctor 1.10.0 → 1.11.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 +1 -1
- data/lib/active_record_doctor/detectors/base.rb +180 -50
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +24 -27
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -5
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +63 -21
- 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 +5 -11
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +1 -1
- 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 +2 -4
- 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/version.rb +1 -1
- data/lib/active_record_doctor.rb +3 -0
- data/test/active_record_doctor/detectors/disable_test.rb +1 -1
- 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 +136 -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 +36 -36
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
- data/test/active_record_doctor/runner_test.rb +18 -19
- data/test/setup.rb +10 -6
- metadata +19 -4
- data/test/model_factory.rb +0 -128
@@ -3,17 +3,17 @@
|
|
3
3
|
class ActiveRecordDoctor::Detectors::UndefinedTableReferencesTest < Minitest::Test
|
4
4
|
def test_model_backed_by_table
|
5
5
|
create_table(:users) do
|
6
|
-
end.
|
6
|
+
end.define_model do
|
7
7
|
end
|
8
8
|
|
9
9
|
refute_problems
|
10
10
|
end
|
11
11
|
|
12
12
|
def test_model_backed_by_non_existent_table
|
13
|
-
|
13
|
+
define_model(:User)
|
14
14
|
|
15
15
|
assert_problems(<<~OUTPUT)
|
16
|
-
|
16
|
+
TransientRecord::Models::User references a non-existent table or view named users
|
17
17
|
OUTPUT
|
18
18
|
end
|
19
19
|
|
@@ -21,22 +21,20 @@ class ActiveRecordDoctor::Detectors::UndefinedTableReferencesTest < Minitest::Te
|
|
21
21
|
# We replace the underlying table with a view. The view doesn't have to be
|
22
22
|
# backed by an actual table - it can simply return a predefined tuple.
|
23
23
|
ActiveRecord::Base.connection.execute("CREATE VIEW users AS SELECT 1")
|
24
|
-
|
24
|
+
define_model(:User)
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
ActiveRecord::Base.connection.execute("DROP VIEW users")
|
30
|
-
end
|
26
|
+
refute_problems
|
27
|
+
ensure
|
28
|
+
ActiveRecord::Base.connection.execute("DROP VIEW users")
|
31
29
|
end
|
32
30
|
|
33
31
|
def test_config_ignore_tables
|
34
|
-
|
32
|
+
define_model(:User)
|
35
33
|
|
36
34
|
config_file(<<-CONFIG)
|
37
35
|
ActiveRecordDoctor.configure do |config|
|
38
36
|
config.detector :undefined_table_references,
|
39
|
-
ignore_models: ["
|
37
|
+
ignore_models: ["TransientRecord::Models::User"]
|
40
38
|
end
|
41
39
|
CONFIG
|
42
40
|
|
@@ -44,11 +42,11 @@ class ActiveRecordDoctor::Detectors::UndefinedTableReferencesTest < Minitest::Te
|
|
44
42
|
end
|
45
43
|
|
46
44
|
def test_global_ignore_tables
|
47
|
-
|
45
|
+
define_model(:User)
|
48
46
|
|
49
47
|
config_file(<<-CONFIG)
|
50
48
|
ActiveRecordDoctor.configure do |config|
|
51
|
-
config.global :ignore_models, ["
|
49
|
+
config.global :ignore_models, ["TransientRecord::Models::User"]
|
52
50
|
end
|
53
51
|
CONFIG
|
54
52
|
|
@@ -1,42 +1,41 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class ActiveRecordDoctor::RunnerTest < Minitest::Test
|
4
|
-
def
|
5
|
-
io = StringIO.new
|
6
|
-
runner = ActiveRecordDoctor::Runner.new(
|
4
|
+
def setup
|
5
|
+
@io = StringIO.new
|
6
|
+
@runner = ActiveRecordDoctor::Runner.new(
|
7
|
+
config: load_config,
|
8
|
+
logger: ActiveRecordDoctor::Logger::Dummy.new,
|
9
|
+
io: @io
|
10
|
+
)
|
11
|
+
end
|
7
12
|
|
13
|
+
def test_run_one_raises_on_unknown_detectors
|
8
14
|
assert_raises(KeyError) do
|
9
|
-
runner.run_one(:performance_issues)
|
15
|
+
@runner.run_one(:performance_issues)
|
10
16
|
end
|
11
17
|
end
|
12
18
|
|
13
19
|
def test_run_all_returns_true_when_no_errors
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
assert(runner.run_all)
|
18
|
-
assert(io.string.blank?)
|
20
|
+
assert(@runner.run_all)
|
21
|
+
assert(@io.string.blank?)
|
19
22
|
end
|
20
23
|
|
21
24
|
def test_run_all_returns_false_when_errors
|
22
25
|
# Create a model without its underlying table to trigger an error.
|
23
|
-
|
24
|
-
|
25
|
-
io = StringIO.new
|
26
|
-
runner = ActiveRecordDoctor::Runner.new(load_config, io)
|
26
|
+
define_model(:User)
|
27
27
|
|
28
|
-
refute(runner.run_all)
|
29
|
-
refute(io.string.blank?)
|
28
|
+
refute(@runner.run_all)
|
29
|
+
refute(@io.string.blank?)
|
30
30
|
end
|
31
31
|
|
32
32
|
def test_help_prints_help
|
33
33
|
ActiveRecordDoctor.detectors.each do |name, _|
|
34
|
-
io
|
35
|
-
runner = ActiveRecordDoctor::Runner.new(load_config, io)
|
34
|
+
@io.truncate(0)
|
36
35
|
|
37
|
-
runner.help(name)
|
36
|
+
@runner.help(name)
|
38
37
|
|
39
|
-
refute(io.string.blank?, "expected help for #{name}")
|
38
|
+
refute(@io.string.blank?, "expected help for #{name}")
|
40
39
|
end
|
41
40
|
end
|
42
41
|
end
|
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.11.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-03-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -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,6 +152,9 @@ 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
|
@@ -164,7 +181,6 @@ files:
|
|
164
181
|
- test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb
|
165
182
|
- test/active_record_doctor/runner_test.rb
|
166
183
|
- test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb
|
167
|
-
- test/model_factory.rb
|
168
184
|
- test/setup.rb
|
169
185
|
homepage: https://github.com/gregnavis/active_record_doctor
|
170
186
|
licenses:
|
@@ -208,5 +224,4 @@ test_files:
|
|
208
224
|
- test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb
|
209
225
|
- test/active_record_doctor/runner_test.rb
|
210
226
|
- test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb
|
211
|
-
- test/model_factory.rb
|
212
227
|
- 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
|