active_record_doctor 1.14.0 → 1.15.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 +4 -4
- data/README.md +46 -6
- data/lib/active_record_doctor/config/default.rb +8 -2
- data/lib/active_record_doctor/detectors/base.rb +2 -2
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +11 -2
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +21 -2
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +9 -1
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +31 -2
- data/lib/active_record_doctor/detectors/table_without_primary_key.rb +30 -0
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +1 -0
- metadata +10 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de87f340b23468b20a336608acbb3adc75a1505845e9f03c03722199896a2582
|
4
|
+
data.tar.gz: b98205be522fc4a97d21d835fcf81d285209cdbb07b5ac8072d7c912cb544eb0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 492104d7e80292d96b91760e453c27aeff140b83eff8f78092a90a34c83dc1e7f1b0defe5d47fa15408c08eeb48c7c0ee61280108dd2f31f3eeba4712f3093d2
|
7
|
+
data.tar.gz: 2be286fd10dbbbf6b54eebe96ea44618e48906d6875fdfce0e2f1ae3ac5420b0ec95e7c1c25c378f26ba3ffb3c2a40267c4fa3daf14643410ea78e90484b5b91
|
data/README.md
CHANGED
@@ -15,12 +15,15 @@ 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)
|
18
19
|
|
19
20
|
It can also:
|
20
21
|
|
21
22
|
* index unindexed foreign keys - [`active_record_doctor:unindexed_foreign_keys`](#indexing-unindexed-foreign-keys)
|
22
23
|
|
23
|
-
[](https://github.com/gregnavis/active_record_doctor/actions/workflows/lint.yml)
|
25
|
+
[](https://github.com/gregnavis/active_record_doctor/actions/workflows/mysql.yml)
|
26
|
+
[](https://github.com/gregnavis/active_record_doctor/actions/workflows/postgresql.yml)
|
24
27
|
|
25
28
|
## Installation
|
26
29
|
|
@@ -28,14 +31,14 @@ In order to use the latest production release, please add the following to
|
|
28
31
|
your `Gemfile`:
|
29
32
|
|
30
33
|
```ruby
|
31
|
-
gem 'active_record_doctor', group: :development
|
34
|
+
gem 'active_record_doctor', group: [:development, :test]
|
32
35
|
```
|
33
36
|
|
34
37
|
and run `bundle install`. If you'd like to use the most recent development
|
35
38
|
version then use this instead:
|
36
39
|
|
37
40
|
```ruby
|
38
|
-
gem 'active_record_doctor', github: 'gregnavis/active_record_doctor'
|
41
|
+
gem 'active_record_doctor', github: 'gregnavis/active_record_doctor', group: [:development, :test]
|
39
42
|
```
|
40
43
|
|
41
44
|
That's it when it comes to Rails projects. If your project doesn't use Rails
|
@@ -101,6 +104,17 @@ bundle exec rake active_record_doctor:extraneous_indexes:help
|
|
101
104
|
This will show the detector help text in the terminal, along with supported
|
102
105
|
configuration options, their meaning, and whether they're global or local.
|
103
106
|
|
107
|
+
### Debug Logging
|
108
|
+
|
109
|
+
It may be that `active_record_doctor` fails with an exception and it is hard to tell
|
110
|
+
what went wrong. For easier debugging, use `ACTIVE_RECORD_DOCTOR_DEBUG` environment variable.
|
111
|
+
If `active_record_doctor` fails for some reason for your application, feel free
|
112
|
+
to open an issue or a PR with the fix.
|
113
|
+
|
114
|
+
```
|
115
|
+
ACTIVE_RECORD_DOCTOR_DEBUG=1 bundle exec rake active_record_doctor
|
116
|
+
```
|
117
|
+
|
104
118
|
### Configuration
|
105
119
|
|
106
120
|
`active_record_doctor` can be configured to better suit your project's needs.
|
@@ -358,9 +372,9 @@ Supported configuration options:
|
|
358
372
|
|
359
373
|
### Detecting Uniqueness Validations not Backed by an Index
|
360
374
|
|
361
|
-
Model-level uniqueness validations
|
362
|
-
by a database index in order to be robust.
|
363
|
-
duplicate values under a heavy load.
|
375
|
+
Model-level uniqueness validations, `has_one` and `has_and_belongs_to_many`
|
376
|
+
associations should be backed by a database index in order to be robust.
|
377
|
+
Otherwise you risk inserting duplicate values under a heavy load.
|
364
378
|
|
365
379
|
In order to detect such validations run:
|
366
380
|
|
@@ -382,6 +396,8 @@ Supported configuration options:
|
|
382
396
|
- `ignore_models` - models whose uniqueness validators should not be checked.
|
383
397
|
- `ignore_columns` - specific validators, written as Model(column1, ...), that
|
384
398
|
should not be checked.
|
399
|
+
- `ignore_join_tables` - join tables that should not be checked for existence
|
400
|
+
of unique indexes.
|
385
401
|
|
386
402
|
### Detecting Missing Non-`NULL` Constraints
|
387
403
|
|
@@ -442,6 +458,7 @@ Supported configuration options:
|
|
442
458
|
- `ignore_models` - models whose underlying tables' columns should not be checked.
|
443
459
|
- `ignore_attributes` - specific attributes, written as Model.attribute, that
|
444
460
|
should not be checked.
|
461
|
+
- `ignore_columns_with_default` - set to `true` to ignore columns with default values.
|
445
462
|
|
446
463
|
### Detecting Incorrect Presence Validations on Boolean Columns
|
447
464
|
|
@@ -602,6 +619,29 @@ Supported configuration options:
|
|
602
619
|
- `ignore_columns` - foreign keys, written as table.column, that should not be
|
603
620
|
checked.
|
604
621
|
|
622
|
+
### Detecting Tables Without Primary Keys
|
623
|
+
|
624
|
+
Tables should have primary keys. Otherwise, it becomes problematic to easily find a specific record,
|
625
|
+
logical replication in PostgreSQL will be troublesome, because all the rows need to be unique
|
626
|
+
in the table then etc.
|
627
|
+
|
628
|
+
Running the command below will list all tables without primary keys:
|
629
|
+
|
630
|
+
```
|
631
|
+
bundle exec rake active_record_doctor:table_without_primary_key
|
632
|
+
```
|
633
|
+
|
634
|
+
The output of the command looks like this:
|
635
|
+
|
636
|
+
```
|
637
|
+
add a primary key to companies
|
638
|
+
```
|
639
|
+
|
640
|
+
Supported configuration options:
|
641
|
+
|
642
|
+
- `enabled` - set to `false` to disable the detector altogether
|
643
|
+
- `ignore_tables` - tables whose primary key existense should not be checked
|
644
|
+
|
605
645
|
## Ruby and Rails Compatibility Policy
|
606
646
|
|
607
647
|
The goal of the policy is to ensure proper functioning in reasonable
|
@@ -47,17 +47,23 @@ 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
|
+
|
61
67
|
detector :undefined_table_references,
|
62
68
|
enabled: true,
|
63
69
|
ignore_models: []
|
@@ -157,7 +157,7 @@ module ActiveRecordDoctor
|
|
157
157
|
end
|
158
158
|
|
159
159
|
def models
|
160
|
-
ActiveRecord::Base.descendants
|
160
|
+
ActiveRecord::Base.descendants.sort_by(&:name)
|
161
161
|
end
|
162
162
|
|
163
163
|
def underscored_name
|
@@ -323,7 +323,7 @@ module ActiveRecordDoctor
|
|
323
323
|
end
|
324
324
|
|
325
325
|
def ignored?(name, patterns)
|
326
|
-
patterns.any? { |pattern| pattern === name } # rubocop:disable Style/CaseEquality
|
326
|
+
patterns.any? { |pattern| pattern === name || name == pattern.to_s } # rubocop:disable Style/CaseEquality
|
327
327
|
end
|
328
328
|
end
|
329
329
|
end
|
@@ -81,11 +81,12 @@ module ActiveRecordDoctor
|
|
81
81
|
|
82
82
|
case [index1.unique, index2.unique]
|
83
83
|
when [true, true]
|
84
|
-
(index2_columns
|
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
|
@@ -36,6 +36,7 @@ module ActiveRecordDoctor
|
|
36
36
|
next if ignored?("#{table}.#{column.name}", config(:ignore_columns))
|
37
37
|
next if !column.null
|
38
38
|
next if !concrete_models.all? { |model| non_null_needed?(model, column) }
|
39
|
+
next if sti_column?(models, column.name)
|
39
40
|
next if not_null_check_constraint_exists?(table, column)
|
40
41
|
|
41
42
|
problem!(column: column.name, table: table)
|
@@ -66,10 +67,28 @@ module ActiveRecordDoctor
|
|
66
67
|
model.validators.select do |validator|
|
67
68
|
validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
|
68
69
|
!validator.options[:allow_nil] &&
|
69
|
-
!validator
|
70
|
-
!validator.options[:unless]
|
70
|
+
(rails_belongs_to_presence_validator?(validator) || !conditional_validator?(validator))
|
71
71
|
end
|
72
72
|
end
|
73
|
+
|
74
|
+
def sti_column?(models, column_name)
|
75
|
+
models.any? { |model| model.inheritance_column == column_name }
|
76
|
+
end
|
77
|
+
|
78
|
+
def rails_belongs_to_presence_validator?(validator)
|
79
|
+
ActiveRecord.version >= Gem::Version.new("7.1") &&
|
80
|
+
!ActiveRecord.belongs_to_required_validates_foreign_key &&
|
81
|
+
validator.options[:message] == :required &&
|
82
|
+
proc_from_activerecord?(validator.options[:if])
|
83
|
+
end
|
84
|
+
|
85
|
+
def conditional_validator?(validator)
|
86
|
+
validator.options[:if] || validator.options[:unless]
|
87
|
+
end
|
88
|
+
|
89
|
+
def proc_from_activerecord?(object)
|
90
|
+
object.is_a?(Proc) && object.binding.receiver.name.start_with?("ActiveRecord::")
|
91
|
+
end
|
73
92
|
end
|
74
93
|
end
|
75
94
|
end
|
@@ -13,6 +13,9 @@ 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
|
|
@@ -35,7 +38,12 @@ module ActiveRecordDoctor
|
|
35
38
|
|
36
39
|
def validator_needed?(model, column)
|
37
40
|
![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))
|
41
|
+
(!column.null || not_null_check_constraint_exists?(model.table_name, column)) &&
|
42
|
+
!default_value_instead_of_validation?(column)
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_value_instead_of_validation?(column)
|
46
|
+
!column.default.nil? && config(:ignore_columns_with_default)
|
39
47
|
end
|
40
48
|
|
41
49
|
def validator_present?(model, column)
|
@@ -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
|
@@ -63,8 +74,11 @@ module ActiveRecordDoctor
|
|
63
74
|
|
64
75
|
next if ignore_columns.include?("#{model.name}(#{columns.join(',')})")
|
65
76
|
|
66
|
-
|
67
|
-
|
77
|
+
# citext is case-insensitive by default, so it doesn't have to be
|
78
|
+
# lowered.
|
79
|
+
if !case_sensitive && model.columns_hash[columns[-1]].type != :citext
|
80
|
+
columns[-1] = "lower(#{columns[-1]})"
|
81
|
+
end
|
68
82
|
next if unique_index?(model.table_name, columns)
|
69
83
|
|
70
84
|
if case_sensitive
|
@@ -108,6 +122,21 @@ module ActiveRecordDoctor
|
|
108
122
|
(validator.options.keys & [:if, :unless, :conditions]).present?
|
109
123
|
end
|
110
124
|
|
125
|
+
def has_and_belongs_to_many_without_indexes # rubocop:disable Naming/PredicateName
|
126
|
+
each_model do |model|
|
127
|
+
each_association(model, type: :has_and_belongs_to_many, has_scope: false) do |association|
|
128
|
+
join_table = association.join_table
|
129
|
+
next if @reported_join_tables.include?(join_table) || config(:ignore_join_tables).include?(join_table)
|
130
|
+
|
131
|
+
columns = [association.foreign_key, association.association_foreign_key]
|
132
|
+
next if unique_index?(join_table, columns)
|
133
|
+
|
134
|
+
@reported_join_tables << join_table
|
135
|
+
problem!(model: model, table: join_table, columns: columns, problem: :has_and_belongs_to_many)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
111
140
|
def resolve_attributes(model, attributes)
|
112
141
|
attributes.flat_map do |attribute|
|
113
142
|
reflection = model.reflect_on_association(attribute)
|
@@ -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 existense 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
|
data/lib/active_record_doctor.rb
CHANGED
@@ -20,6 +20,7 @@ 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"
|
23
24
|
require "active_record_doctor/errors"
|
24
25
|
require "active_record_doctor/help"
|
25
26
|
require "active_record_doctor/runner"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record_doctor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg Navis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-08-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 1.
|
61
|
+
version: 1.5.6
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 1.
|
68
|
+
version: 1.5.6
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: railties
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -98,16 +98,16 @@ dependencies:
|
|
98
98
|
name: transient_record
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- -
|
101
|
+
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 2.0.0
|
103
|
+
version: 2.0.0
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- -
|
108
|
+
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 2.0.0
|
110
|
+
version: 2.0.0
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: rubocop
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -147,6 +147,7 @@ files:
|
|
147
147
|
- lib/active_record_doctor/detectors/missing_presence_validation.rb
|
148
148
|
- lib/active_record_doctor/detectors/missing_unique_indexes.rb
|
149
149
|
- lib/active_record_doctor/detectors/short_primary_key_type.rb
|
150
|
+
- lib/active_record_doctor/detectors/table_without_primary_key.rb
|
150
151
|
- lib/active_record_doctor/detectors/undefined_table_references.rb
|
151
152
|
- lib/active_record_doctor/detectors/unindexed_deleted_at.rb
|
152
153
|
- lib/active_record_doctor/detectors/unindexed_foreign_keys.rb
|
@@ -184,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
184
185
|
- !ruby/object:Gem::Version
|
185
186
|
version: '0'
|
186
187
|
requirements: []
|
187
|
-
rubygems_version: 3.
|
188
|
+
rubygems_version: 3.5.11
|
188
189
|
signing_key:
|
189
190
|
specification_version: 4
|
190
191
|
summary: Identify database issues before they hit production.
|