active_record_doctor 1.9.0 → 1.10.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 +83 -19
- data/lib/active_record_doctor/config/default.rb +17 -0
- data/lib/active_record_doctor/detectors/base.rb +52 -22
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +25 -40
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +1 -2
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +40 -9
- data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +63 -0
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +2 -1
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +3 -4
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +65 -15
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +5 -1
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +1 -3
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +2 -3
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +1 -0
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +5 -5
- data/test/active_record_doctor/detectors/disable_test.rb +30 -0
- data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +34 -0
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +105 -7
- data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +105 -0
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +34 -0
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +37 -1
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +167 -3
- data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +27 -19
- data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +9 -3
- data/test/setup.rb +6 -2
- metadata +8 -3
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record_doctor/detectors/base"
|
4
|
+
|
5
|
+
module ActiveRecordDoctor
|
6
|
+
module Detectors
|
7
|
+
class IncorrectLengthValidation < Base # :nodoc:
|
8
|
+
@description = "detect mismatches between database length limits and model length validations"
|
9
|
+
@config = {
|
10
|
+
ignore_models: {
|
11
|
+
description: "models whose validators should not be checked",
|
12
|
+
global: true
|
13
|
+
},
|
14
|
+
ignore_attributes: {
|
15
|
+
description: "attributes, written as Model.attribute, whose validators should not be checked"
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def message(model:, attribute:, table:, database_maximum:, model_maximum:)
|
22
|
+
# rubocop:disable Layout/LineLength
|
23
|
+
if database_maximum && model_maximum
|
24
|
+
"the schema limits #{table}.#{attribute} to #{database_maximum} characters but the length validator on #{model}.#{attribute} enforces a maximum of #{model_maximum} characters - set both limits to the same value or remove both"
|
25
|
+
elsif database_maximum && model_maximum.nil?
|
26
|
+
"the schema limits #{table}.#{attribute} to #{database_maximum} characters but there's no length validator on #{model}.#{attribute} - remove the database limit or add the validator"
|
27
|
+
elsif database_maximum.nil? && model_maximum
|
28
|
+
"the length validator on #{model}.#{attribute} enforces a maximum of #{model_maximum} characters but there's no schema limit on #{table}.#{attribute} - remove the validator or the schema length limit"
|
29
|
+
end
|
30
|
+
# rubocop:enable Layout/LineLength
|
31
|
+
end
|
32
|
+
|
33
|
+
def detect
|
34
|
+
models(except: config(:ignore_models)).each do |model|
|
35
|
+
next unless model.table_exists?
|
36
|
+
|
37
|
+
connection.columns(model.table_name).each do |column|
|
38
|
+
next if config(:ignore_attributes).include?("#{model.name}.#{column.name}")
|
39
|
+
next if ![:string, :text].include?(column.type)
|
40
|
+
|
41
|
+
model_maximum = maximum_allowed_by_validations(model)
|
42
|
+
next if model_maximum == column.limit
|
43
|
+
|
44
|
+
problem!(
|
45
|
+
model: model.name,
|
46
|
+
attribute: column.name,
|
47
|
+
table: model.table_name,
|
48
|
+
database_maximum: column.limit,
|
49
|
+
model_maximum: model_maximum
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def maximum_allowed_by_validations(model)
|
56
|
+
length_validator = model.validators.find do |validator|
|
57
|
+
validator.kind == :length && validator.options.include?(:maximum)
|
58
|
+
end
|
59
|
+
length_validator ? length_validator.options[:maximum] : nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -24,7 +24,7 @@ module ActiveRecordDoctor
|
|
24
24
|
|
25
25
|
def detect
|
26
26
|
table_models = models.group_by(&:table_name)
|
27
|
-
table_models.delete_if { |
|
27
|
+
table_models.delete_if { |_table, models| !models.first.table_exists? }
|
28
28
|
|
29
29
|
table_models.each do |table, models|
|
30
30
|
next if config(:ignore_tables).include?(table)
|
@@ -37,6 +37,7 @@ module ActiveRecordDoctor
|
|
37
37
|
next if config(:ignore_columns).include?("#{table}.#{column.name}")
|
38
38
|
next if !column.null
|
39
39
|
next if !concrete_models.all? { |model| non_null_needed?(model, column) }
|
40
|
+
next if not_null_check_constraint_exists?(table, column)
|
40
41
|
|
41
42
|
problem!(column: column.name, table: table)
|
42
43
|
end
|
@@ -24,8 +24,7 @@ module ActiveRecordDoctor
|
|
24
24
|
|
25
25
|
def detect
|
26
26
|
models(except: config(:ignore_models)).each do |model|
|
27
|
-
next
|
28
|
-
next unless table_exists?(model.table_name)
|
27
|
+
next unless model.table_exists?
|
29
28
|
|
30
29
|
connection.columns(model.table_name).each do |column|
|
31
30
|
next unless validator_needed?(model, column)
|
@@ -38,8 +37,8 @@ module ActiveRecordDoctor
|
|
38
37
|
end
|
39
38
|
|
40
39
|
def validator_needed?(model, column)
|
41
|
-
![model.primary_key, "created_at", "updated_at"].include?(column.name) &&
|
42
|
-
!column.null
|
40
|
+
![model.primary_key, "created_at", "updated_at", "created_on", "updated_on"].include?(column.name) &&
|
41
|
+
(!column.null || not_null_check_constraint_exists?(model.table_name, column))
|
43
42
|
end
|
44
43
|
|
45
44
|
def validator_present?(model, column)
|
@@ -18,31 +18,60 @@ module ActiveRecordDoctor
|
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
# rubocop:disable Layout/LineLength
|
22
|
+
def message(model:, table:, columns:, problem:)
|
23
|
+
case problem
|
24
|
+
when :validations
|
25
|
+
"add a unique index on #{table}(#{columns.join(', ')}) - validating uniqueness in the model without an index can lead to duplicates"
|
26
|
+
when :has_ones
|
27
|
+
"add a unique index on #{table}(#{columns.first}) - using `has_one` in the #{model.name} model without an index can lead to duplicates"
|
28
|
+
end
|
25
29
|
end
|
30
|
+
# rubocop:enable Layout/LineLength
|
26
31
|
|
27
32
|
def detect
|
28
|
-
|
29
|
-
|
30
|
-
|
33
|
+
validations_without_indexes
|
34
|
+
has_ones_without_indexes
|
35
|
+
end
|
31
36
|
|
37
|
+
def validations_without_indexes
|
32
38
|
models(except: config(:ignore_models)).each do |model|
|
33
|
-
next
|
39
|
+
next unless model.table_exists?
|
34
40
|
|
35
41
|
model.validators.each do |validator|
|
36
42
|
scope = Array(validator.options.fetch(:scope, []))
|
37
43
|
|
38
44
|
next unless validator.is_a?(ActiveRecord::Validations::UniquenessValidator)
|
39
45
|
next unless supported_validator?(validator)
|
40
|
-
next if unique_index?(model.table_name, validator.attributes, scope)
|
41
46
|
|
42
|
-
|
43
|
-
|
47
|
+
validator.attributes.each do |attribute|
|
48
|
+
columns = resolve_attributes(model, scope + [attribute])
|
49
|
+
|
50
|
+
next if unique_index?(model.table_name, columns)
|
51
|
+
next if ignore_columns.include?("#{model.name}(#{columns.join(',')})")
|
52
|
+
|
53
|
+
problem!(model: model, table: model.table_name, columns: columns, problem: :validations)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def has_ones_without_indexes # rubocop:disable Naming/PredicateName
|
60
|
+
models.each do |model|
|
61
|
+
has_ones = model.reflect_on_all_associations(:has_one)
|
62
|
+
has_ones.each do |has_one|
|
63
|
+
next if has_one.is_a?(ActiveRecord::Reflection::ThroughReflection) || has_one.scope
|
64
|
+
|
65
|
+
association_model = has_one.klass
|
66
|
+
next if config(:ignore_models).include?(association_model.name)
|
44
67
|
|
45
|
-
|
68
|
+
foreign_key = has_one.foreign_key
|
69
|
+
next if ignore_columns.include?(foreign_key.to_s)
|
70
|
+
|
71
|
+
table_name = association_model.table_name
|
72
|
+
next if unique_index?(table_name, [foreign_key])
|
73
|
+
|
74
|
+
problem!(model: model, table: table_name, columns: [foreign_key], problem: :has_ones)
|
46
75
|
end
|
47
76
|
end
|
48
77
|
end
|
@@ -59,11 +88,32 @@ module ActiveRecordDoctor
|
|
59
88
|
validator.options.fetch(:case_sensitive, true)
|
60
89
|
end
|
61
90
|
|
62
|
-
def
|
63
|
-
|
91
|
+
def resolve_attributes(model, attributes)
|
92
|
+
attributes.flat_map do |attribute|
|
93
|
+
reflection = model.reflect_on_association(attribute)
|
94
|
+
|
95
|
+
if reflection.nil?
|
96
|
+
attribute
|
97
|
+
elsif reflection.polymorphic?
|
98
|
+
[reflection.foreign_type, reflection.foreign_key]
|
99
|
+
else
|
100
|
+
reflection.foreign_key
|
101
|
+
end
|
102
|
+
end.map(&:to_s)
|
103
|
+
end
|
64
104
|
|
105
|
+
def unique_index?(table_name, columns, scope = nil)
|
106
|
+
columns = (Array(scope) + columns).map(&:to_s)
|
65
107
|
indexes(table_name).any? do |index|
|
66
|
-
index.
|
108
|
+
index.unique &&
|
109
|
+
index.where.nil? &&
|
110
|
+
(Array(index.columns) - columns).empty?
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def ignore_columns
|
115
|
+
@ignore_columns ||= config(:ignore_columns).map do |column|
|
116
|
+
column.gsub(" ", "")
|
67
117
|
end
|
68
118
|
end
|
69
119
|
end
|
@@ -23,7 +23,7 @@ module ActiveRecordDoctor
|
|
23
23
|
tables(except: config(:ignore_tables)).each do |table|
|
24
24
|
column = primary_key(table)
|
25
25
|
next if column.nil?
|
26
|
-
next if bigint?(column)
|
26
|
+
next if bigint?(column) || uuid?(column)
|
27
27
|
|
28
28
|
problem!(table: table, column: column.name)
|
29
29
|
end
|
@@ -36,6 +36,10 @@ module ActiveRecordDoctor
|
|
36
36
|
/\Abigint\b/.match?(column.sql_type)
|
37
37
|
end
|
38
38
|
end
|
39
|
+
|
40
|
+
def uuid?(column)
|
41
|
+
column.sql_type == "uuid"
|
42
|
+
end
|
39
43
|
end
|
40
44
|
end
|
41
45
|
end
|
@@ -21,9 +21,7 @@ module ActiveRecordDoctor
|
|
21
21
|
|
22
22
|
def detect
|
23
23
|
models(except: config(:ignore_models)).each do |model|
|
24
|
-
next if model.
|
25
|
-
next if tables.include?(model.table_name)
|
26
|
-
next if tables_and_views.include?(model.table_name)
|
24
|
+
next if model.table_exists? || views.include?(model.table_name)
|
27
25
|
|
28
26
|
problem!(model: model.name, table: model.table_name)
|
29
27
|
end
|
@@ -26,7 +26,7 @@ module ActiveRecordDoctor
|
|
26
26
|
|
27
27
|
def message(index:, column_name:)
|
28
28
|
# rubocop:disable Layout/LineLength
|
29
|
-
"consider adding `WHERE #{column_name} IS NULL` to #{index} - a partial index can speed lookups of soft-deletable models"
|
29
|
+
"consider adding `WHERE #{column_name} IS NULL` or `WHERE #{column_name} IS NOT NULL` to #{index} - a partial index can speed lookups of soft-deletable models"
|
30
30
|
# rubocop:enable Layout/LineLength
|
31
31
|
end
|
32
32
|
|
@@ -42,8 +42,7 @@ module ActiveRecordDoctor
|
|
42
42
|
|
43
43
|
timestamp_columns.each do |timestamp_column|
|
44
44
|
indexes(table, except: config(:ignore_indexes)).each do |index|
|
45
|
-
|
46
|
-
next if index.where =~ /\b#{timestamp_column.name}\s+IS\s+NULL\b/i
|
45
|
+
next if index.where =~ /\b#{timestamp_column.name}\s+IS\s+(NOT\s+)?NULL\b/i
|
47
46
|
|
48
47
|
problem!(index: index.name, column_name: timestamp_column.name)
|
49
48
|
end
|
data/lib/active_record_doctor.rb
CHANGED
@@ -7,6 +7,7 @@ require "active_record_doctor/detectors/missing_presence_validation"
|
|
7
7
|
require "active_record_doctor/detectors/missing_foreign_keys"
|
8
8
|
require "active_record_doctor/detectors/missing_unique_indexes"
|
9
9
|
require "active_record_doctor/detectors/incorrect_boolean_presence_validation"
|
10
|
+
require "active_record_doctor/detectors/incorrect_length_validation"
|
10
11
|
require "active_record_doctor/detectors/extraneous_indexes"
|
11
12
|
require "active_record_doctor/detectors/unindexed_deleted_at"
|
12
13
|
require "active_record_doctor/detectors/undefined_table_references"
|
@@ -62,14 +62,14 @@ MIGRATION
|
|
62
62
|
end
|
63
63
|
|
64
64
|
def add_index(table, column)
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
65
|
+
connection = ActiveRecord::Base.connection
|
66
|
+
|
67
|
+
index_name = connection.index_name(table, column)
|
68
|
+
if index_name.size > connection.index_name_length
|
69
|
+
" add_index :#{table}, :#{column}, name: '#{index_name.first(connection.index_name_length)}'"
|
69
70
|
else
|
70
71
|
" add_index :#{table}, :#{column}"
|
71
72
|
end
|
72
|
-
# rubocop:enable Layout/LineLength
|
73
73
|
end
|
74
74
|
|
75
75
|
def migration_version
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveRecordDoctor::Detectors::DisableTest < Minitest::Test
|
4
|
+
# Disabling detectors is implemented in the base class. It's enought to test
|
5
|
+
# it on a single detector to be reasonably certain it works on all of them.
|
6
|
+
def test_disabling
|
7
|
+
create_table(:users) do |t|
|
8
|
+
t.string :name, null: true
|
9
|
+
end.create_model do
|
10
|
+
validates :name, presence: true
|
11
|
+
end
|
12
|
+
|
13
|
+
config_file(<<-CONFIG)
|
14
|
+
ActiveRecordDoctor.configure do |config|
|
15
|
+
config.detector :missing_non_null_constraint,
|
16
|
+
enabled: false
|
17
|
+
end
|
18
|
+
CONFIG
|
19
|
+
|
20
|
+
refute_problems
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# We need to override that method in order to skip the mechanism that
|
26
|
+
# infers detector name from the test class name.
|
27
|
+
def detector_name
|
28
|
+
:missing_non_null_constraint
|
29
|
+
end
|
30
|
+
end
|
@@ -11,6 +11,27 @@ remove index_users_on_id - coincides with the primary key on the table
|
|
11
11
|
OUTPUT
|
12
12
|
end
|
13
13
|
|
14
|
+
def test_partial_index_on_primary_key
|
15
|
+
skip("MySQL doesn't support partial indexes") if mysql?
|
16
|
+
|
17
|
+
create_table(:users) do |t|
|
18
|
+
t.boolean :admin
|
19
|
+
t.index :id, where: "admin"
|
20
|
+
end
|
21
|
+
|
22
|
+
refute_problems
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_index_on_non_standard_primary_key
|
26
|
+
create_table(:profiles, primary_key: :user_id) do |t|
|
27
|
+
t.index :user_id
|
28
|
+
end
|
29
|
+
|
30
|
+
assert_problems(<<OUTPUT)
|
31
|
+
remove index_profiles_on_user_id - coincides with the primary key on the table
|
32
|
+
OUTPUT
|
33
|
+
end
|
34
|
+
|
14
35
|
def test_non_unique_version_of_index_is_duplicate
|
15
36
|
create_table(:users) do |t|
|
16
37
|
t.string :email
|
@@ -61,6 +82,19 @@ remove index_users_on_last_name_and_first_name - can be replaced by index_users_
|
|
61
82
|
OUTPUT
|
62
83
|
end
|
63
84
|
|
85
|
+
def test_unique_index_with_fewer_columns
|
86
|
+
create_table(:users) do |t|
|
87
|
+
t.string :first_name
|
88
|
+
t.string :last_name
|
89
|
+
t.index :first_name, unique: true
|
90
|
+
t.index [:last_name, :first_name], unique: true
|
91
|
+
end
|
92
|
+
|
93
|
+
assert_problems(<<OUTPUT)
|
94
|
+
remove index_users_on_last_name_and_first_name - can be replaced by index_users_on_first_name
|
95
|
+
OUTPUT
|
96
|
+
end
|
97
|
+
|
64
98
|
def test_not_covered_by_different_index_type
|
65
99
|
create_table(:users) do |t|
|
66
100
|
t.string :first_name
|
@@ -14,7 +14,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
|
|
14
14
|
end
|
15
15
|
|
16
16
|
assert_problems(<<~OUTPUT)
|
17
|
-
use `dependent: :delete_all` or similar on ModelFactory::Models::Company.users - associated
|
17
|
+
use `dependent: :delete_all` or similar on ModelFactory::Models::Company.users - associated model ModelFactory::Models::User has no validations and can be deleted in bulk
|
18
18
|
OUTPUT
|
19
19
|
end
|
20
20
|
|
@@ -56,7 +56,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
|
|
56
56
|
end
|
57
57
|
|
58
58
|
assert_problems(<<~OUTPUT)
|
59
|
-
use `dependent: :destroy` or similar on ModelFactory::Models::Company.users - the associated model has callbacks that are currently skipped
|
59
|
+
use `dependent: :destroy` or similar on ModelFactory::Models::Company.users - the associated model ModelFactory::Models::User has callbacks that are currently skipped
|
60
60
|
OUTPUT
|
61
61
|
end
|
62
62
|
|
@@ -93,7 +93,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
|
|
93
93
|
end
|
94
94
|
|
95
95
|
assert_problems(<<~OUTPUT)
|
96
|
-
use `dependent: :delete` or similar on ModelFactory::Models::Company.owner - the associated model has no callbacks and can be deleted without loading
|
96
|
+
use `dependent: :delete` or similar on ModelFactory::Models::Company.owner - the associated model ModelFactory::Models::User has no callbacks and can be deleted without loading
|
97
97
|
OUTPUT
|
98
98
|
end
|
99
99
|
|
@@ -110,7 +110,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
|
|
110
110
|
end
|
111
111
|
|
112
112
|
assert_problems(<<~OUTPUT)
|
113
|
-
use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model has no callbacks and can be deleted without loading
|
113
|
+
use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has no callbacks and can be deleted without loading
|
114
114
|
OUTPUT
|
115
115
|
end
|
116
116
|
|
@@ -134,7 +134,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
|
|
134
134
|
end
|
135
135
|
|
136
136
|
assert_problems(<<~OUTPUT)
|
137
|
-
use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model has no callbacks and can be deleted without loading
|
137
|
+
use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has no callbacks and can be deleted without loading
|
138
138
|
OUTPUT
|
139
139
|
end
|
140
140
|
|
@@ -158,7 +158,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
|
|
158
158
|
end
|
159
159
|
|
160
160
|
assert_problems(<<~OUTPUT)
|
161
|
-
use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model has no callbacks and can be deleted without loading
|
161
|
+
use `dependent: :delete` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has no callbacks and can be deleted without loading
|
162
162
|
OUTPUT
|
163
163
|
end
|
164
164
|
|
@@ -187,7 +187,7 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
|
|
187
187
|
end
|
188
188
|
|
189
189
|
assert_problems(<<~OUTPUT)
|
190
|
-
use `dependent: :destroy` or similar on ModelFactory::Models::User.company - the associated model has callbacks that are currently skipped
|
190
|
+
use `dependent: :destroy` or similar on ModelFactory::Models::User.company - the associated model ModelFactory::Models::Company has callbacks that are currently skipped
|
191
191
|
OUTPUT
|
192
192
|
end
|
193
193
|
|
@@ -228,6 +228,104 @@ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Te
|
|
228
228
|
refute_problems
|
229
229
|
end
|
230
230
|
|
231
|
+
def test_polymorphic_destroy_reported_when_all_associations_deletable
|
232
|
+
create_table(:images) do |t|
|
233
|
+
t.bigint :imageable_id, null: false
|
234
|
+
t.string :imageable_type, null: true
|
235
|
+
end.create_model do
|
236
|
+
belongs_to :imageable, polymorphic: true, dependent: :destroy
|
237
|
+
end
|
238
|
+
|
239
|
+
create_table(:users) do
|
240
|
+
end.create_model do
|
241
|
+
has_one :image, as: :imageable
|
242
|
+
end
|
243
|
+
|
244
|
+
create_table(:companies) do
|
245
|
+
end.create_model do
|
246
|
+
has_one :image, as: :imageable
|
247
|
+
end
|
248
|
+
|
249
|
+
assert_problems(<<~OUTPUT)
|
250
|
+
use `dependent: :delete` or similar on ModelFactory::Models::Image.imageable - the associated models ModelFactory::Models::Company, ModelFactory::Models::User have no callbacks and can be deleted without loading
|
251
|
+
OUTPUT
|
252
|
+
end
|
253
|
+
|
254
|
+
def test_polymorphic_destroy_not_reported_when_some_associations_not_deletable
|
255
|
+
create_table(:images) do |t|
|
256
|
+
t.bigint :imageable_id, null: false
|
257
|
+
t.string :imageable_type, null: true
|
258
|
+
end.create_model do
|
259
|
+
belongs_to :imageable, polymorphic: true, dependent: :destroy
|
260
|
+
end
|
261
|
+
|
262
|
+
create_table(:users) do
|
263
|
+
end.create_model do
|
264
|
+
has_one :image, as: :imageable
|
265
|
+
|
266
|
+
before_destroy :log
|
267
|
+
|
268
|
+
def log
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
create_table(:companies) do
|
273
|
+
end.create_model do
|
274
|
+
has_one :image, as: :imageable
|
275
|
+
end
|
276
|
+
|
277
|
+
refute_problems
|
278
|
+
end
|
279
|
+
|
280
|
+
def test_polymorphic_delete_reported_when_some_associations_not_deletable
|
281
|
+
create_table(:images) do |t|
|
282
|
+
t.bigint :imageable_id, null: false
|
283
|
+
t.string :imageable_type, null: true
|
284
|
+
end.create_model do
|
285
|
+
belongs_to :imageable, polymorphic: true, dependent: :delete
|
286
|
+
end
|
287
|
+
|
288
|
+
create_table(:users) do
|
289
|
+
end.create_model do
|
290
|
+
has_one :image, as: :imageable
|
291
|
+
|
292
|
+
before_destroy :log
|
293
|
+
|
294
|
+
def log
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
create_table(:companies) do
|
299
|
+
end.create_model do
|
300
|
+
has_one :image, as: :imageable
|
301
|
+
end
|
302
|
+
|
303
|
+
assert_problems(<<~OUTPUT)
|
304
|
+
use `dependent: :destroy` or similar on ModelFactory::Models::Image.imageable - the associated model ModelFactory::Models::User has callbacks that are currently skipped
|
305
|
+
OUTPUT
|
306
|
+
end
|
307
|
+
|
308
|
+
def test_polymorphic_delete_not_reported_when_all_associations_deletable
|
309
|
+
create_table(:images) do |t|
|
310
|
+
t.bigint :imageable_id, null: false
|
311
|
+
t.string :imageable_type, null: true
|
312
|
+
end.create_model do
|
313
|
+
belongs_to :imageable, polymorphic: true, dependent: :delete
|
314
|
+
end
|
315
|
+
|
316
|
+
create_table(:users) do
|
317
|
+
end.create_model do
|
318
|
+
has_one :image, as: :imageable
|
319
|
+
end
|
320
|
+
|
321
|
+
create_table(:companies) do
|
322
|
+
end.create_model do
|
323
|
+
has_one :image, as: :imageable
|
324
|
+
end
|
325
|
+
|
326
|
+
refute_problems
|
327
|
+
end
|
328
|
+
|
231
329
|
def test_config_ignore_models
|
232
330
|
create_table(:companies) do
|
233
331
|
end.create_model do
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveRecordDoctor::Detectors::IncorrectLengthValidationTest < Minitest::Test
|
4
|
+
def test_validation_and_limit_equal_is_ok
|
5
|
+
create_table(:users) do |t|
|
6
|
+
t.string :email, limit: 64
|
7
|
+
end.create_model do
|
8
|
+
validates :email, length: { maximum: 64 }
|
9
|
+
end
|
10
|
+
|
11
|
+
refute_problems
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_validation_and_limit_different_is_error
|
15
|
+
create_table(:users) do |t|
|
16
|
+
t.string :email, limit: 64
|
17
|
+
end.create_model do
|
18
|
+
validates :email, length: { maximum: 32 }
|
19
|
+
end
|
20
|
+
|
21
|
+
assert_problems(<<~OUTPUT)
|
22
|
+
the schema limits users.email to 64 characters but the length validator on ModelFactory::Models::User.email enforces a maximum of 32 characters - set both limits to the same value or remove both
|
23
|
+
OUTPUT
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_validation_and_no_limit_is_error
|
27
|
+
skip("MySQL always sets a limit on text columns") if mysql?
|
28
|
+
|
29
|
+
create_table(:users) do |t|
|
30
|
+
t.string :email
|
31
|
+
end.create_model do
|
32
|
+
validates :email, length: { maximum: 32 }
|
33
|
+
end
|
34
|
+
|
35
|
+
assert_problems(<<~OUTPUT)
|
36
|
+
the length validator on ModelFactory::Models::User.email enforces a maximum of 32 characters but there's no schema limit on users.email - remove the validator or the schema length limit
|
37
|
+
OUTPUT
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_no_validation_and_limit_is_error
|
41
|
+
create_table(:users) do |t|
|
42
|
+
t.string :email, limit: 64
|
43
|
+
end.create_model do
|
44
|
+
end
|
45
|
+
|
46
|
+
assert_problems(<<~OUTPUT)
|
47
|
+
the schema limits users.email to 64 characters but there's no length validator on ModelFactory::Models::User.email - remove the database limit or add the validator
|
48
|
+
OUTPUT
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_no_validation_and_no_limit_is_ok
|
52
|
+
skip("MySQL always sets a limit on text columns") if mysql?
|
53
|
+
|
54
|
+
create_table(:users) do |t|
|
55
|
+
t.string :email
|
56
|
+
end.create_model do
|
57
|
+
end
|
58
|
+
|
59
|
+
refute_problems
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_config_ignore_models
|
63
|
+
create_table(:users) do |t|
|
64
|
+
t.string :email, limit: 64
|
65
|
+
end.create_model
|
66
|
+
|
67
|
+
config_file(<<-CONFIG)
|
68
|
+
ActiveRecordDoctor.configure do |config|
|
69
|
+
config.detector :incorrect_length_validation,
|
70
|
+
ignore_models: ["ModelFactory::Models::User"]
|
71
|
+
end
|
72
|
+
CONFIG
|
73
|
+
|
74
|
+
refute_problems
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_global_ignore_models
|
78
|
+
create_table(:users) do |t|
|
79
|
+
t.string :email, limit: 64
|
80
|
+
end.create_model
|
81
|
+
|
82
|
+
config_file(<<-CONFIG)
|
83
|
+
ActiveRecordDoctor.configure do |config|
|
84
|
+
config.global :ignore_models, ["ModelFactory::Models::User"]
|
85
|
+
end
|
86
|
+
CONFIG
|
87
|
+
|
88
|
+
refute_problems
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_config_ignore_attributes
|
92
|
+
create_table(:users) do |t|
|
93
|
+
t.string :email, limit: 64
|
94
|
+
end.create_model
|
95
|
+
|
96
|
+
config_file(<<-CONFIG)
|
97
|
+
ActiveRecordDoctor.configure do |config|
|
98
|
+
config.detector :incorrect_length_validation,
|
99
|
+
ignore_attributes: ["ModelFactory::Models::User.email"]
|
100
|
+
end
|
101
|
+
CONFIG
|
102
|
+
|
103
|
+
refute_problems
|
104
|
+
end
|
105
|
+
end
|