active_record_doctor 1.8.0 → 1.9.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +246 -48
  3. data/lib/active_record_doctor/config/default.rb +59 -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 +110 -19
  7. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +63 -37
  8. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +32 -23
  9. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +70 -34
  10. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +45 -0
  11. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +32 -23
  12. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +40 -28
  13. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +28 -21
  14. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +40 -30
  15. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +41 -0
  16. data/lib/active_record_doctor/detectors/undefined_table_references.rb +19 -20
  17. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +44 -18
  18. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +31 -20
  19. data/lib/active_record_doctor/detectors.rb +12 -4
  20. data/lib/active_record_doctor/errors.rb +226 -0
  21. data/lib/active_record_doctor/help.rb +39 -0
  22. data/lib/active_record_doctor/rake/task.rb +78 -0
  23. data/lib/active_record_doctor/runner.rb +41 -0
  24. data/lib/active_record_doctor/version.rb +1 -1
  25. data/lib/active_record_doctor.rb +7 -3
  26. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +34 -21
  27. data/lib/tasks/active_record_doctor.rake +9 -18
  28. data/test/active_record_doctor/config/loader_test.rb +120 -0
  29. data/test/active_record_doctor/config_test.rb +116 -0
  30. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +131 -8
  31. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +48 -5
  32. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +190 -12
  33. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +82 -0
  34. data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +50 -4
  35. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +138 -24
  36. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +74 -13
  37. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +57 -8
  38. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +64 -0
  39. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +34 -21
  40. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +112 -8
  41. data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +56 -4
  42. data/test/active_record_doctor/runner_test.rb +42 -0
  43. data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +131 -0
  44. data/test/model_factory.rb +73 -23
  45. data/test/setup.rb +62 -72
  46. metadata +40 -9
  47. data/lib/active_record_doctor/printers/io_printer.rb +0 -133
  48. data/lib/active_record_doctor/task.rb +0 -28
  49. data/test/active_record_doctor/printers/io_printer_test.rb +0 -33
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 296a8bbb4a326fd78afb08638435754646e22a4416dc18b02dc66f54198a9130
4
- data.tar.gz: 5548b71ee3bb95344e34896a7c972cf39ce33f8a4bd1b03a1a2bf02b4081a1dd
3
+ metadata.gz: 4126519be8847ccb2b1584696ae1c13b26c61bcfa9eb39ad69f8907afb514efb
4
+ data.tar.gz: c6cbafc9d9ef3679bb79ac6c8bad03f19a4f2e5188dd8be2672166a4742b3fa2
5
5
  SHA512:
6
- metadata.gz: 48c447ca18f35ee44db354414980f54d0ee9416ecc81b6b3fef9401afbaeb44b2951f0fabe43c89027d8783430bdbb6b055ca1000779e37d3729a171649ce795
7
- data.tar.gz: 89ab1d3e4cca4df9c94022e0f11217b41239a3b5f2b3c579d8e466a4fe337909ab48f0b05d28eb5a67fc93afa4afe88a47c7c0ebbd3dc7d45c6d0259db22de40
6
+ metadata.gz: 110732302b0603e41b54c2c0bcec8b313899fc6f43a985c955db7e2a8582980bb756cfa35be965e862b0f6b8904e5972a6ce999ebfb2b1a6871d7dcc71bc21f3
7
+ data.tar.gz: a899ae429e450cc2e1fe5caf8d37b8a2820a5d6121981e9f361106805ec93e6739cc190a5eb264fb74a41bc4738575e372e03fad5fe70a0870052a210ea632fa
data/README.md CHANGED
@@ -1,42 +1,147 @@
1
1
  # Active Record Doctor
2
2
 
3
3
  Active Record Doctor helps to keep the database in a good shape. Currently, it
4
- can:
4
+ can detect:
5
+
6
+ * extraneous indexes - [`active_record_doctor:extraneous_indexes`](#removing-extraneous-indexes)
7
+ * unindexed `deleted_at` columns - [`active_record_doctor:unindexed_deleted_at`](#detecting-unindexed-deleted_at-columns)
8
+ * missing foreign key constraints - [`active_record_doctor:missing_foreign_keys`](#detecting-missing-foreign-key-constraints)
9
+ * models referencing undefined tables - [`active_record_doctor:undefined_table_references`](#detecting-models-referencing-undefined-tables)
10
+ * uniqueness validations not backed by an unique index - [`active_record_doctor:missing_unique_indexes`](#detecting-uniqueness-validations-not-backed-by-an-index)
11
+ * missing non-`NULL` constraints - [`active_record_doctor:missing_non_null_constraint`](#detecting-missing-non-null-constraints)
12
+ * missing presence validations - [`active_record_doctor:missing_presence_validation`](#detecting-missing-presence-validations)
13
+ * incorrect presence validations on boolean columns - [`active_record_doctor:incorrect_boolean_presence_validation`](#detecting-incorrect-presence-validations-on-boolean-columns)
14
+ * incorrect values of `dependent` on associations - [`active_record_doctor:incorrect_dependent_option`](#detecting-incorrect-dependent-option-on-associations)
15
+ * primary keys having short integer types - [`active_record_doctor:short_primary_key_type`](#detecting-primary-keys-having-short-integer-types)
16
+ * mismatched foreign key types - [`active_record_doctor:mismatched_foreign_key_type`](#detecting-mismatched-foreign-key-types)
17
+
18
+ It can also:
5
19
 
6
20
  * index unindexed foreign keys - [`active_record_doctor:unindexed_foreign_keys`](#indexing-unindexed-foreign-keys)
7
- * detect extraneous indexes - [`active_record_doctor:extraneous_indexes`](#removing-extraneous-indexes)
8
- * detect unindexed `deleted_at` columns - [`active_record_doctor:unindexed_deleted_at`](#detecting-unindexed-deleted_at-columns)
9
- * detect missing foreign key constraints - [`active_record_doctor:missing_foreign_keys`](#detecting-missing-foreign-key-constraints)
10
- * detect models referencing undefined tables - [`active_record_doctor:undefined_table_references`](#detecting-models-referencing-undefined-tables)
11
- * detect uniqueness validations not backed by an unique index - [`active_record_doctor:missing_unique_indexes`](#detecting-uniqueness-validations-not-backed-by-an-index)
12
- * detect missing non-`NULL` constraints - [`active_record_doctor:missing_non_null_constraint`](#detecting-missing-non-null-constraints)
13
- * detect missing presence validations - [`active_record_doctor:missing_presence_validation`](#detecting-missing-presence-validations)
14
- * detect incorrect presence validations on boolean columns - [`active_record_doctor:incorrect_boolean_presence_validation`](#detecting-incorrect-presence-validations-on-boolean-columns)
15
- * detect incorrect values of `dependent` on associations - [`active_record_doctor:incorrect_dependent_option`](#detecting-incorrect-dependent-option-on-associations)
16
21
 
17
- More features coming soon!
18
-
19
- Want to suggest a feature? Just shoot me [an email](mailto:contact@gregnavis.com).
20
-
21
- [<img src="https://travis-ci.org/gregnavis/active_record_doctor.svg?branch=master" alt="Build Status" />](https://travis-ci.org/gregnavis/active_record_doctor)
22
+ [![Build Status](https://github.com/gregnavis/active_record_doctor/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/gregnavis/active_record_doctor/actions/workflows/test.yml)
22
23
 
23
24
  ## Installation
24
25
 
25
- The preferred installation method is adding `active_record_doctor` to your
26
- `Gemfile`:
26
+ In order to use the latest production release, please add the following to
27
+ your `Gemfile`:
27
28
 
28
29
  ```ruby
29
30
  gem 'active_record_doctor', group: :development
30
31
  ```
31
32
 
32
- Then run:
33
+ and run `bundle install`. If you'd like to use the most recent development
34
+ version then use this instead:
33
35
 
34
- ```bash
35
- bundle install
36
+ ```ruby
37
+ gem 'active_record_doctor', github: 'gregnavis/active_record_doctor'
38
+ ```
39
+
40
+ That's it when it comes to Rails projects. If your project doesn't use Rails
41
+ then you can use `active_record_doctor` via `Rakefile`.
42
+
43
+ ### Additional Installation Steps for non-Rails Projects
44
+
45
+ If your project uses Rake then you can add the following to `Rakefile` in order
46
+ to use `active_record_doctor`:
47
+
48
+ ```ruby
49
+ require "active_record_doctor"
50
+
51
+ ActiveRecordDoctor::Rake::Task.new do |task|
52
+ # Add project-specific Rake dependencies that should be run before running
53
+ # active_record_doctor.
54
+ task.deps = []
55
+
56
+ # A path to your active_record_doctor configuration file.
57
+ task.config_path = ::Rails.root.join(".active_record_doctor")
58
+
59
+ # A Proc called right before running detectors that should ensure your Active
60
+ # Record models are preloaded and a database connection is ready.
61
+ task.setup = -> { ::Rails.application.eager_load! }
62
+ end
36
63
  ```
37
64
 
65
+ **IMPORTANT**. `active_record_doctor` expects that after running `deps` and
66
+ calling `setup` your Active Record models are loaded and a database connection
67
+ is established.
68
+
38
69
  ## Usage
39
70
 
71
+ `active_record_doctor` can be used via `rake` or `rails`.
72
+
73
+ You can run all available detectors via:
74
+
75
+ ```
76
+ bundle exec rake active_record_doctor
77
+ ```
78
+
79
+ You can run a specific detector via:
80
+
81
+ ```
82
+ bundle exec rake active_record_doctor:extraneous_indexes
83
+ ```
84
+
85
+ ### Continuous Integration
86
+
87
+ If you want to use `active_record_doctor` in a Continuous Integration setting
88
+ then ensure the configuration file is committed and run the tool as one of your
89
+ build steps -- it returns a non-zero exit status if any errors were reported.
90
+
91
+ ### Obtaining Help
92
+
93
+ If you'd like to obtain help on a specific detector then use the `help` sub-task:
94
+
95
+ ```
96
+ bundle exec rake active_record_doctor:extraneous_indexes:help
97
+ ```
98
+
99
+ This will show the detector help text in the terminal, along with supported
100
+ configuration options, their meaning, and whether they're global or local.
101
+
102
+ ### Configuration
103
+
104
+ `active_record_doctor` can be configured to better suite your project's needs.
105
+ For example, if it complains about a model that you want ignored then you can
106
+ add that model to the configuration file.
107
+
108
+ If you want to use the default configuration then you don't have to do anything.
109
+ Just run `active_record_doctor` in your project directory.
110
+
111
+ If you want to customize the tool you should create a file named
112
+ `.active_record_doctor` in your project root directory with content like:
113
+
114
+ ```ruby
115
+ ActiveRecordDoctor.configure do
116
+ # Global settings affect all detectors.
117
+ global :ignore_tables, [
118
+ # Ignore internal Rails-related tables.
119
+ "ar_internal_metadata",
120
+ "schema_migrations",
121
+ "active_storage_blobs",
122
+ "active_storage_attachments",
123
+ "action_text_rich_texts",
124
+
125
+ # Add project-specific tables here.
126
+ "legacy_users"
127
+ ]
128
+
129
+ # Detector-specific settings affect only one specific detector.
130
+ detector :extraneous_indexes,
131
+ ignore_tables: ["users"],
132
+ ignore_indexes: ["accounts_on_email_organization_id"]
133
+ end
134
+ ```
135
+
136
+ The configuration file above will make `active_record_doctor` ignore internal
137
+ Rails tables (which are ignored by default) and also the `legacy_users` table.
138
+ It'll also make the `extraneous_indexes` detector skip the `users` table
139
+ entirely and will not report the index named `accounts_on_email_organization_id`
140
+ as extraneous.
141
+
142
+ Configuration options for each detector are listed below. They can also be
143
+ obtained via the help mechanism described in the previous section.
144
+
40
145
  ### Indexing Unindexed Foreign Keys
41
146
 
42
147
  Foreign keys should be indexed unless it's proven ineffective. However, Rails
@@ -66,6 +171,11 @@ three-step process:
66
171
  bundle exec rake db:migrate
67
172
  ```
68
173
 
174
+ Supported configuration options:
175
+
176
+ - `ignore_tables` - tables whose foreign keys should not be checked
177
+ - `ignore_columns` - columns, written as table.column, that should not be checked.
178
+
69
179
  ### Removing Extraneous Indexes
70
180
 
71
181
  Let me illustrate with an example. Consider a `users` table with columns
@@ -107,6 +217,11 @@ example, if there's a unique index on `users.login` and a non-unique index on
107
217
  `users.login, users.domain` then the tool will _not_ suggest dropping
108
218
  `users.login` as it could violate the uniqueness assumption.
109
219
 
220
+ Supported configuration options:
221
+
222
+ - `ignore_tables` - tables whose indexes should never be reported as extraneous.
223
+ - `ignore_columns` - indexes that should never be reported as extraneous.
224
+
110
225
  ### Detecting Unindexed `deleted_at` Columns
111
226
 
112
227
  If you soft-delete some models (e.g. with `paranoia`) then you need to modify
@@ -125,6 +240,13 @@ This will print a list of indexes that don't have the `deleted_at IS NULL`
125
240
  clause. Currently, `active_record_doctor` cannot automatically generate
126
241
  appropriate migrations. You need to do that manually.
127
242
 
243
+ Supported configuration options:
244
+
245
+ - `ignore_tables` - tables whose indexes should not be checked.
246
+ - `ignore_columns` - specific columns, written as table.column, that should not be reported as unindexed.
247
+ - `ignore_indexes` - specific indexes that should not be reported as excluding a timestamp column.
248
+ - `column_names` - deletion timestamp column names.
249
+
128
250
  ### Detecting Missing Foreign Key Constraints
129
251
 
130
252
  If `users.profile_id` references a row in `profiles` then this can be expressed
@@ -142,20 +264,8 @@ keys with the following command:
142
264
  bundle exec rake active_record_doctor:missing_foreign_keys
143
265
  ```
144
266
 
145
- The output will look like:
146
-
147
- ```
148
- users profile_id
149
- comments user_id article_id
150
- ```
151
-
152
- Tables are listed one per line. Each line starts with a table name followed by
153
- column names that should have a foreign key constraint. In the example above,
154
- `users.profile_id`, `comments.user_id`, and `comments.article_id` lack a foreign
155
- key constraint.
156
-
157
- In order to add a foreign key constraint to `users.profile_id` use the following
158
- migration:
267
+ In order to add a foreign key constraint to `users.profile_id` use a migration
268
+ like:
159
269
 
160
270
  ```ruby
161
271
  class AddForeignKeyConstraintToUsersProfileId < ActiveRecord::Migration
@@ -165,6 +275,11 @@ class AddForeignKeyConstraintToUsersProfileId < ActiveRecord::Migration
165
275
  end
166
276
  ```
167
277
 
278
+ Supported configuration options:
279
+
280
+ - `ignore_tables` - tables whose columns should not be checked.
281
+ - `ignore_columns` - columns, written as table.column, that should not be checked.
282
+
168
283
  ### Detecting Models Referencing Undefined Tables
169
284
 
170
285
  Active Record guesses the table name based on the class name. There are a few
@@ -187,13 +302,16 @@ If there a model references an undefined table then you'll see a message like
187
302
  this:
188
303
 
189
304
  ```
190
- The following models reference undefined tables:
191
- Contract (the table contract_records is undefined)
305
+ Contract references a non-existent table or view named contract_records
192
306
  ```
193
307
 
194
308
  On top of that `rake` will exit with status code of 1. This allows you to use
195
309
  this check as part of your Continuous Integration pipeline.
196
310
 
311
+ Supported configuration options:
312
+
313
+ - `ignore_models` - models whose underlying tables should not be checked for existence.
314
+
197
315
  ### Detecting Uniqueness Validations not Backed by an Index
198
316
 
199
317
  A model-level uniqueness validations should be backed by a database index in
@@ -209,12 +327,16 @@ bundle exec rake active_record_doctor:missing_unique_indexes
209
327
  If there are such indexes then the command will print:
210
328
 
211
329
  ```
212
- The following indexes should be created to back model-level uniqueness validations:
213
- users: email
330
+ add a unique index on users(email) - validating uniqueness in the model without an index can lead to duplicates
214
331
  ```
215
332
 
216
333
  This means that you should create a unique index on `users.email`.
217
334
 
335
+ Supported configuration options:
336
+
337
+ - `ignore_models` - models whose uniqueness validators should not be checked.
338
+ - `ignore_columns` - specific validators, written as Model(column1, column2, ...), that should not be checked.
339
+
218
340
  ### Detecting Missing Non-`NULL` Constraints
219
341
 
220
342
  If there's an unconditional presence validation on a column then it should be
@@ -230,9 +352,7 @@ bundle exec rake active_record_doctor:missing_non_null_constraint
230
352
  The output of the command is similar to:
231
353
 
232
354
  ```
233
- The following columns should be marked as `null: false`:
234
- users: name
235
-
355
+ add `NOT NULL` to users.name - models validates its presence but it's not non-NULL in the database
236
356
  ```
237
357
 
238
358
  You can mark the columns mentioned in the output as `null: false` by creating a
@@ -240,6 +360,11 @@ migration and calling `change_column_null`.
240
360
 
241
361
  This validator skips models whose corresponding database tables don't exist.
242
362
 
363
+ Supported configuration options:
364
+
365
+ - `ignore_tables` - tables whose columns should not be checked.
366
+ - `ignore_columns` - columns, written as table.column, that should not be checked.
367
+
243
368
  ### Detecting Missing Presence Validations
244
369
 
245
370
  If a column is marked as `null: false` then it's likely it should have the
@@ -254,14 +379,19 @@ bundle exec rake active_record_doctor:missing_presence_validation
254
379
  The output of the command looks like this:
255
380
 
256
381
  ```
257
- The following models and columns should have presence validations:
258
- User: email, name
382
+ add a `presence` validator to User.email - it's NOT NULL but lacks a validator
383
+ add a `presence` validator to User.name - it's NOT NULL but lacks a validator
259
384
  ```
260
385
 
261
386
  This means `User` should have a presence validator on `email` and `name`.
262
387
 
263
388
  This validator skips models whose corresponding database tables don't exist.
264
389
 
390
+ Supported configuration options:
391
+
392
+ - `ignore_models` - models whose underlying tables' columns should not be checked.
393
+ - `ignore_columns` - specific attributes, written as Model.attribute, that should not be checked.
394
+
265
395
  ### Detecting Incorrect Presence Validations on Boolean Columns
266
396
 
267
397
  A boolean column's presence should be validated using inclusion or exclusion
@@ -276,8 +406,7 @@ bundle exec rake active_record_doctor:incorrect_boolean_presence_validation
276
406
  The output of the command looks like this:
277
407
 
278
408
  ```
279
- The presence of the following boolean columns is validated incorrectly:
280
- User: active
409
+ replace the `presence` validator on User.active with `inclusion` - `presence` can't be used on booleans
281
410
  ```
282
411
 
283
412
  This means `active` is validated with `presence: true` instead of
@@ -285,6 +414,11 @@ This means `active` is validated with `presence: true` instead of
285
414
 
286
415
  This validator skips models whose corresponding database tables don't exist.
287
416
 
417
+ Supported configuration options:
418
+
419
+ - `ignore_models` - models whose validators should not be checked.
420
+ - `ignore_columns` - attributes, written as Model.attribute, whose validators should not be checked.
421
+
288
422
  ### Detecting Incorrect `dependent` Option on Associations
289
423
 
290
424
  Cascading model deletions can be sped up with `dependent: :delete_all` (to
@@ -308,11 +442,75 @@ bundle exec rake active_record_doctor:incorrect_dependent_option
308
442
  The output of the command looks like this:
309
443
 
310
444
  ```
311
- The following associations might be using invalid dependent settings:
312
- Company: users loads models one-by-one to invoke callbacks even though the related model defines none - consider using `dependent: :delete_all`
313
- Post: comments skips callbacks that are defined on the associated model - consider changing to `dependent: :destroy` or similar
445
+ use `dependent: :delete_all` or similar on Company.users - associated models have no validations and can be deleted in bulk
446
+ use `dependent: :destroy` or similar on Post.comments - the associated model has callbacks that are currently skipped
314
447
  ```
315
448
 
449
+ Supported configuration options:
450
+
451
+ - `ignore_models` - models whose associations should not be checked.
452
+ - `ignore_columns` - associations, written as Model.association, that should not be checked.
453
+
454
+ ### Detecting Primary Keys Having Short Integer Types
455
+
456
+ Active Record 5.1 changed the default primary and foreign key type from INTEGER
457
+ to BIGINT. The reason is to reduce the risk of running out of IDs on inserts.
458
+
459
+ In order to detect primary keys using shorter integer types, for example created
460
+ before migrating to 5.1, you can run the following command:
461
+
462
+ ```
463
+ bundle exec rake active_record_doctor:short_primary_key_type
464
+ ```
465
+
466
+ The output of the command looks like this:
467
+
468
+ ```
469
+ change the type of companies.id to bigint
470
+ ```
471
+
472
+ The above means `comanies.id` should be migrated to a wider integer type. An
473
+ example migration to accomplish this looks likes this:
474
+
475
+ ```ruby
476
+ class ChangeCompaniesPrimaryKeyType < ActiveRecord::Migration[5.1]
477
+ def change
478
+ change_column :companies, :id, :bigint
479
+ end
480
+ end
481
+ ```
482
+
483
+ **IMPORTANT**. Running the above migration on a large table can cause downtime
484
+ as all rows need to be rewritten.
485
+
486
+ Supported configuration options:
487
+
488
+ - `ignore_tables` - tables whose primary keys should not be checked.
489
+
490
+ ### Detecting Mismatched Foreign Key Types
491
+
492
+ Foreign keys should be of the same type as the referenced primary key.
493
+ Otherwise, there's a risk of bugs caused by IDs representable by one type but
494
+ not the other.
495
+
496
+ Running the command below will list all foreign keys whose type is different
497
+ from the referenced primary key:
498
+
499
+ ```
500
+ bundle exec rake active_record_doctor:mismatched_foreign_key_type
501
+ ```
502
+
503
+ The output of the command looks like this:
504
+
505
+ ```
506
+ companies.user_id references a column of different type - foreign keys should be of the same type as the referenced column
507
+ ```
508
+
509
+ Supported configuration options:
510
+
511
+ - `ignore_tables` - tables whose foreign keys should not be checked.
512
+ - `ignore_columns` - foreign keys, written as table.column, that should not be checked.
513
+
316
514
  ## Ruby and Rails Compatibility Policy
317
515
 
318
516
  The goal of the policy is to ensure proper functioning in reasonable
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecordDoctor.configure do
4
+ global :ignore_tables, [
5
+ "ar_internal_metadata",
6
+ "schema_migrations",
7
+ "active_storage_blobs",
8
+ "active_storage_attachments",
9
+ "action_text_rich_texts"
10
+ ]
11
+
12
+ detector :extraneous_indexes,
13
+ ignore_tables: [],
14
+ ignore_indexes: []
15
+
16
+ detector :incorrect_boolean_presence_validation,
17
+ ignore_models: [],
18
+ ignore_attributes: []
19
+
20
+ detector :incorrect_dependent_option,
21
+ ignore_models: [],
22
+ ignore_associations: []
23
+
24
+ detector :mismatched_foreign_key_type,
25
+ ignore_tables: [],
26
+ ignore_columns: []
27
+
28
+ detector :missing_foreign_keys,
29
+ ignore_tables: [],
30
+ ignore_columns: []
31
+
32
+ detector :missing_non_null_constraint,
33
+ ignore_tables: [],
34
+ ignore_columns: []
35
+
36
+ detector :missing_presence_validation,
37
+ ignore_models: [],
38
+ ignore_attributes: []
39
+
40
+ detector :missing_unique_indexes,
41
+ ignore_models: [],
42
+ ignore_columns: []
43
+
44
+ detector :short_primary_key_type,
45
+ ignore_tables: []
46
+
47
+ detector :undefined_table_references,
48
+ ignore_models: []
49
+
50
+ detector :unindexed_deleted_at,
51
+ ignore_tables: [],
52
+ ignore_columns: [],
53
+ ignore_indexes: [],
54
+ column_names: ["deleted_at", "discarded_at"]
55
+
56
+ detector :unindexed_foreign_keys,
57
+ ignore_tables: [],
58
+ ignore_columns: []
59
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDoctor # :nodoc:
4
+ class << self
5
+ # The config file that's currently being processed by .load_config.
6
+ attr_reader :current_config
7
+
8
+ # This method is part of the public API that is intended for use by
9
+ # active_record_doctor users. The remaining methods are considered to be
10
+ # public-not-published.
11
+ def configure(&block)
12
+ # If current_config is set it means that .configure was already called
13
+ # so we must raise an error.
14
+ raise ActiveRecordDoctor::Error::ConfigureCalledTwice if current_config
15
+
16
+ # Determine the recognized global and detector settings based on detector
17
+ # metadata. recognizedd_detectors maps detector names to setting names.
18
+ # recognized_globals contains global setting names.
19
+ recognized_detectors = {}
20
+ recognized_globals = []
21
+
22
+ ActiveRecordDoctor.detectors.each do |name, detector|
23
+ locals, globals = detector.locals_and_globals
24
+
25
+ recognized_detectors[name] = locals
26
+ recognized_globals.concat(globals)
27
+ end
28
+
29
+ # The same global can be used by multiple detectors so we must remove
30
+ # duplicates to ensure they aren't reported mutliple times via the user
31
+ # interface (e.g. in error messages).
32
+ recognized_globals.uniq!
33
+
34
+ # Prepare an empty configuration and call the loader. After .new returns
35
+ # @current_config will contain the configuration provided by the block.
36
+ @current_config = Config.new({}, {})
37
+ Loader.new(current_config, recognized_globals, recognized_detectors, &block)
38
+
39
+ # This method is part of the public API expected to be called by users.
40
+ # In order to avoid leaking internal objects, we return an explicit nil.
41
+ nil
42
+ end
43
+
44
+ def load_config(path)
45
+ begin
46
+ load(path)
47
+ rescue ActiveRecordDoctor::Error
48
+ raise
49
+ rescue LoadError
50
+ raise ActiveRecordDoctor::Error::ConfigurationFileMissing
51
+ rescue StandardError => e
52
+ raise ActiveRecordDoctor::Error::ConfigurationError[e]
53
+ end
54
+ raise ActiveRecordDoctor::Error::ConfigureNotCalled if current_config.nil?
55
+
56
+ # Store the configuration and reset @current_config. We cannot reset
57
+ # @current_config in .configure because that would prevent us from
58
+ # detecting multiple calls to that method.
59
+ config = @current_config
60
+ @current_config = nil
61
+
62
+ config
63
+ rescue ActiveRecordDoctor::Error => e
64
+ e.config_path = path
65
+ raise e
66
+ end
67
+
68
+ DEFAULT_CONFIG_PATH = File.join(__dir__, "default.rb").freeze
69
+ private_constant :DEFAULT_CONFIG_PATH
70
+
71
+ def load_config_with_defaults(path)
72
+ default_config = load_config(DEFAULT_CONFIG_PATH)
73
+ return default_config if path.nil?
74
+
75
+ config = load_config(path)
76
+ default_config.merge(config)
77
+ end
78
+ end
79
+
80
+ # A class used for loading user-provided configuration files.
81
+ class Loader
82
+ def initialize(config, recognized_globals, recognized_detectors, &block)
83
+ @config = config
84
+ @recognized_globals = recognized_globals
85
+ @recognized_detectors = recognized_detectors
86
+ instance_eval(&block)
87
+ end
88
+
89
+ def global(name, value)
90
+ name = name.to_sym
91
+
92
+ unless recognized_globals.include?(name)
93
+ raise ActiveRecordDoctor::Error::UnrecognizedGlobalSetting[
94
+ name,
95
+ recognized_globals
96
+ ]
97
+ end
98
+
99
+ if config.globals.include?(name)
100
+ raise ActiveRecordDoctor::Error::DuplicateGlobalSetting[name]
101
+ end
102
+
103
+ config.globals[name] = value
104
+ end
105
+
106
+ def detector(name, settings)
107
+ name = name.to_sym
108
+
109
+ recognized_settings = recognized_detectors[name]
110
+ if recognized_settings.nil?
111
+ raise ActiveRecordDoctor::Error::UnrecognizedDetectorName[
112
+ name,
113
+ recognized_detectors.keys
114
+ ]
115
+ end
116
+
117
+ if config.detectors.include?(name)
118
+ raise ActiveRecordDoctor::Error::DetectorConfiguredTwice[name]
119
+ end
120
+
121
+ unrecognized_settings = settings.keys - recognized_settings
122
+ unless unrecognized_settings.empty?
123
+ raise ActiveRecordDoctor::Error::UnrecognizedDetectorSettings[
124
+ name,
125
+ unrecognized_settings,
126
+ recognized_settings
127
+ ]
128
+ end
129
+
130
+ config.detectors[name] = settings
131
+ end
132
+
133
+ private
134
+
135
+ attr_reader :config, :recognized_globals, :recognized_detectors
136
+ end
137
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDoctor # :nodoc:
4
+ Config = Struct.new(:globals, :detectors) do
5
+ def merge(config)
6
+ globals = self.globals.merge(config.globals)
7
+ detectors = self.detectors.merge(config.detectors) do |_name, self_settings, config_settings|
8
+ self_settings.merge(config_settings)
9
+ end
10
+
11
+ Config.new(globals, detectors)
12
+ end
13
+ end
14
+ end