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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -15
  3. data/lib/active_record_doctor/detectors/base.rb +194 -53
  4. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +36 -34
  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 +87 -37
  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 +70 -35
  13. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +4 -4
  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 +35 -11
  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/utils.rb +21 -0
  23. data/lib/active_record_doctor/version.rb +1 -1
  24. data/lib/active_record_doctor.rb +5 -0
  25. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +14 -14
  26. data/test/active_record_doctor/detectors/disable_test.rb +1 -1
  27. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +59 -6
  28. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +7 -7
  29. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +175 -57
  30. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +16 -14
  31. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +35 -1
  32. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +46 -23
  33. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +55 -27
  34. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +216 -47
  35. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +5 -0
  36. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
  37. data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +39 -1
  38. data/test/active_record_doctor/runner_test.rb +18 -19
  39. data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +16 -6
  40. data/test/setup.rb +10 -6
  41. metadata +23 -7
  42. 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.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
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(4, Dir.entries("./db/migrate").size)
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. - foreign keys are often used in database lookups and should be indexed for performance reasons
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.legacy_owner_id_compatible_with_v1_to_v8 - foreign keys are often used in database lookups and should be indexed for performance reasons
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
- 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.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: 2022-07-07 00:00:00.000000000 Z
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.1.4
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.1.4
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.2.33
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
@@ -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