active_record_doctor 1.10.0 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
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