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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/active_record_doctor/detectors/base.rb +180 -50
  4. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +24 -27
  5. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -5
  6. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +63 -21
  7. data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +7 -10
  8. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +16 -9
  9. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +2 -4
  10. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +13 -11
  11. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +14 -7
  12. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +5 -11
  13. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +1 -1
  14. data/lib/active_record_doctor/detectors/undefined_table_references.rb +2 -2
  15. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +5 -13
  16. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +2 -4
  17. data/lib/active_record_doctor/logger/dummy.rb +11 -0
  18. data/lib/active_record_doctor/logger/hierarchical.rb +22 -0
  19. data/lib/active_record_doctor/logger.rb +6 -0
  20. data/lib/active_record_doctor/rake/task.rb +10 -1
  21. data/lib/active_record_doctor/runner.rb +8 -3
  22. data/lib/active_record_doctor/version.rb +1 -1
  23. data/lib/active_record_doctor.rb +3 -0
  24. data/test/active_record_doctor/detectors/disable_test.rb +1 -1
  25. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +7 -7
  26. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +136 -57
  27. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +16 -14
  28. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +35 -1
  29. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +46 -23
  30. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +55 -27
  31. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +36 -36
  32. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
  33. data/test/active_record_doctor/runner_test.rb +18 -19
  34. data/test/setup.rb +10 -6
  35. metadata +19 -4
  36. 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.create_model do
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
- create_model(:User)
13
+ define_model(:User)
14
14
 
15
15
  assert_problems(<<~OUTPUT)
16
- ModelFactory::Models::User references a non-existent table or view named users
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
- create_model(:User)
24
+ define_model(:User)
25
25
 
26
- begin
27
- refute_problems
28
- ensure
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
- create_model(:User)
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: ["ModelFactory::Models::User"]
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
- create_model(:User)
45
+ define_model(:User)
48
46
 
49
47
  config_file(<<-CONFIG)
50
48
  ActiveRecordDoctor.configure do |config|
51
- config.global :ignore_models, ["ModelFactory::Models::User"]
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 test_run_one_raises_on_unknown_detectors
5
- io = StringIO.new
6
- runner = ActiveRecordDoctor::Runner.new(load_config, io)
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
- io = StringIO.new
15
- runner = ActiveRecordDoctor::Runner.new(load_config, io)
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
- create_model(:User)
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 = StringIO.new
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
- require_relative "model_factory"
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 ModelFactory
47
+ include TransientRecord
48
48
 
49
49
  def setup
50
50
  # Delete remnants (models and tables) of previous test case runs.
51
- cleanup_models
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
- cleanup_models
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(load_config, io)
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.join("\n")
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.10.0
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: 2022-07-07 00:00:00.000000000 Z
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
@@ -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