active_record_doctor 1.14.0 → 1.15.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status](https://github.com/gregnavis/active_record_doctor/actions/workflows/
|
24
|
+
[![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)
|
25
|
+
[![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)
|
26
|
+
[![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
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.
|