active_record_doctor 1.9.0 → 1.10.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 +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
|