active_record_doctor 1.7.1 → 1.9.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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +287 -43
  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 +155 -0
  7. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +114 -0
  8. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +49 -0
  9. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +107 -0
  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 +60 -0
  12. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +72 -0
  13. data/lib/active_record_doctor/{tasks → detectors}/missing_presence_validation.rb +35 -21
  14. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +71 -0
  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 +33 -0
  17. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +55 -0
  18. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +59 -0
  19. data/lib/active_record_doctor/detectors.rb +21 -0
  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/printers.rb +3 -1
  23. data/lib/active_record_doctor/railtie.rb +2 -0
  24. data/lib/active_record_doctor/rake/task.rb +78 -0
  25. data/lib/active_record_doctor/runner.rb +41 -0
  26. data/lib/active_record_doctor/version.rb +3 -1
  27. data/lib/active_record_doctor.rb +24 -2
  28. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +46 -29
  29. data/lib/tasks/active_record_doctor.rake +21 -29
  30. data/test/active_record_doctor/config/loader_test.rb +120 -0
  31. data/test/active_record_doctor/config_test.rb +116 -0
  32. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +190 -0
  33. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +79 -0
  34. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +295 -0
  35. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +82 -0
  36. data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +70 -0
  37. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +216 -0
  38. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +168 -0
  39. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +163 -0
  40. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +64 -0
  41. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +57 -0
  42. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +171 -0
  43. data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +78 -0
  44. data/test/active_record_doctor/runner_test.rb +42 -0
  45. data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +131 -0
  46. data/test/model_factory.rb +128 -0
  47. data/test/setup.rb +116 -0
  48. metadata +103 -154
  49. data/Rakefile +0 -28
  50. data/lib/active_record_doctor/printers/io_printer.rb +0 -105
  51. data/lib/active_record_doctor/tasks/base.rb +0 -78
  52. data/lib/active_record_doctor/tasks/extraneous_indexes.rb +0 -82
  53. data/lib/active_record_doctor/tasks/incorrect_boolean_presence_validation.rb +0 -33
  54. data/lib/active_record_doctor/tasks/missing_foreign_keys.rb +0 -46
  55. data/lib/active_record_doctor/tasks/missing_non_null_constraint.rb +0 -52
  56. data/lib/active_record_doctor/tasks/missing_unique_indexes.rb +0 -56
  57. data/lib/active_record_doctor/tasks/undefined_table_references.rb +0 -33
  58. data/lib/active_record_doctor/tasks/unindexed_deleted_at.rb +0 -19
  59. data/lib/active_record_doctor/tasks/unindexed_foreign_keys.rb +0 -43
  60. data/lib/active_record_doctor/tasks.rb +0 -7
  61. data/test/active_record_doctor/printers/io_printer_test.rb +0 -20
  62. data/test/active_record_doctor/tasks/extraneous_indexes_test.rb +0 -81
  63. data/test/active_record_doctor/tasks/incorrect_boolean_presence_validation_test.rb +0 -33
  64. data/test/active_record_doctor/tasks/missing_foreign_keys_test.rb +0 -27
  65. data/test/active_record_doctor/tasks/missing_non_null_constraint_test.rb +0 -108
  66. data/test/active_record_doctor/tasks/missing_presence_validation_test.rb +0 -110
  67. data/test/active_record_doctor/tasks/missing_unique_indexes_test.rb +0 -95
  68. data/test/active_record_doctor/tasks/undefined_table_references_test.rb +0 -51
  69. data/test/active_record_doctor/tasks/unindexed_deleted_at_test.rb +0 -34
  70. data/test/active_record_doctor/tasks/unindexed_foreign_keys_test.rb +0 -27
  71. data/test/dummy/README.rdoc +0 -28
  72. data/test/dummy/Rakefile +0 -6
  73. data/test/dummy/app/assets/config/manifest.js +0 -1
  74. data/test/dummy/app/assets/javascripts/application.js +0 -13
  75. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  76. data/test/dummy/app/controllers/application_controller.rb +0 -5
  77. data/test/dummy/app/helpers/application_helper.rb +0 -2
  78. data/test/dummy/app/models/application_record.rb +0 -3
  79. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  80. data/test/dummy/bin/bundle +0 -3
  81. data/test/dummy/bin/rails +0 -4
  82. data/test/dummy/bin/rake +0 -4
  83. data/test/dummy/bin/setup +0 -29
  84. data/test/dummy/config/application.rb +0 -23
  85. data/test/dummy/config/boot.rb +0 -5
  86. data/test/dummy/config/database.yml +0 -19
  87. data/test/dummy/config/database.yml.travis +0 -5
  88. data/test/dummy/config/environment.rb +0 -5
  89. data/test/dummy/config/environments/development.rb +0 -41
  90. data/test/dummy/config/environments/production.rb +0 -79
  91. data/test/dummy/config/environments/test.rb +0 -47
  92. data/test/dummy/config/initializers/assets.rb +0 -11
  93. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  94. data/test/dummy/config/initializers/cookies_serializer.rb +0 -3
  95. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  96. data/test/dummy/config/initializers/inflections.rb +0 -16
  97. data/test/dummy/config/initializers/mime_types.rb +0 -4
  98. data/test/dummy/config/initializers/session_store.rb +0 -3
  99. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  100. data/test/dummy/config/locales/en.yml +0 -23
  101. data/test/dummy/config/routes.rb +0 -56
  102. data/test/dummy/config/secrets.yml +0 -22
  103. data/test/dummy/config.ru +0 -4
  104. data/test/dummy/db/migrate/base_migration.rb +0 -5
  105. data/test/dummy/db/schema.rb +0 -19
  106. data/test/dummy/log/development.log +0 -111
  107. data/test/dummy/log/test.log +0 -79424
  108. data/test/dummy/public/404.html +0 -67
  109. data/test/dummy/public/422.html +0 -67
  110. data/test/dummy/public/500.html +0 -66
  111. data/test/dummy/public/favicon.ico +0 -0
  112. data/test/support/assertions.rb +0 -11
  113. data/test/support/temping.rb +0 -25
  114. data/test/test_helper.rb +0 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d5091f9668515416b73453756daa5f78598f6d35f0f60073a947719e535110f
4
- data.tar.gz: 428450c43db5adb8510debfb9e895efba1f21649355255fe3eb7a2ea16c82642
3
+ metadata.gz: e69330f83b83c3adcd1397554bc916ee163acf3797b8be5b8f96f1960e2d8a3e
4
+ data.tar.gz: 8f6e09d3af78bb9aaf01374437653b25070c2c328a0ffeaedc800378a10074f6
5
5
  SHA512:
6
- metadata.gz: fffc1ffd284a2448ac5c74dae34a3bbe45eda4836b0e22a0af072ab6353f16b1386385254b1cdb2b87e4776372869d8cee67bf0a066f180b85bd45ef7ed3cc33
7
- data.tar.gz: 0011f60f1e37542911a72cce23cc8c8430310f8263fe7ba73aba30230ec8ff3f6e7ee9df72f42d8db6ff54e732a86aa49f1e6489a5b44d51bf1bd6c68821ebcd
6
+ metadata.gz: 4b9d75665c0524cd5ad18c5aa21aaf59b3e7a75b63c88fb332cd3f600de4f2a9fa073b985f5155b81d54f177b25da5fba5dde494fa6eed28e16086a955082012
7
+ data.tar.gz: 94c14b73d06c92803b196fde5ca0572c9545b2ef8b89bc91e88755c5aed9c4e51087ab59a53a94f1fbcacf3f7a58a25cac38dfb4f81d7096ee4d03c7796632b4
data/README.md CHANGED
@@ -1,41 +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
5
 
6
- * index unindexed foreign keys
7
- * detect extraneous indexes
8
- * detect unindexed `deleted_at` columns
9
- * detect missing foreign key constraints
10
- * detect models referencing undefined tables
11
- * detect uniqueness validations not backed by an unique index
12
- * detect missing non-`NULL` constraints
13
- * detect missing presence validations
14
- * detect incorrect presence validations on boolean columns
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)
15
17
 
16
- More features coming soon!
18
+ It can also:
17
19
 
18
- Want to suggest a feature? Just shoot me [an email](mailto:contact@gregnavis.com).
20
+ * index unindexed foreign keys - [`active_record_doctor:unindexed_foreign_keys`](#indexing-unindexed-foreign-keys)
19
21
 
20
- [<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)
21
23
 
22
24
  ## Installation
23
25
 
24
- The preferred installation method is adding `active_record_doctor` to your
25
- `Gemfile`:
26
+ In order to use the latest production release, please add the following to
27
+ your `Gemfile`:
26
28
 
27
29
  ```ruby
28
30
  gem 'active_record_doctor', group: :development
29
31
  ```
30
32
 
31
- Then run:
33
+ and run `bundle install`. If you'd like to use the most recent development
34
+ version then use this instead:
32
35
 
33
- ```bash
34
- bundle install
36
+ ```ruby
37
+ gem 'active_record_doctor', github: 'gregnavis/active_record_doctor'
35
38
  ```
36
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
63
+ ```
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
+
37
69
  ## Usage
38
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
+
39
145
  ### Indexing Unindexed Foreign Keys
40
146
 
41
147
  Foreign keys should be indexed unless it's proven ineffective. However, Rails
@@ -65,6 +171,11 @@ three-step process:
65
171
  bundle exec rake db:migrate
66
172
  ```
67
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
+
68
179
  ### Removing Extraneous Indexes
69
180
 
70
181
  Let me illustrate with an example. Consider a `users` table with columns
@@ -106,6 +217,11 @@ example, if there's a unique index on `users.login` and a non-unique index on
106
217
  `users.login, users.domain` then the tool will _not_ suggest dropping
107
218
  `users.login` as it could violate the uniqueness assumption.
108
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
+
109
225
  ### Detecting Unindexed `deleted_at` Columns
110
226
 
111
227
  If you soft-delete some models (e.g. with `paranoia`) then you need to modify
@@ -124,6 +240,13 @@ This will print a list of indexes that don't have the `deleted_at IS NULL`
124
240
  clause. Currently, `active_record_doctor` cannot automatically generate
125
241
  appropriate migrations. You need to do that manually.
126
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
+
127
250
  ### Detecting Missing Foreign Key Constraints
128
251
 
129
252
  If `users.profile_id` references a row in `profiles` then this can be expressed
@@ -141,20 +264,8 @@ keys with the following command:
141
264
  bundle exec rake active_record_doctor:missing_foreign_keys
142
265
  ```
143
266
 
144
- The output will look like:
145
-
146
- ```
147
- users profile_id
148
- comments user_id article_id
149
- ```
150
-
151
- Tables are listed one per line. Each line starts with a table name followed by
152
- column names that should have a foreign key constraint. In the example above,
153
- `users.profile_id`, `comments.user_id`, and `comments.article_id` lack a foreign
154
- key constraint.
155
-
156
- In order to add a foreign key constraint to `users.profile_id` use the following
157
- migration:
267
+ In order to add a foreign key constraint to `users.profile_id` use a migration
268
+ like:
158
269
 
159
270
  ```ruby
160
271
  class AddForeignKeyConstraintToUsersProfileId < ActiveRecord::Migration
@@ -164,6 +275,11 @@ class AddForeignKeyConstraintToUsersProfileId < ActiveRecord::Migration
164
275
  end
165
276
  ```
166
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
+
167
283
  ### Detecting Models Referencing Undefined Tables
168
284
 
169
285
  Active Record guesses the table name based on the class name. There are a few
@@ -186,13 +302,16 @@ If there a model references an undefined table then you'll see a message like
186
302
  this:
187
303
 
188
304
  ```
189
- The following models reference undefined tables:
190
- Contract (the table contract_records is undefined)
305
+ Contract references a non-existent table or view named contract_records
191
306
  ```
192
307
 
193
308
  On top of that `rake` will exit with status code of 1. This allows you to use
194
309
  this check as part of your Continuous Integration pipeline.
195
310
 
311
+ Supported configuration options:
312
+
313
+ - `ignore_models` - models whose underlying tables should not be checked for existence.
314
+
196
315
  ### Detecting Uniqueness Validations not Backed by an Index
197
316
 
198
317
  A model-level uniqueness validations should be backed by a database index in
@@ -208,12 +327,16 @@ bundle exec rake active_record_doctor:missing_unique_indexes
208
327
  If there are such indexes then the command will print:
209
328
 
210
329
  ```
211
- The following indexes should be created to back model-level uniqueness validations:
212
- users: email
330
+ add a unique index on users(email) - validating uniqueness in the model without an index can lead to duplicates
213
331
  ```
214
332
 
215
333
  This means that you should create a unique index on `users.email`.
216
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
+
217
340
  ### Detecting Missing Non-`NULL` Constraints
218
341
 
219
342
  If there's an unconditional presence validation on a column then it should be
@@ -229,14 +352,19 @@ bundle exec rake active_record_doctor:missing_non_null_constraint
229
352
  The output of the command is similar to:
230
353
 
231
354
  ```
232
- The following columns should be marked as `null: false`:
233
- users: name
234
-
355
+ add `NOT NULL` to users.name - models validates its presence but it's not non-NULL in the database
235
356
  ```
236
357
 
237
358
  You can mark the columns mentioned in the output as `null: false` by creating a
238
359
  migration and calling `change_column_null`.
239
360
 
361
+ This validator skips models whose corresponding database tables don't exist.
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
+
240
368
  ### Detecting Missing Presence Validations
241
369
 
242
370
  If a column is marked as `null: false` then it's likely it should have the
@@ -251,12 +379,19 @@ bundle exec rake active_record_doctor:missing_presence_validation
251
379
  The output of the command looks like this:
252
380
 
253
381
  ```
254
- The following models and columns should have presence validations:
255
- 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
256
384
  ```
257
385
 
258
386
  This means `User` should have a presence validator on `email` and `name`.
259
387
 
388
+ This validator skips models whose corresponding database tables don't exist.
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
+
260
395
  ### Detecting Incorrect Presence Validations on Boolean Columns
261
396
 
262
397
  A boolean column's presence should be validated using inclusion or exclusion
@@ -271,13 +406,122 @@ bundle exec rake active_record_doctor:incorrect_boolean_presence_validation
271
406
  The output of the command looks like this:
272
407
 
273
408
  ```
274
- The presence of the following boolean columns is validated incorrectly:
275
- User: active
409
+ replace the `presence` validator on User.active with `inclusion` - `presence` can't be used on booleans
276
410
  ```
277
411
 
278
412
  This means `active` is validated with `presence: true` instead of
279
413
  `inclusion: { in: [true, false] }` or `exclusion: { in: [nil] }`.
280
414
 
415
+ This validator skips models whose corresponding database tables don't exist.
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
+
422
+ ### Detecting Incorrect `dependent` Option on Associations
423
+
424
+ Cascading model deletions can be sped up with `dependent: :delete_all` (to
425
+ delete all dependent models with one SQL query) but only if the deleted models
426
+ have no callbacks as they're skipped.
427
+
428
+ This can lead to two types of errors:
429
+
430
+ - Using `delete_all` when dependent models define callbacks - they will NOT be
431
+ invoked.
432
+ - Using `destroy` when dependent models define no callbacks - dependent models
433
+ will be loaded one-by-one with no reason
434
+
435
+ In order to detect associations affected by the two aforementioned problems run
436
+ the following command:
437
+
438
+ ```
439
+ bundle exec rake active_record_doctor:incorrect_dependent_option
440
+ ```
441
+
442
+ The output of the command looks like this:
443
+
444
+ ```
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
447
+ ```
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
+
514
+ ## Ruby and Rails Compatibility Policy
515
+
516
+ The goal of the policy is to ensure proper functioning in reasonable
517
+ combinations of Ruby and Rails versions. Specifically:
518
+
519
+ 1. If a Rails version is officially supported by the Rails Core Team then it's
520
+ supported by `active_record_doctor`.
521
+ 2. If a Ruby version is compatible with a supported Rails version then it's
522
+ also supported by `active_record_doctor`.
523
+ 3. Only most recent teeny Ruby versions and patch Rails versions are supported.
524
+
281
525
  ## Author
282
526
 
283
527
  This gem is developed and maintained by [Greg Navis](http://www.gregnavis.com).
@@ -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