active_record_doctor 1.14.0 → 2.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89e283c2844a768587302646735a080d8db2b3514775083cc6bc74a25e7aff18
4
- data.tar.gz: 975a75da213c565dba3ee7918ffe64d5b75d812059b8080d6a5134a2fb27028f
3
+ metadata.gz: 8bae986a04fe63835ce7e190f32471cc06a370972161c487cac195ef33f73269
4
+ data.tar.gz: c5a2f92f0b31ec1faedfd65fc5abe9ef656f75b9fa933719e57f180b4fb0f28d
5
5
  SHA512:
6
- metadata.gz: ca4a89d161ef3a57ee325ea6551417b9e542967d2f932f383eca6e7394ba99f02cf08ac9cd699e98f1f59018669405220fe1175f80d31f60576fc606d2d3f5f6
7
- data.tar.gz: 70644778b22b9fe2892d12704cdfb3d81337702e8770ca94f8410694e3a8b12ad931bc511bc5188f77726849fca7c87c1ce8fec059d647ea4225adf6ab1363ef
6
+ metadata.gz: 7759e1effdfcea2382966f8973c2c132bda909b847a14914fb7a46b795a7d0407f9605efa39dfece9d889ee65b35f334f78e6664928fbbe03e349f792845ae7f
7
+ data.tar.gz: c563de0bbf91966325eddd2bafedb3dadcc07ca39e7c27a529fdb544113eadba4f2373e9d618a074205a8716687f7684f232a212711fcf5d50fc1484999b1074
data/README.md CHANGED
@@ -15,12 +15,16 @@ can detect:
15
15
  * incorrect values of `dependent` on associations - [`active_record_doctor:incorrect_dependent_option`](#detecting-incorrect-dependent-option-on-associations)
16
16
  * primary keys having short integer types - [`active_record_doctor:short_primary_key_type`](#detecting-primary-keys-having-short-integer-types)
17
17
  * mismatched foreign key types - [`active_record_doctor:mismatched_foreign_key_type`](#detecting-mismatched-foreign-key-types)
18
+ * tables without primary keys - [`active_record_doctor:table_without_primary_key`](#detecting-tables-without-primary-keys)
19
+ * tables without timestamps - [`active_record_doctor:table_without_timestamps`](#detecting-tables-without-timestamps)
18
20
 
19
21
  It can also:
20
22
 
21
23
  * index unindexed foreign keys - [`active_record_doctor:unindexed_foreign_keys`](#indexing-unindexed-foreign-keys)
22
24
 
23
- [![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)
25
+ [![Build Status](https://github.com/gregnavis/active_record_doctor/actions/workflows/lint.yml/badge.svg?branch=master)](https://github.com/gregnavis/active_record_doctor/actions/workflows/lint.yml)
26
+ [![Build Status](https://github.com/gregnavis/active_record_doctor/actions/workflows/mysql.yml/badge.svg?branch=master)](https://github.com/gregnavis/active_record_doctor/actions/workflows/mysql.yml)
27
+ [![Build Status](https://github.com/gregnavis/active_record_doctor/actions/workflows/postgresql.yml/badge.svg?branch=master)](https://github.com/gregnavis/active_record_doctor/actions/workflows/postgresql.yml)
24
28
 
25
29
  ## Installation
26
30
 
@@ -28,14 +32,14 @@ In order to use the latest production release, please add the following to
28
32
  your `Gemfile`:
29
33
 
30
34
  ```ruby
31
- gem 'active_record_doctor', group: :development
35
+ gem 'active_record_doctor', group: [:development, :test]
32
36
  ```
33
37
 
34
38
  and run `bundle install`. If you'd like to use the most recent development
35
39
  version then use this instead:
36
40
 
37
41
  ```ruby
38
- gem 'active_record_doctor', github: 'gregnavis/active_record_doctor'
42
+ gem 'active_record_doctor', github: 'gregnavis/active_record_doctor', group: [:development, :test]
39
43
  ```
40
44
 
41
45
  That's it when it comes to Rails projects. If your project doesn't use Rails
@@ -101,6 +105,17 @@ bundle exec rake active_record_doctor:extraneous_indexes:help
101
105
  This will show the detector help text in the terminal, along with supported
102
106
  configuration options, their meaning, and whether they're global or local.
103
107
 
108
+ ### Debug Logging
109
+
110
+ It may be that `active_record_doctor` fails with an exception and it is hard to tell
111
+ what went wrong. For easier debugging, use `ACTIVE_RECORD_DOCTOR_DEBUG` environment variable.
112
+ If `active_record_doctor` fails for some reason for your application, feel free
113
+ to open an issue or a PR with the fix.
114
+
115
+ ```
116
+ ACTIVE_RECORD_DOCTOR_DEBUG=1 bundle exec rake active_record_doctor
117
+ ```
118
+
104
119
  ### Configuration
105
120
 
106
121
  `active_record_doctor` can be configured to better suit your project's needs.
@@ -146,28 +161,26 @@ obtained via the help mechanism described in the previous section.
146
161
 
147
162
  ### Regexp-Based Ignores
148
163
 
149
- Settings like `ignore_tables`, `ignore_indexes`, and so on accept list of
150
- identifiers to ignore. These can be either:
164
+ Settings like `ignore_tables`, `ignore_indexes`, `ignore_models` and so on
165
+ accept list of identifiers to ignore. These can be either:
151
166
 
152
167
  1. Strings - in which case an exact match is needed.
153
168
  2. Regexps - which are matched against object names, and matching ones are
154
169
  excluded from output.
155
170
 
156
- For example, to ignore all tables starting with `legacy_` you can write:
171
+ For example, to ignore all tables starting with `legacy_` and all models under
172
+ the `Legacy::` namespace you can write:
157
173
 
158
174
  ```ruby
159
175
  ActiveRecordDoctor.configure do
160
176
  global :ignore_tables, [
161
- # Ignore internal Rails-related tables.
162
- "ar_internal_metadata",
163
- "schema_migrations",
164
- "active_storage_blobs",
165
- "active_storage_attachments",
166
- "action_text_rich_texts",
167
-
168
177
  # Ignore all legacy tables.
169
178
  /^legacy_/
170
179
  ]
180
+ global :ignore_models, [
181
+ # Ignore all legacy models.
182
+ /^Legacy::/
183
+ ]
171
184
  end
172
185
  ```
173
186
 
@@ -256,7 +269,7 @@ Supported configuration options:
256
269
 
257
270
  - `enabled` - set to `false` to disable the detector altogether
258
271
  - `ignore_tables` - tables whose indexes should never be reported as extraneous.
259
- - `ignore_columns` - indexes that should never be reported as extraneous.
272
+ - `ignore_indexes` - indexes that should never be reported as extraneous.
260
273
 
261
274
  ### Detecting Unindexed `deleted_at` Columns
262
275
 
@@ -358,9 +371,9 @@ Supported configuration options:
358
371
 
359
372
  ### Detecting Uniqueness Validations not Backed by an Index
360
373
 
361
- Model-level uniqueness validations and `has_one` associations should be backed
362
- by a database index in order to be robust. Otherwise you risk inserting
363
- duplicate values under a heavy load.
374
+ Model-level uniqueness validations, `has_one` and `has_and_belongs_to_many`
375
+ associations should be backed by a database index in order to be robust.
376
+ Otherwise you risk inserting duplicate values under a heavy load.
364
377
 
365
378
  In order to detect such validations run:
366
379
 
@@ -382,12 +395,15 @@ Supported configuration options:
382
395
  - `ignore_models` - models whose uniqueness validators should not be checked.
383
396
  - `ignore_columns` - specific validators, written as Model(column1, ...), that
384
397
  should not be checked.
398
+ - `ignore_join_tables` - join tables that should not be checked for existence
399
+ of unique indexes.
385
400
 
386
401
  ### Detecting Missing Non-`NULL` Constraints
387
402
 
388
403
  If there's an unconditional presence validation on a column then it should be
389
404
  marked as non-`NULL`-able at the database level or should have a `IS NOT NULL`
390
- constraint.
405
+ constraint. Timestamp columns are also expected to be made `NOT NULL` as they're
406
+ managed automatically by Active Record.
391
407
 
392
408
  In order to detect columns whose presence is required but that are marked
393
409
  `null: true` in the database run the following command:
@@ -417,7 +433,8 @@ Supported configuration options:
417
433
  ### Detecting Missing Presence Validations
418
434
 
419
435
  If a column is marked as `null: false` then it's likely it should have the
420
- corresponding presence validator.
436
+ corresponding presence validator or an appropriately configured inclusion or
437
+ exclusion validation.
421
438
 
422
439
  In order to detect models lacking these validations run:
423
440
 
@@ -442,6 +459,7 @@ Supported configuration options:
442
459
  - `ignore_models` - models whose underlying tables' columns should not be checked.
443
460
  - `ignore_attributes` - specific attributes, written as Model.attribute, that
444
461
  should not be checked.
462
+ - `ignore_columns_with_default` - set to `true` to ignore columns with default values.
445
463
 
446
464
  ### Detecting Incorrect Presence Validations on Boolean Columns
447
465
 
@@ -602,6 +620,52 @@ Supported configuration options:
602
620
  - `ignore_columns` - foreign keys, written as table.column, that should not be
603
621
  checked.
604
622
 
623
+ ### Detecting Tables Without Primary Keys
624
+
625
+ Tables should have primary keys. Otherwise, it becomes problematic to easily find a specific record,
626
+ logical replication in PostgreSQL will be troublesome, because all the rows need to be unique
627
+ in the table then etc.
628
+
629
+ Running the command below will list all tables without primary keys:
630
+
631
+ ```
632
+ bundle exec rake active_record_doctor:table_without_primary_key
633
+ ```
634
+
635
+ The output of the command looks like this:
636
+
637
+ ```
638
+ add a primary key to companies
639
+ ```
640
+
641
+ Supported configuration options:
642
+
643
+ - `enabled` - set to `false` to disable the detector altogether
644
+ - `ignore_tables` - tables whose primary key existence should not be checked
645
+
646
+ ### Detecting Tables Without Timestamps
647
+
648
+ Tables should have timestamp columns (`created_at`/`updated_at`). Otherwise, it becomes problematic
649
+ to easily find when the record was created/updated, if the table is active or can be removed,
650
+ automatic Rails cache expiration after record updates is not possible.
651
+
652
+ Running the command below will list all tables without default timestamp columns:
653
+
654
+ ```
655
+ bundle exec rake active_record_doctor:table_without_timestamps
656
+ ```
657
+
658
+ The output of the command looks like this:
659
+
660
+ ```
661
+ add a created_at column to companies
662
+ ```
663
+
664
+ Supported configuration options:
665
+
666
+ - `enabled` - set to `false` to disable the detector altogether
667
+ - `ignore_tables` - tables whose timestamp columns existence should not be checked
668
+
605
669
  ## Ruby and Rails Compatibility Policy
606
670
 
607
671
  The goal of the policy is to ensure proper functioning in reasonable
@@ -0,0 +1,17 @@
1
+ module ActiveRecordDoctor
2
+ module Adapters
3
+ class Database
4
+ def initialize(connection)
5
+ @connection = connection
6
+ end
7
+
8
+ def tables
9
+ connection.tables
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :connection
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module ActiveRecordDoctor
2
+ module Adapters
3
+ class Table
4
+ def initialize(connection, table_name)
5
+ @connection = connection
6
+ @table_name = table_name
7
+ end
8
+
9
+ def columns
10
+ connection.columns(table_name)
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :connection, :table_name
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveRecordDoctor
2
+ module Adapters
3
+ class Validators
4
+ def initialize(model_class, kind: :any)
5
+ @model_class = model_class
6
+ end
7
+
8
+ def each
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :model_class
14
+ end
15
+ end
16
+ end
@@ -47,17 +47,27 @@ ActiveRecordDoctor.configure do
47
47
  detector :missing_presence_validation,
48
48
  enabled: true,
49
49
  ignore_models: [],
50
- ignore_attributes: []
50
+ ignore_attributes: [],
51
+ ignore_columns_with_default: false
51
52
 
52
53
  detector :missing_unique_indexes,
53
54
  enabled: true,
54
55
  ignore_models: [],
55
- ignore_columns: []
56
+ ignore_columns: [],
57
+ ignore_join_tables: []
56
58
 
57
59
  detector :short_primary_key_type,
58
60
  enabled: true,
59
61
  ignore_tables: []
60
62
 
63
+ detector :table_without_primary_key,
64
+ enabled: true,
65
+ ignore_tables: []
66
+
67
+ detector :table_without_timestamps,
68
+ enabled: true,
69
+ ignore_tables: []
70
+
61
71
  detector :undefined_table_references,
62
72
  enabled: true,
63
73
  ignore_models: []
@@ -136,8 +136,7 @@ module ActiveRecordDoctor
136
136
  end
137
137
 
138
138
  def check_constraints(table_name)
139
- # ActiveRecord 6.1+
140
- if connection.respond_to?(:supports_check_constraints?) && connection.supports_check_constraints?
139
+ if connection.supports_check_constraints?
141
140
  connection.check_constraints(table_name).select(&:validated?).map(&:expression)
142
141
  elsif Utils.postgresql?(connection)
143
142
  definitions =
@@ -157,7 +156,7 @@ module ActiveRecordDoctor
157
156
  end
158
157
 
159
158
  def models
160
- ActiveRecord::Base.descendants
159
+ ActiveRecord::Base.descendants.sort_by(&:name)
161
160
  end
162
161
 
163
162
  def underscored_name
@@ -245,6 +244,13 @@ module ActiveRecordDoctor
245
244
  end
246
245
  end
247
246
 
247
+ def looks_like_foreign_key?(column)
248
+ type = column.type.to_s
249
+
250
+ column.name.end_with?("_id") &&
251
+ (type == "integer" || type.include?("uuid"))
252
+ end
253
+
248
254
  def each_foreign_key(table_name)
249
255
  log("Iterating over foreign keys on #{table_name}") do
250
256
  connection.foreign_keys(table_name).each do |foreign_key|
@@ -256,15 +262,8 @@ module ActiveRecordDoctor
256
262
  end
257
263
 
258
264
  def each_table(except: [])
259
- tables =
260
- if ActiveRecord::VERSION::STRING >= "5.1"
261
- connection.tables
262
- else
263
- connection.data_sources
264
- end
265
-
266
265
  log("Iterating over tables") do
267
- tables.each do |table|
266
+ connection.tables.each do |table|
268
267
  case
269
268
  when ignored?(table, except)
270
269
  log("#{table} - ignored via the configuration; skipping")
@@ -323,7 +322,7 @@ module ActiveRecordDoctor
323
322
  end
324
323
 
325
324
  def ignored?(name, patterns)
326
- patterns.any? { |pattern| pattern === name } # rubocop:disable Style/CaseEquality
325
+ patterns.any? { |pattern| pattern === name || name == pattern.to_s } # rubocop:disable Style/CaseEquality
327
326
  end
328
327
  end
329
328
  end
@@ -81,11 +81,12 @@ module ActiveRecordDoctor
81
81
 
82
82
  case [index1.unique, index2.unique]
83
83
  when [true, true]
84
- (index2_columns - index1_columns).empty?
84
+ contains_all?(index1_columns, index2_columns)
85
85
  when [true, false]
86
86
  false
87
87
  else
88
- prefix?(index1_columns, index2_columns)
88
+ prefix?(index1_columns, index2_columns) &&
89
+ contains_all?(index2_columns + includes(index2), includes(index1))
89
90
  end
90
91
  end
91
92
 
@@ -96,6 +97,14 @@ module ActiveRecordDoctor
96
97
  def prefix?(lhs, rhs)
97
98
  lhs.count <= rhs.count && rhs[0...lhs.count] == lhs
98
99
  end
100
+
101
+ def contains_all?(array1, array2)
102
+ (array2 - array1).empty?
103
+ end
104
+
105
+ def includes(index)
106
+ index.respond_to?(:include) ? Array(index.include) : []
107
+ end
99
108
  end
100
109
  end
101
110
  end
@@ -58,7 +58,7 @@ module ActiveRecordDoctor
58
58
  # model lacks the next leg in the :through relationship. For
59
59
  # instance, if user has many comments through posts then a nil
60
60
  # source_reflection means that Post doesn't define +has_many :comments+.
61
- if through?(association) && association.source_reflection.nil?
61
+ if association.through_reflection? && association.source_reflection.nil?
62
62
  log("through association with nil source_reflection")
63
63
 
64
64
  through_association = model.reflect_on_association(association.options.fetch(:through))
@@ -77,14 +77,14 @@ module ActiveRecordDoctor
77
77
  associated_models: [through_association.klass.name],
78
78
  associated_models_type: "join"
79
79
  )
80
- next
81
80
  end
81
+ next
82
82
  end
83
83
 
84
84
  associated_models, associated_models_type =
85
85
  if association.polymorphic?
86
86
  [models_having_association_with_options(as: association.name), nil]
87
- elsif through?(association)
87
+ elsif association.through_reflection?
88
88
  [[association.source_reflection.active_record], "join"]
89
89
  else
90
90
  [[association.klass], nil]
@@ -159,10 +159,6 @@ module ActiveRecordDoctor
159
159
  end
160
160
  end
161
161
 
162
- def through?(reflection)
163
- reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
164
- end
165
-
166
162
  def defines_destroy_callbacks?(model)
167
163
  # Destroying an associated model involves loading it first hence
168
164
  # initialize and find are present. If they are defined on the model
@@ -33,21 +33,23 @@ module ActiveRecordDoctor
33
33
  def detect
34
34
  each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
35
35
  each_attribute(model, except: config(:ignore_attributes), type: [:string, :text]) do |column|
36
- model_maximum = maximum_allowed_by_validations(model, column.name.to_sym)
37
- next if model_maximum == column.limit
36
+ model_maximum = maximum_allowed_by_length_validation(model, column.name.to_sym)
37
+ database_maximum = column.limit
38
+ next if model_maximum == database_maximum
39
+ next if database_maximum && covered_by_inclusion_validation?(model, column.name.to_sym, database_maximum)
38
40
 
39
41
  problem!(
40
42
  model: model.name,
41
43
  attribute: column.name,
42
44
  table: model.table_name,
43
- database_maximum: column.limit,
45
+ database_maximum: database_maximum,
44
46
  model_maximum: model_maximum
45
47
  )
46
48
  end
47
49
  end
48
50
  end
49
51
 
50
- def maximum_allowed_by_validations(model, column)
52
+ def maximum_allowed_by_length_validation(model, column)
51
53
  length_validator = model.validators.find do |validator|
52
54
  validator.kind == :length &&
53
55
  validator.options.include?(:maximum) &&
@@ -55,6 +57,20 @@ module ActiveRecordDoctor
55
57
  end
56
58
  length_validator ? length_validator.options[:maximum] : nil
57
59
  end
60
+
61
+ def covered_by_inclusion_validation?(model, column, limit)
62
+ inclusion_validator = model.validators.find do |validator|
63
+ validator.kind == :inclusion &&
64
+ validator.attributes.include?(column) &&
65
+ [:if, :unless].none? { |option| validator.options.key?(option) } &&
66
+ [:allow_nil, :allow_blank].none? { |option| validator.options[option] == true }
67
+ end
68
+
69
+ return false if !inclusion_validator
70
+
71
+ values = inclusion_validator.options[:in] || inclusion_validator.options[:within]
72
+ values.is_a?(Array) && values.all? { |value| value.is_a?(String) && value.size <= limit }
73
+ end
58
74
  end
59
75
  end
60
76
  end
@@ -28,19 +28,16 @@ module ActiveRecordDoctor
28
28
  # We need to skip polymorphic associations as they can reference
29
29
  # multiple tables but a foreign key constraint can reference
30
30
  # a single predefined table.
31
- next unless named_like_foreign_key?(column)
31
+ next unless looks_like_foreign_key?(column)
32
32
  next if foreign_key?(table, column)
33
33
  next if polymorphic_foreign_key?(table, column)
34
+ next if model_destroyed_async?(table, column)
34
35
 
35
36
  problem!(table: table, column: column.name)
36
37
  end
37
38
  end
38
39
  end
39
40
 
40
- def named_like_foreign_key?(column)
41
- column.name.end_with?("_id")
42
- end
43
-
44
41
  def foreign_key?(table, column)
45
42
  connection.foreign_keys(table).any? do |foreign_key|
46
43
  foreign_key.options[:column] == column.name
@@ -53,6 +50,18 @@ module ActiveRecordDoctor
53
50
  another_column.name == type_column_name
54
51
  end
55
52
  end
53
+
54
+ def model_destroyed_async?(table, column)
55
+ # Check if there are any models having `has_many ..., dependent: :destroy_async`
56
+ # referencing the specified table.
57
+ models.any? do |model|
58
+ model.reflect_on_all_associations(:has_many).any? do |reflection|
59
+ reflection.options[:dependent] == :destroy_async &&
60
+ reflection.foreign_key == column.name &&
61
+ reflection.klass.table_name == table
62
+ end
63
+ end
64
+ end
56
65
  end
57
66
  end
58
67
  end
@@ -18,15 +18,27 @@ module ActiveRecordDoctor
18
18
 
19
19
  private
20
20
 
21
+ TIMESTAMPS = ["created_at", "created_on", "updated_at", "updated_on"].freeze
22
+
21
23
  def message(column:, table:)
22
- "add `NOT NULL` to #{table}.#{column} - models validates its presence but it's not non-NULL in the database"
24
+ if TIMESTAMPS.include?(column)
25
+ <<~WARN.squish
26
+ add `NOT NULL` to #{table}.#{column} - timestamp columns are set
27
+ automatically by Active Record and allowing NULL may lead to
28
+ inconsistencies introduced by bulk operations
29
+ WARN
30
+ else
31
+ "add `NOT NULL` to #{table}.#{column} - models validates its presence but it's not non-NULL in the database"
32
+ end
23
33
  end
24
34
 
25
35
  def detect
26
36
  table_models = models.select(&:table_exists?).group_by(&:table_name)
37
+ views = connection.views
27
38
 
28
39
  table_models.each do |table, models|
29
40
  next if ignored?(table, config(:ignore_tables))
41
+ next if views.include?(table)
30
42
 
31
43
  concrete_models = models.reject do |model|
32
44
  model.abstract_class? || sti_base_model?(model)
@@ -36,6 +48,7 @@ module ActiveRecordDoctor
36
48
  next if ignored?("#{table}.#{column.name}", config(:ignore_columns))
37
49
  next if !column.null
38
50
  next if !concrete_models.all? { |model| non_null_needed?(model, column) }
51
+ next if sti_column?(models, column.name)
39
52
  next if not_null_check_constraint_exists?(table, column)
40
53
 
41
54
  problem!(column: column.name, table: table)
@@ -49,6 +62,8 @@ module ActiveRecordDoctor
49
62
  end
50
63
 
51
64
  def non_null_needed?(model, column)
65
+ return true if TIMESTAMPS.include?(column.name)
66
+
52
67
  belongs_to = model.reflect_on_all_associations(:belongs_to).find do |reflection|
53
68
  reflection.foreign_key == column.name ||
54
69
  (reflection.polymorphic? && reflection.foreign_type == column.name)
@@ -66,10 +81,29 @@ module ActiveRecordDoctor
66
81
  model.validators.select do |validator|
67
82
  validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
68
83
  !validator.options[:allow_nil] &&
69
- !validator.options[:if] &&
70
- !validator.options[:unless]
84
+ validator.options[:on].blank? &&
85
+ (rails_belongs_to_presence_validator?(validator) || !conditional_validator?(validator))
71
86
  end
72
87
  end
88
+
89
+ def sti_column?(models, column_name)
90
+ models.any? { |model| model.inheritance_column == column_name }
91
+ end
92
+
93
+ def rails_belongs_to_presence_validator?(validator)
94
+ ActiveRecord.version >= Gem::Version.new("7.1") &&
95
+ !ActiveRecord.belongs_to_required_validates_foreign_key &&
96
+ validator.options[:message] == :required &&
97
+ proc_from_activerecord?(validator.options[:if])
98
+ end
99
+
100
+ def conditional_validator?(validator)
101
+ validator.options[:if] || validator.options[:unless]
102
+ end
103
+
104
+ def proc_from_activerecord?(object)
105
+ object.is_a?(Proc) && object.binding.receiver.name.start_with?("ActiveRecord::")
106
+ end
73
107
  end
74
108
  end
75
109
  end
@@ -13,79 +13,170 @@ module ActiveRecordDoctor
13
13
  },
14
14
  ignore_attributes: {
15
15
  description: "specific attributes, written as Model.attribute, that should not be checked"
16
+ },
17
+ ignore_columns_with_default: {
18
+ description: "ignore columns with default values, should be provided as boolean"
16
19
  }
17
20
  }
18
21
 
19
22
  private
20
23
 
21
- def message(column:, model:)
22
- "add a `presence` validator to #{model}.#{column} - it's NOT NULL but lacks a validator"
24
+ def message(type:, column_or_association:, model:)
25
+ case type
26
+ when :missing_validator
27
+ "add a `presence` validator to #{model}.#{column_or_association} - it's NOT NULL but lacks a validator"
28
+ when :optional_association
29
+ "add `optional: false` to #{model}.#{column_or_association} - the foreign key #{column_or_association}_id is NOT NULL" # rubocop:disable Layout/LineLength
30
+ when :optional_polymorphic_association
31
+ "add `optional: false` to #{model}.#{column_or_association} - the foreign key #{column_or_association}_id or type #{column_or_association}_type are NOT NULL" # rubocop:disable Layout/LineLength
32
+ end
23
33
  end
24
34
 
25
35
  def detect
26
36
  each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
27
- each_attribute(model, except: config(:ignore_attributes)) do |column|
28
- next unless validator_needed?(model, column)
29
- next if validator_present?(model, column)
37
+ # List all columns and then remove those that don't need or don't have
38
+ # a missing validator.
39
+ problematic_columns = connection.columns(model.table_name)
40
+ problematic_columns.reject! do |column|
41
+ # The primary key, timestamps, and counter caches are special
42
+ # columns that are automatically managed by Rails and don't need
43
+ # an explicit presence validator.
44
+ column.name == model.primary_key ||
45
+ ["created_at", "updated_at", "created_on", "updated_on"].include?(column.name) ||
46
+ counter_cache_column?(model, column) ||
47
+
48
+ # NULL-able columns don't need a presence validator as they can be
49
+ # set to NULL after all. A check constraint (column IS NOT NULL)
50
+ # is an alternative approach and the absence of such constraint is
51
+ # tested below.
52
+ (column.null && !not_null_check_constraint_exists?(model.table_name, column)) ||
53
+
54
+ # If requested, columns with a default value don't need presence
55
+ # validation as they'd have the default value substituted automatically.
56
+ (config(:ignore_columns_with_default) && (column.default || column.default_function)) ||
57
+
58
+ # Explicitly ignored columns should be skipped.
59
+ config(:ignore_attributes).include?("#{model.name}.#{column.name}")
60
+ end
30
61
 
31
- problem!(column: column.name, model: model.name)
62
+ # At this point the only columns that are left are those that DO
63
+ # need presence validation in the model. Let's iterate over all
64
+ # validators to see which columns are actually validated, but before
65
+ # we do that let's define a map for quickly translating foreign key
66
+ # names to belongs_to association names.
67
+ column_name_to_association_name = {}
68
+ model.reflect_on_all_associations(:belongs_to).each do |reflection|
69
+ column_name_to_association_name[reflection.foreign_key] = reflection.name
70
+ if reflection.polymorphic?
71
+ column_name_to_association_name[reflection.foreign_type] = reflection.name
72
+ end
32
73
  end
33
- end
34
- end
35
74
 
36
- def validator_needed?(model, column)
37
- ![model.primary_key, "created_at", "updated_at", "created_on", "updated_on"].include?(column.name) &&
38
- (!column.null || not_null_check_constraint_exists?(model.table_name, column))
39
- end
75
+ # We're now ready to iterate over the validators and remove columns
76
+ # that are validated directly or via an association name.
77
+ model.validators.each do |validator|
78
+ problematic_columns.reject! do |column|
79
+ # Translate a foreign key or type to the association name.
80
+ attribute = column_name_to_association_name[column.name] || column.name.to_sym
81
+
82
+ case validator
83
+
84
+ # A regular presence validator is enough if the column name is
85
+ # listed among the attributes it's validating.
86
+ when ActiveRecord::Validations::PresenceValidator
87
+ validator.attributes.include?(attribute)
88
+
89
+ # An inclusion validator ensures the column is not nil if it covers
90
+ # the column and nil is NOT one of the values it allows.
91
+ when ActiveModel::Validations::InclusionValidator
92
+ validator_items = inclusion_or_exclusion_validator_items(validator)
93
+ validator.attributes.include?(attribute) &&
94
+ (validator_items.is_a?(Proc) || validator_items.exclude?(nil))
95
+
96
+ # An exclusion validator ensures the column is not nil if it covers
97
+ # the column and excludes nil as an allowed value explicitly.
98
+ when ActiveModel::Validations::ExclusionValidator
99
+ validator_items = inclusion_or_exclusion_validator_items(validator)
100
+ validator.attributes.include?(attribute) &&
101
+ (validator_items.is_a?(Proc) || validator_items.include?(nil))
102
+
103
+ end
104
+ end
105
+ end
40
106
 
41
- def validator_present?(model, column)
42
- if column.type == :boolean
43
- inclusion_validator_present?(model, column) ||
44
- exclusion_validator_present?(model, column)
45
- else
46
- presence_validator_present?(model, column)
47
- end
48
- end
107
+ # Associations need to be checked whether they're marked optional
108
+ # while the underlying foreign key or type columns are marked NOT NULL.
109
+ problematic_associations = []
110
+ problematic_polymorphic_associations = []
111
+
112
+ model.reflect_on_all_associations.each do |reflection|
113
+ foreign_key_column = problematic_columns.find { |column| column.name == reflection.foreign_key }
114
+ if reflection.polymorphic?
115
+ # If the foreign key and type are not one of the columns that lack
116
+ # a validator then it means the association added a validator and
117
+ # is configured correctly.
118
+ foreign_type_column = problematic_columns.find { |column| column.name == reflection.foreign_type }
119
+ next if foreign_key_column.nil? && foreign_type_column.nil?
120
+
121
+ # Otherwise, don't report errors about missing validators on the
122
+ # foreign key or type, but instead ...
123
+ problematic_columns.delete(foreign_key_column)
124
+ problematic_columns.delete(foreign_type_column)
125
+
126
+ # ... report an error about an incorrectly configured polymorphic
127
+ # association.
128
+ problematic_polymorphic_associations << reflection.name
129
+ else
130
+ # If the foreign key is not one of the columns that lack a
131
+ # validator then it means the association added a validator and is
132
+ # configured correctly.
133
+ next if foreign_key_column.nil?
134
+
135
+ # Otherwise, don't report an error about a missing validator on
136
+ # the foreign key, but instead ...
137
+ problematic_columns.delete(foreign_key_column)
138
+
139
+ # ... report an error about an incorrectly configured association.
140
+ problematic_associations << reflection.name
141
+ end
142
+ end
49
143
 
50
- def inclusion_validator_present?(model, column)
51
- model.validators.any? do |validator|
52
- validator_items = inclusion_validator_items(validator)
53
- return true if validator_items.is_a?(Proc)
144
+ # Finally, regular and polymorphic associations that are explicitly
145
+ # ignored should be removed from the output. It's NOT enough to skip
146
+ # processing them in the loop above because their underlying foreign
147
+ # key and type columns must be removed from output, too.
148
+ problematic_associations.reject! do |name|
149
+ config(:ignore_attributes).include?("#{model.name}.#{name}")
150
+ end
151
+ problematic_polymorphic_associations.reject! do |name|
152
+ config(:ignore_attributes).include?("#{model.name}.#{name}")
153
+ end
54
154
 
55
- validator.is_a?(ActiveModel::Validations::InclusionValidator) &&
56
- validator.attributes.include?(column.name.to_sym) &&
57
- !validator_items.include?(nil)
155
+ # Job is done and all accumulated errors can be reported.
156
+ problematic_polymorphic_associations.each do |name|
157
+ problem!(type: :optional_polymorphic_association, column_or_association: name, model: model.name)
158
+ end
159
+ problematic_associations.each do |name|
160
+ problem!(type: :optional_association, column_or_association: name, model: model.name)
161
+ end
162
+ problematic_columns.each do |column|
163
+ problem!(type: :missing_validator, column_or_association: column.name, model: model.name)
164
+ end
58
165
  end
59
166
  end
60
167
 
61
- def exclusion_validator_present?(model, column)
62
- model.validators.any? do |validator|
63
- validator_items = inclusion_validator_items(validator)
64
- return true if validator_items.is_a?(Proc)
65
-
66
- validator.is_a?(ActiveModel::Validations::ExclusionValidator) &&
67
- validator.attributes.include?(column.name.to_sym) &&
68
- validator_items.include?(nil)
69
- end
168
+ # Normalizes the list of values passed to an inclusion or exclusion validator.
169
+ def inclusion_or_exclusion_validator_items(validator)
170
+ validator.options[:in] || validator.options[:within] || []
70
171
  end
71
172
 
72
- def presence_validator_present?(model, column)
73
- allowed_attributes = [column.name.to_sym]
74
-
75
- belongs_to = model.reflect_on_all_associations(:belongs_to).find do |reflection|
76
- reflection.foreign_key == column.name
77
- end
78
- allowed_attributes << belongs_to.name.to_sym if belongs_to
79
-
80
- model.validators.any? do |validator|
81
- validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
82
- (validator.attributes & allowed_attributes).present?
173
+ # Determines whether the given column is used as a counter cache column by
174
+ # a has_many association on the model.
175
+ def counter_cache_column?(model, column)
176
+ model.reflect_on_all_associations(:has_many).any? do |reflection|
177
+ reflection.has_cached_counter? && reflection.counter_cache_column == column.name
83
178
  end
84
179
  end
85
-
86
- def inclusion_validator_items(validator)
87
- validator.options[:in] || validator.options[:within] || []
88
- end
89
180
  end
90
181
  end
91
182
  end
@@ -13,9 +13,17 @@ module ActiveRecordDoctor
13
13
  },
14
14
  ignore_columns: {
15
15
  description: "specific validators, written as Model(column1, column2, ...), that should not be checked"
16
+ },
17
+ ignore_join_tables: {
18
+ description: "join tables that should not be checked for existence of unique indexes"
16
19
  }
17
20
  }
18
21
 
22
+ def initialize(**)
23
+ super
24
+ @reported_join_tables = []
25
+ end
26
+
19
27
  private
20
28
 
21
29
  # rubocop:disable Layout/LineLength
@@ -28,6 +36,8 @@ module ActiveRecordDoctor
28
36
  "without an expression index can lead to duplicates (a regular unique index is not enough)"
29
37
  when :has_ones
30
38
  "add a unique index on #{table}(#{columns.join(', ')}) - using `has_one` in #{model.name} without an index can lead to duplicates"
39
+ when :has_and_belongs_to_many
40
+ "add a unique index on #{table}(#{columns.join(', ')}) - using `has_and_belongs_to_many` in #{model.name} without an index can lead to duplicates"
31
41
  end
32
42
  end
33
43
  # rubocop:enable Layout/LineLength
@@ -35,6 +45,7 @@ module ActiveRecordDoctor
35
45
  def detect
36
46
  validations_without_indexes
37
47
  has_ones_without_indexes
48
+ has_and_belongs_to_many_without_indexes
38
49
  end
39
50
 
40
51
  def validations_without_indexes
@@ -54,17 +65,19 @@ module ActiveRecordDoctor
54
65
  # put true literally.
55
66
  case_sensitive = validator.options.fetch(:case_sensitive, true)
56
67
 
57
- # ActiveRecord < 5.0 does not support expression indexes,
58
- # so this will always be a false positive.
59
- next if !case_sensitive && Utils.expression_indexes_unsupported?
68
+ # Avoid a false positive if expression indexes are unsupported.
69
+ next if !case_sensitive && !connection.supports_expression_index?
60
70
 
61
71
  validator.attributes.each do |attribute|
62
72
  columns = resolve_attributes(model, scope + [attribute])
63
73
 
64
74
  next if ignore_columns.include?("#{model.name}(#{columns.join(',')})")
65
75
 
66
- columns[-1] = "lower(#{columns[-1]})" unless case_sensitive
67
-
76
+ # citext is case-insensitive by default, so it doesn't have to be
77
+ # lowered.
78
+ if !case_sensitive && model.columns_hash[columns[-1]].type != :citext
79
+ columns[-1] = "lower(#{columns[-1]})"
80
+ end
68
81
  next if unique_index?(model.table_name, columns)
69
82
 
70
83
  if case_sensitive
@@ -108,6 +121,21 @@ module ActiveRecordDoctor
108
121
  (validator.options.keys & [:if, :unless, :conditions]).present?
109
122
  end
110
123
 
124
+ def has_and_belongs_to_many_without_indexes # rubocop:disable Naming/PredicateName
125
+ each_model do |model|
126
+ each_association(model, type: :has_and_belongs_to_many, has_scope: false) do |association|
127
+ join_table = association.join_table
128
+ next if @reported_join_tables.include?(join_table) || config(:ignore_join_tables).include?(join_table)
129
+
130
+ columns = [association.foreign_key, association.association_foreign_key]
131
+ next if unique_index?(join_table, columns)
132
+
133
+ @reported_join_tables << join_table
134
+ problem!(model: model, table: join_table, columns: columns, problem: :has_and_belongs_to_many)
135
+ end
136
+ end
137
+ end
138
+
111
139
  def resolve_attributes(model, attributes)
112
140
  attributes.flat_map do |attribute|
113
141
  reflection = model.reflect_on_association(attribute)
@@ -20,6 +20,8 @@ module ActiveRecordDoctor
20
20
  end
21
21
 
22
22
  def detect
23
+ return if ActiveRecordDoctor::Utils.sqlite?
24
+
23
25
  each_table(except: config(:ignore_tables)) do |table|
24
26
  column = primary_key(table)
25
27
  next if column.nil?
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_doctor/detectors/base"
4
+
5
+ module ActiveRecordDoctor
6
+ module Detectors
7
+ class TableWithoutPrimaryKey < Base # :nodoc:
8
+ @description = "detect tables without primary keys"
9
+ @config = {
10
+ ignore_tables: {
11
+ description: "tables whose primary key existence should not be checked",
12
+ global: true
13
+ }
14
+ }
15
+
16
+ private
17
+
18
+ def message(table:)
19
+ "add a primary key to #{table}"
20
+ end
21
+
22
+ def detect
23
+ each_table(except: config(:ignore_tables)) do |table|
24
+ column = primary_key(table)
25
+ problem!(table: table) unless column
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record_doctor/detectors/base"
4
+
5
+ module ActiveRecordDoctor
6
+ module Detectors
7
+ class TableWithoutTimestamps < Base # :nodoc:
8
+ @description = "detect tables without created_at/updated_at columns"
9
+ @config = {
10
+ ignore_tables: {
11
+ description: "tables whose timestamp columns existence should not be checked",
12
+ global: true
13
+ }
14
+ }
15
+
16
+ private
17
+
18
+ TIMESTAMPS = {
19
+ "created_at" => "created_on",
20
+ "updated_at" => "updated_on"
21
+ }.freeze
22
+
23
+ def message(table:, column:)
24
+ "add a #{column} column to #{table}"
25
+ end
26
+
27
+ def detect
28
+ each_table(except: config(:ignore_tables)) do |table|
29
+ TIMESTAMPS.each do |column, alternative_column|
30
+ unless column(table, column) || column(table, alternative_column)
31
+ problem!(table: table, column: column)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -27,7 +27,7 @@ module ActiveRecordDoctor
27
27
  def detect
28
28
  each_table(except: config(:ignore_tables)) do |table|
29
29
  each_column(table, except: config(:ignore_columns)) do |column|
30
- next unless named_like_foreign_key?(column) || foreign_key?(table, column)
30
+ next unless looks_like_foreign_key?(column) || foreign_key?(table, column)
31
31
  next if indexed?(table, column)
32
32
  next if indexed_as_polymorphic?(table, column)
33
33
  next if connection.primary_key(table) == column.name
@@ -46,10 +46,6 @@ module ActiveRecordDoctor
46
46
  end
47
47
  end
48
48
 
49
- def named_like_foreign_key?(column)
50
- column.name.end_with?("_id")
51
- end
52
-
53
49
  def foreign_key?(table, column)
54
50
  connection.foreign_keys(table).any? do |foreign_key|
55
51
  foreign_key.column == column.name
@@ -27,7 +27,7 @@ module ActiveRecordDoctor # :nodoc:
27
27
  # We can't use #all? because of its short-circuit behavior - it stops
28
28
  # iteration and returns false upon the first falsey value. This
29
29
  # prevents other detectors from running if there's a failure.
30
- ActiveRecordDoctor.detectors.each do |name, _|
30
+ ActiveRecordDoctor.detectors.each_key do |name|
31
31
  success = false if !run_one(name)
32
32
  end
33
33
 
@@ -11,10 +11,8 @@ module ActiveRecordDoctor
11
11
  connection.adapter_name == "Mysql2"
12
12
  end
13
13
 
14
- def expression_indexes_unsupported?(connection = ActiveRecord::Base.connection)
15
- (ActiveRecord::VERSION::STRING < "5.0") ||
16
- # Active Record is unable to correctly parse expression indexes for MySQL.
17
- (mysql?(connection) && ActiveRecord::VERSION::STRING < "7.1")
14
+ def sqlite?(connection = ActiveRecord::Base.connection)
15
+ connection.adapter_name == "SQLite"
18
16
  end
19
17
  end
20
18
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordDoctor
4
- VERSION = "1.14.0"
4
+ VERSION = "2.0.0"
5
5
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_record_doctor/railtie" if defined?(Rails) && defined?(Rails::Railtie)
3
+ require "active_record_doctor/railtie" if defined?(Rails::Railtie)
4
4
  require "active_record_doctor/utils"
5
5
  require "active_record_doctor/logger"
6
6
  require "active_record_doctor/logger/dummy"
@@ -20,6 +20,8 @@ require "active_record_doctor/detectors/unindexed_foreign_keys"
20
20
  require "active_record_doctor/detectors/incorrect_dependent_option"
21
21
  require "active_record_doctor/detectors/short_primary_key_type"
22
22
  require "active_record_doctor/detectors/mismatched_foreign_key_type"
23
+ require "active_record_doctor/detectors/table_without_primary_key"
24
+ require "active_record_doctor/detectors/table_without_timestamps"
23
25
  require "active_record_doctor/errors"
24
26
  require "active_record_doctor/help"
25
27
  require "active_record_doctor/runner"
@@ -47,7 +47,7 @@ module ActiveRecordDoctor
47
47
  # rubocop rule below.
48
48
 
49
49
  <<MIGRATION
50
- class IndexForeignKeysIn#{table.camelize} < ActiveRecord::Migration#{migration_version}
50
+ class IndexForeignKeysIn#{table.camelize} < ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]
51
51
  def change
52
52
  #{add_indexes(table, indexes)}
53
53
  end
@@ -71,13 +71,5 @@ MIGRATION
71
71
  " add_index :#{table}, #{columns.inspect}"
72
72
  end
73
73
  end
74
-
75
- def migration_version
76
- if ActiveRecord::VERSION::STRING >= "5.1"
77
- "[#{ActiveRecord::Migration.current_version}]"
78
- else
79
- ""
80
- end
81
- end
82
74
  end
83
75
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_doctor
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.14.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg Navis
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-12-08 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: 4.2.0
18
+ version: 7.0.0
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: 4.2.0
25
+ version: 7.0.0
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: minitest-fork_executor
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -44,85 +43,98 @@ dependencies:
44
43
  requirements:
45
44
  - - "~>"
46
45
  - !ruby/object:Gem::Version
47
- version: 0.5.3
46
+ version: 0.5.6
48
47
  type: :development
49
48
  prerelease: false
50
49
  version_requirements: !ruby/object:Gem::Requirement
51
50
  requirements:
52
51
  - - "~>"
53
52
  - !ruby/object:Gem::Version
54
- version: 0.5.3
53
+ version: 0.5.6
55
54
  - !ruby/object:Gem::Dependency
56
55
  name: pg
57
56
  requirement: !ruby/object:Gem::Requirement
58
57
  requirements:
59
58
  - - "~>"
60
59
  - !ruby/object:Gem::Version
61
- version: 1.1.4
60
+ version: 1.5.9
62
61
  type: :development
63
62
  prerelease: false
64
63
  version_requirements: !ruby/object:Gem::Requirement
65
64
  requirements:
66
65
  - - "~>"
67
66
  - !ruby/object:Gem::Version
68
- version: 1.1.4
67
+ version: 1.5.9
69
68
  - !ruby/object:Gem::Dependency
70
69
  name: railties
71
70
  requirement: !ruby/object:Gem::Requirement
72
71
  requirements:
73
72
  - - ">="
74
73
  - !ruby/object:Gem::Version
75
- version: 4.2.0
74
+ version: 7.0.0
76
75
  type: :development
77
76
  prerelease: false
78
77
  version_requirements: !ruby/object:Gem::Requirement
79
78
  requirements:
80
79
  - - ">="
81
80
  - !ruby/object:Gem::Version
82
- version: 4.2.0
81
+ version: 7.0.0
83
82
  - !ruby/object:Gem::Dependency
84
83
  name: rake
85
84
  requirement: !ruby/object:Gem::Requirement
86
85
  requirements:
87
86
  - - "~>"
88
87
  - !ruby/object:Gem::Version
89
- version: 12.3.3
88
+ version: 13.2.1
90
89
  type: :development
91
90
  prerelease: false
92
91
  version_requirements: !ruby/object:Gem::Requirement
93
92
  requirements:
94
93
  - - "~>"
95
94
  - !ruby/object:Gem::Version
96
- version: 12.3.3
95
+ version: 13.2.1
97
96
  - !ruby/object:Gem::Dependency
98
- name: transient_record
97
+ name: rubocop
99
98
  requirement: !ruby/object:Gem::Requirement
100
99
  requirements:
101
- - - '='
100
+ - - "~>"
102
101
  - !ruby/object:Gem::Version
103
- version: 2.0.0.rc2
102
+ version: 1.68.0
104
103
  type: :development
105
104
  prerelease: false
106
105
  version_requirements: !ruby/object:Gem::Requirement
107
106
  requirements:
108
- - - '='
107
+ - - "~>"
109
108
  - !ruby/object:Gem::Version
110
- version: 2.0.0.rc2
109
+ version: 1.68.0
111
110
  - !ruby/object:Gem::Dependency
112
- name: rubocop
111
+ name: sqlite3
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 2.2.0
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 2.2.0
124
+ - !ruby/object:Gem::Dependency
125
+ name: transient_record
113
126
  requirement: !ruby/object:Gem::Requirement
114
127
  requirements:
115
128
  - - "~>"
116
129
  - !ruby/object:Gem::Version
117
- version: 1.57.1
130
+ version: 3.0.0
118
131
  type: :development
119
132
  prerelease: false
120
133
  version_requirements: !ruby/object:Gem::Requirement
121
134
  requirements:
122
135
  - - "~>"
123
136
  - !ruby/object:Gem::Version
124
- version: 1.57.1
125
- description:
137
+ version: 3.0.0
126
138
  email:
127
139
  - contact@gregnavis.com
128
140
  executables: []
@@ -132,6 +144,9 @@ files:
132
144
  - MIT-LICENSE.txt
133
145
  - README.md
134
146
  - lib/active_record_doctor.rb
147
+ - lib/active_record_doctor/adapters/database.rb
148
+ - lib/active_record_doctor/adapters/table.rb
149
+ - lib/active_record_doctor/adapters/validators.rb
135
150
  - lib/active_record_doctor/config.rb
136
151
  - lib/active_record_doctor/config/default.rb
137
152
  - lib/active_record_doctor/config/loader.rb
@@ -147,6 +162,8 @@ files:
147
162
  - lib/active_record_doctor/detectors/missing_presence_validation.rb
148
163
  - lib/active_record_doctor/detectors/missing_unique_indexes.rb
149
164
  - lib/active_record_doctor/detectors/short_primary_key_type.rb
165
+ - lib/active_record_doctor/detectors/table_without_primary_key.rb
166
+ - lib/active_record_doctor/detectors/table_without_timestamps.rb
150
167
  - lib/active_record_doctor/detectors/undefined_table_references.rb
151
168
  - lib/active_record_doctor/detectors/unindexed_deleted_at.rb
152
169
  - lib/active_record_doctor/detectors/unindexed_foreign_keys.rb
@@ -169,7 +186,6 @@ licenses:
169
186
  - MIT
170
187
  metadata:
171
188
  rubygems_mfa_required: 'true'
172
- post_install_message:
173
189
  rdoc_options: []
174
190
  require_paths:
175
191
  - lib
@@ -177,15 +193,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
177
193
  requirements:
178
194
  - - ">="
179
195
  - !ruby/object:Gem::Version
180
- version: 2.1.0
196
+ version: 2.7.0
181
197
  required_rubygems_version: !ruby/object:Gem::Requirement
182
198
  requirements:
183
199
  - - ">="
184
200
  - !ruby/object:Gem::Version
185
201
  version: '0'
186
202
  requirements: []
187
- rubygems_version: 3.3.26
188
- signing_key:
203
+ rubygems_version: 3.6.7
189
204
  specification_version: 4
190
205
  summary: Identify database issues before they hit production.
191
206
  test_files: []