active_record_doctor 1.8.0 → 1.10.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +316 -54
  3. data/lib/active_record_doctor/config/default.rb +76 -0
  4. data/lib/active_record_doctor/config/loader.rb +137 -0
  5. data/lib/active_record_doctor/config.rb +14 -0
  6. data/lib/active_record_doctor/detectors/base.rb +142 -21
  7. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +59 -48
  8. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +31 -23
  9. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +102 -35
  10. data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +63 -0
  11. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +45 -0
  12. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +32 -23
  13. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +41 -28
  14. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +29 -23
  15. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +92 -32
  16. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +45 -0
  17. data/lib/active_record_doctor/detectors/undefined_table_references.rb +17 -20
  18. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +43 -18
  19. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +31 -20
  20. data/lib/active_record_doctor/detectors.rb +12 -4
  21. data/lib/active_record_doctor/errors.rb +226 -0
  22. data/lib/active_record_doctor/help.rb +39 -0
  23. data/lib/active_record_doctor/rake/task.rb +78 -0
  24. data/lib/active_record_doctor/runner.rb +41 -0
  25. data/lib/active_record_doctor/version.rb +1 -1
  26. data/lib/active_record_doctor.rb +8 -3
  27. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +34 -21
  28. data/lib/tasks/active_record_doctor.rake +9 -18
  29. data/test/active_record_doctor/config/loader_test.rb +120 -0
  30. data/test/active_record_doctor/config_test.rb +116 -0
  31. data/test/active_record_doctor/detectors/disable_test.rb +30 -0
  32. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +165 -8
  33. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +48 -5
  34. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +288 -12
  35. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +105 -0
  36. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +82 -0
  37. data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +50 -4
  38. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +172 -24
  39. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +111 -14
  40. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +223 -10
  41. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +72 -0
  42. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +34 -21
  43. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +118 -8
  44. data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +56 -4
  45. data/test/active_record_doctor/runner_test.rb +42 -0
  46. data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +131 -0
  47. data/test/model_factory.rb +73 -23
  48. data/test/setup.rb +65 -71
  49. metadata +43 -7
  50. data/lib/active_record_doctor/printers/io_printer.rb +0 -133
  51. data/lib/active_record_doctor/task.rb +0 -28
  52. data/test/active_record_doctor/printers/io_printer_test.rb +0 -33
@@ -4,30 +4,27 @@ require "active_record_doctor/detectors/base"
4
4
 
5
5
  module ActiveRecordDoctor
6
6
  module Detectors
7
- # Find models referencing non-existent database tables or views.
8
- class UndefinedTableReferences < Base
9
- @description = "Detect models referencing undefined tables or views"
7
+ class UndefinedTableReferences < Base # :nodoc:
8
+ @description = "detect models referencing undefined tables or views"
9
+ @config = {
10
+ ignore_models: {
11
+ description: "models whose underlying tables should not be checked for existence",
12
+ global: true
13
+ }
14
+ }
10
15
 
11
- def run
12
- eager_load!
16
+ private
13
17
 
14
- # If we can't list views due to old Rails version or unsupported
15
- # database then existing_views is nil. We inform the caller we haven't
16
- # consulted views so that it can display an appropriate warning.
17
- existing_views = views
18
+ def message(model:, table:)
19
+ "#{model} references a non-existent table or view named #{table}"
20
+ end
18
21
 
19
- offending_models = models.select do |model|
20
- model.table_name.present? &&
21
- !tables.include?(model.table_name) &&
22
- (
23
- existing_views.nil? ||
24
- !existing_views.include?(model.table_name)
25
- )
26
- end.map do |model|
27
- [model.name, model.table_name]
28
- end
22
+ def detect
23
+ models(except: config(:ignore_models)).each do |model|
24
+ next if model.table_exists? || views.include?(model.table_name)
29
25
 
30
- problems(offending_models, views_checked: !existing_views.nil?)
26
+ problem!(model: model.name, table: model.table_name)
27
+ end
31
28
  end
32
29
  end
33
30
  end
@@ -4,25 +4,50 @@ require "active_record_doctor/detectors/base"
4
4
 
5
5
  module ActiveRecordDoctor
6
6
  module Detectors
7
- # Find unindexed deleted_at columns.
8
- class UnindexedDeletedAt < Base
9
- PATTERN = [
10
- "deleted_at",
11
- "discarded_at"
12
- ].join("|").freeze
13
-
14
- @description = "Detect unindexed deleted_at columns"
15
-
16
- def run
17
- problems(connection.tables.select do |table|
18
- connection.columns(table).any? { |column| column.name =~ /^#{PATTERN}$/ }
19
- end.flat_map do |table|
20
- connection.indexes(table).reject do |index|
21
- index.where =~ /\b#{PATTERN}\s+IS\s+NULL\b/i
22
- end.map do |index|
23
- index.name
7
+ class UnindexedDeletedAt < Base # :nodoc:
8
+ @description = "detect indexes that exclude deletion timestamp columns"
9
+ @config = {
10
+ ignore_tables: {
11
+ description: "tables whose indexes should not be checked",
12
+ global: true
13
+ },
14
+ ignore_columns: {
15
+ description: "specific columns, written as table.column, that should not be reported as unindexed"
16
+ },
17
+ ignore_indexes: {
18
+ description: "specific indexes that should not be reported as excluding a timestamp column"
19
+ },
20
+ column_names: {
21
+ description: "deletion timestamp column names"
22
+ }
23
+ }
24
+
25
+ private
26
+
27
+ def message(index:, column_name:)
28
+ # rubocop:disable Layout/LineLength
29
+ "consider adding `WHERE #{column_name} IS NULL` or `WHERE #{column_name} IS NOT NULL` to #{index} - a partial index can speed lookups of soft-deletable models"
30
+ # rubocop:enable Layout/LineLength
31
+ end
32
+
33
+ def detect
34
+ tables(except: config(:ignore_tables)).each do |table|
35
+ timestamp_columns = connection.columns(table).reject do |column|
36
+ config(:ignore_columns).include?("#{table}.#{column.name}")
37
+ end.select do |column|
38
+ config(:column_names).include?(column.name)
39
+ end
40
+
41
+ next if timestamp_columns.empty?
42
+
43
+ timestamp_columns.each do |timestamp_column|
44
+ indexes(table, except: config(:ignore_indexes)).each do |index|
45
+ next if index.where =~ /\b#{timestamp_column.name}\s+IS\s+(NOT\s+)?NULL\b/i
46
+
47
+ problem!(index: index.name, column_name: timestamp_column.name)
48
+ end
24
49
  end
25
- end)
50
+ end
26
51
  end
27
52
  end
28
53
  end
@@ -4,29 +4,40 @@ require "active_record_doctor/detectors/base"
4
4
 
5
5
  module ActiveRecordDoctor
6
6
  module Detectors
7
- # Find foreign keys that lack indexes (usually recommended for performance reasons).
8
- class UnindexedForeignKeys < Base
9
- @description = "Detect foreign keys without an index on them"
10
-
11
- def run
12
- problems(hash_from_pairs(tables.reject do |table|
13
- table == "schema_migrations"
14
- end.map do |table|
15
- [
16
- table,
17
- connection.columns(table).select do |column|
18
- foreign_key?(column) &&
19
- !indexed?(table, column) &&
20
- !indexed_as_polymorphic?(table, column)
21
- end.map(&:name)
22
- ]
23
- end.reject do |_table, columns|
24
- columns.empty?
25
- end))
26
- end
7
+ class UnindexedForeignKeys < Base # :nodoc:
8
+ @description = "detect unindexed foreign keys"
9
+ @config = {
10
+ ignore_tables: {
11
+ description: "tables whose foreign keys should not be checked",
12
+ global: true
13
+ },
14
+ ignore_columns: {
15
+ description: "columns, written as table.column, that should not be checked"
16
+ }
17
+ }
27
18
 
28
19
  private
29
20
 
21
+ def message(table:, column:)
22
+ # rubocop:disable Layout/LineLength
23
+ "add an index on #{table}.#{column} - foreign keys are often used in database lookups and should be indexed for performance reasons"
24
+ # rubocop:enable Layout/LineLength
25
+ end
26
+
27
+ def detect
28
+ tables(except: config(:ignore_tables)).each do |table|
29
+ connection.columns(table).each do |column|
30
+ next if config(:ignore_columns).include?("#{table}.#{column.name}")
31
+
32
+ next unless foreign_key?(column)
33
+ next if indexed?(table, column)
34
+ next if indexed_as_polymorphic?(table, column)
35
+
36
+ problem!(table: table, column: column.name)
37
+ end
38
+ end
39
+ end
40
+
30
41
  def foreign_key?(column)
31
42
  column.name.end_with?("_id")
32
43
  end
@@ -3,11 +3,19 @@
3
3
  require "active_support"
4
4
  require "active_support/core_ext/class/subclasses"
5
5
 
6
- module ActiveRecordDoctor
6
+ module ActiveRecordDoctor # :nodoc:
7
+ def self.detectors
8
+ @detectors ||=
9
+ begin
10
+ detectors = {}
11
+ ActiveRecordDoctor::Detectors::Base.subclasses.each do |detector|
12
+ detectors[detector.underscored_name] = detector
13
+ end
14
+ detectors
15
+ end
16
+ end
17
+
7
18
  # Container module for all detectors, implemented as separate classes.
8
19
  module Detectors
9
- def self.all
10
- ActiveRecordDoctor::Detectors::Base.subclasses
11
- end
12
20
  end
13
21
  end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDoctor # :nodoc:
4
+ def self.handle_exception
5
+ yield
6
+ rescue ActiveRecordDoctor::Error => e
7
+ $stderr.puts(e.user_message)
8
+ exit(1)
9
+ end
10
+
11
+ # Generic active_record_doctor exception class.
12
+ class Error < RuntimeError
13
+ attr_accessor :config_path
14
+
15
+ def self.[](*args)
16
+ new(*args)
17
+ end
18
+
19
+ def details
20
+ nil
21
+ end
22
+
23
+ def user_message
24
+ result =
25
+ <<-MESSAGE
26
+ active_record_doctor aborted due to the following error:
27
+ #{message}
28
+
29
+ Configuration file:
30
+ #{config_path_or_message}
31
+ MESSAGE
32
+
33
+ if details
34
+ result << (
35
+ <<-MESSAGE
36
+
37
+ Additional information:
38
+ #{details}
39
+ MESSAGE
40
+ )
41
+ end
42
+
43
+ result
44
+ end
45
+
46
+ private
47
+
48
+ def config_path_or_message
49
+ @config_path || "no configuration file in use (using default settings)"
50
+ end
51
+
52
+ def hyphenated_list(items)
53
+ items.map { |item| " - #{item}" }.join("\n")
54
+ end
55
+ end
56
+
57
+ class Error
58
+ # We don't need extra documentation for error classes because of their
59
+ # extensive error messages.
60
+ # rubocop:disable Style/Documentation
61
+
62
+ class ConfigurationFileMissing < Error
63
+ def initialize
64
+ super("Configuration file not found")
65
+ end
66
+
67
+ def details
68
+ <<-MESSAGE
69
+ active_record_doctor attempted to read a configuration file but could not find
70
+ it. Please ensure the file exists and is readable (which includes correct
71
+ permissions are set). If it does not exist or it's readable but you still get
72
+ this error then consider filing a bug report as active_record_doctor should
73
+ not attempt to load non-existent configuration files.
74
+ MESSAGE
75
+ end
76
+ end
77
+
78
+ class ConfigurationError < Error
79
+ def initialize(exc)
80
+ @exc = exc
81
+ super("Loading the configuration file resulted in an exception")
82
+ end
83
+
84
+ def details
85
+ <<-MESSAGE
86
+ The information below comes from the exception raised when the configuration
87
+ file was being evaluated. Please try using the details below to fix the error
88
+ and retry.
89
+
90
+ Error class:
91
+ #{@exc.class.name}
92
+
93
+ Error message:
94
+ #{@exc.message}
95
+
96
+ Backtrace:
97
+ #{@exc.backtrace.join("\n")}
98
+ MESSAGE
99
+ end
100
+ end
101
+
102
+ class ConfigureNotCalled < Error
103
+ def initialize
104
+ super("The configuration file did not call ActiveRecordDoctor.configuration")
105
+ end
106
+
107
+ def details
108
+ <<-MESSAGE
109
+ active_record_doctor configuration is a Ruby script that MUST call
110
+ ActiveRecordDoctor.configuration exactly once. That method was NOT called by
111
+ the configuration file in use. If you intend to provide custom configuration
112
+ then please ensure that method is called. Otherwise, please delete the
113
+ configuration file.
114
+ MESSAGE
115
+ end
116
+ end
117
+
118
+ class ConfigureCalledTwice < Error
119
+ def initialize
120
+ super("The configuration file called ActiveRecordDoctor.configuration multiple times")
121
+ end
122
+
123
+ def details
124
+ <<-MESSAGE
125
+ The configuration file in use has called ActiveRecordDoctor.configure more than
126
+ once but should do so EXACTLY ONCE. Please ensure that method is called exactly
127
+ once and retry.
128
+ MESSAGE
129
+ end
130
+ end
131
+
132
+ class DetectorConfiguredTwice < Error
133
+ def initialize(detector)
134
+ super("Detector #{detector} was configured multiple times")
135
+ end
136
+
137
+ def details
138
+ <<-MESSAGE
139
+ The configuration file configured the same detector more than once which is
140
+ disallowed. Detector configuration should be either:
141
+
142
+ - absent - to use the defaults
143
+ - present exactly once - to override the defaults
144
+
145
+ Please ensure the detector is configured at most once and retry.
146
+ MESSAGE
147
+ end
148
+ end
149
+
150
+ class UnrecognizedDetectorName < Error
151
+ def initialize(detector, recognized_detectors)
152
+ @recognized_detectors = recognized_detectors
153
+ super("Received configuration for an unrecognized detector named #{detector}")
154
+ end
155
+
156
+ def details
157
+ <<-MESSAGE
158
+ The configuration file provided configuration for an unknown detector. Please
159
+ ensure only valid detector names are used and retry.
160
+
161
+ Currently, the following detectors are recognized:
162
+
163
+ #{hyphenated_list(@recognized_detectors)}
164
+ MESSAGE
165
+ end
166
+ end
167
+
168
+ class UnrecognizedDetectorSettings < Error
169
+ def initialize(detector, unrecognized_settings, recognized_settings)
170
+ @detector = detector
171
+ @unrecognized_settings = unrecognized_settings
172
+ @recognized_settings = recognized_settings
173
+ super("Detector #{detector} received unrecognized settings")
174
+ end
175
+
176
+ def details
177
+ <<-MESSAGE
178
+ The configuration file provided an unrecognized setting for a detector. Please
179
+ ensure only recognized settings are used and retry.
180
+
181
+ The following settings are not recognized by #{@detector}:
182
+
183
+ #{hyphenated_list(@unrecognized_settings)}
184
+
185
+ The complete of settings recognized by #{@detector} is:
186
+
187
+ #{hyphenated_list(@recognized_settings)}
188
+ MESSAGE
189
+ end
190
+ end
191
+
192
+ class UnrecognizedGlobalSetting < Error
193
+ def initialize(name, recognized_settings)
194
+ @recognized_settings = recognized_settings
195
+ super("Global #{name} is unrecognized")
196
+ end
197
+
198
+ def details
199
+ <<-MESSAGE
200
+ The configuration file set an unrecognized global setting. Please ensure that
201
+ only recognized global settings are used and retry.
202
+
203
+ Currently recognized global settings are:
204
+
205
+ #{hyphenated_list(@recognized_settings)}
206
+ MESSAGE
207
+ end
208
+ end
209
+
210
+ class DuplicateGlobalSetting < Error
211
+ def initialize(name)
212
+ super("Global #{name} was set twice")
213
+ end
214
+
215
+ def details
216
+ <<-MESSAGE
217
+ The configuration file set the same global setting twice. Each global setting
218
+ must be set AT MOST ONCE. Please ensure all global settings are set at most once
219
+ and retry.
220
+ MESSAGE
221
+ end
222
+ end
223
+
224
+ # rubocop:enable Style/Documentation
225
+ end
226
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDoctor
4
+ # Turn a detector class into a human-readable help text.
5
+ class Help
6
+ def initialize(klass)
7
+ @klass = klass
8
+ end
9
+
10
+ def to_s
11
+ <<-HELP
12
+ #{klass.underscored_name} - #{klass.description}
13
+
14
+ Configuration options:
15
+ #{config_to_s}
16
+ HELP
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :klass
22
+
23
+ GLOBAL_COMMENT = "local and global"
24
+ LOCAL_COMMENT = "local only"
25
+
26
+ def config_to_s
27
+ klass.config.map do |key, metadata|
28
+ type =
29
+ if metadata[:global]
30
+ GLOBAL_COMMENT
31
+ else
32
+ LOCAL_COMMENT
33
+ end
34
+
35
+ " - #{key} (#{type}) - #{metadata.fetch(:description)}"
36
+ end.join("\n")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/tasklib"
4
+
5
+ module ActiveRecordDoctor
6
+ module Rake
7
+ # A Rake task for calling active_record_doctor detectors.
8
+ #
9
+ # The three supported attributes are:
10
+ #
11
+ # - deps - project-specific Rake dependencies, e.g. :environment in Rails.
12
+ # - config_path - active_record_doctor configuration file path.
13
+ # - setup - a callable (responding to #call) responsible for finishing.
14
+ # the setup process after deps are invoked, e.g. preloading models.
15
+ #
16
+ # The dependencies between Rake tasks are:
17
+ #
18
+ # active_record_doctor:<detector> => active_record_doctor:setup => <deps>
19
+ #
20
+ # active_record_doctor:setup is where the setup callable is called.
21
+ class Task < ::Rake::TaskLib
22
+ attr_accessor :deps, :config_path, :setup
23
+
24
+ def initialize
25
+ super
26
+
27
+ @deps = []
28
+ @config_path = nil
29
+ @setup = nil
30
+
31
+ yield(self)
32
+
33
+ define
34
+ end
35
+
36
+ def define
37
+ namespace :active_record_doctor do
38
+ task :setup => deps do
39
+ @setup&.call
40
+ config
41
+ end
42
+
43
+ ActiveRecordDoctor.detectors.each do |name, detector|
44
+ desc detector.description
45
+ task name => :"active_record_doctor:setup" do
46
+ runner.run_one(name) or exit(1)
47
+ end
48
+
49
+ namespace name do
50
+ desc "Show help for #{name}"
51
+ task :help => :"active_record_doctor:setup" do
52
+ runner.help(name)
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ desc "Run all active_record_doctor detectors"
59
+ task :active_record_doctor => :"active_record_doctor:setup" do
60
+ runner.run_all or exit(1)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def runner
67
+ @runner ||= ActiveRecordDoctor::Runner.new(config)
68
+ end
69
+
70
+ def config
71
+ @config ||= begin
72
+ path = config_path && File.exist?(config_path) ? config_path : nil
73
+ ActiveRecordDoctor.load_config_with_defaults(path)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDoctor # :nodoc:
4
+ # An excecution environment for active_record_doctor that provides a config
5
+ # and an output device for use by detectors.
6
+ class Runner
7
+ # io is injected via constructor parameters to facilitate testing.
8
+ def initialize(config, io = $stdout)
9
+ @config = config
10
+ @io = io
11
+ end
12
+
13
+ def run_one(name)
14
+ ActiveRecordDoctor.handle_exception do
15
+ ActiveRecordDoctor.detectors.fetch(name).run(config, io)
16
+ end
17
+ end
18
+
19
+ def run_all
20
+ success = true
21
+
22
+ # We can't use #all? because of its short-circuit behavior - it stops
23
+ # iteration and returns false upon the first falsey value. This
24
+ # prevents other detectors from running if there's a failure.
25
+ ActiveRecordDoctor.detectors.each do |name, _|
26
+ success = false if !run_one(name)
27
+ end
28
+
29
+ success
30
+ end
31
+
32
+ def help(name)
33
+ detector = ActiveRecordDoctor.detectors.fetch(name)
34
+ io.puts(ActiveRecordDoctor::Help.new(detector))
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :config, :io
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordDoctor
4
- VERSION = "1.8.0"
4
+ VERSION = "1.10.0"
5
5
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_record_doctor/printers"
4
- require "active_record_doctor/printers/io_printer"
5
3
  require "active_record_doctor/railtie" if defined?(Rails) && defined?(Rails::Railtie)
6
4
  require "active_record_doctor/detectors"
7
5
  require "active_record_doctor/detectors/base"
@@ -9,14 +7,21 @@ require "active_record_doctor/detectors/missing_presence_validation"
9
7
  require "active_record_doctor/detectors/missing_foreign_keys"
10
8
  require "active_record_doctor/detectors/missing_unique_indexes"
11
9
  require "active_record_doctor/detectors/incorrect_boolean_presence_validation"
10
+ require "active_record_doctor/detectors/incorrect_length_validation"
12
11
  require "active_record_doctor/detectors/extraneous_indexes"
13
12
  require "active_record_doctor/detectors/unindexed_deleted_at"
14
13
  require "active_record_doctor/detectors/undefined_table_references"
15
14
  require "active_record_doctor/detectors/missing_non_null_constraint"
16
15
  require "active_record_doctor/detectors/unindexed_foreign_keys"
17
16
  require "active_record_doctor/detectors/incorrect_dependent_option"
18
- require "active_record_doctor/task"
17
+ require "active_record_doctor/detectors/short_primary_key_type"
18
+ require "active_record_doctor/detectors/mismatched_foreign_key_type"
19
+ require "active_record_doctor/errors"
20
+ require "active_record_doctor/help"
21
+ require "active_record_doctor/runner"
19
22
  require "active_record_doctor/version"
23
+ require "active_record_doctor/config"
24
+ require "active_record_doctor/config/loader"
20
25
 
21
26
  module ActiveRecordDoctor # :nodoc:
22
27
  end