active_record_doctor 1.8.0 → 1.9.0.rc1

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 (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