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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -19
  3. data/lib/active_record_doctor/config/default.rb +17 -0
  4. data/lib/active_record_doctor/detectors/base.rb +52 -22
  5. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +25 -40
  6. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +1 -2
  7. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +40 -9
  8. data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +63 -0
  9. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +2 -1
  10. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +3 -4
  11. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +65 -15
  12. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +5 -1
  13. data/lib/active_record_doctor/detectors/undefined_table_references.rb +1 -3
  14. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +2 -3
  15. data/lib/active_record_doctor/version.rb +1 -1
  16. data/lib/active_record_doctor.rb +1 -0
  17. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +5 -5
  18. data/test/active_record_doctor/detectors/disable_test.rb +30 -0
  19. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +34 -0
  20. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +105 -7
  21. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +105 -0
  22. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +34 -0
  23. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +37 -1
  24. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +167 -3
  25. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +27 -19
  26. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +9 -3
  27. data/test/setup.rb +6 -2
  28. 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 { |table| table.nil? || !table_exists?(table) }
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 if model.table_name.nil?
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
- def message(table:, columns:)
22
- # rubocop:disable Layout/LineLength
23
- "add a unique index on #{table}(#{columns.join(', ')}) - validating uniqueness in the model without an index can lead to duplicates"
24
- # rubocop:enable Layout/LineLength
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
- ignore_columns = config(:ignore_columns).map do |column|
29
- column.gsub(" ", "")
30
- end
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 if model.table_name.nil?
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
- columns = (scope + validator.attributes).map(&:to_s)
43
- next if ignore_columns.include?("#{model.name}(#{columns.join(',')})")
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
- problem!(table: model.table_name, columns: columns)
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 unique_index?(table_name, columns, scope)
63
- columns = (Array(scope) + columns).map(&:to_s)
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.columns.to_set == columns.to_set && index.unique
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.table_name.nil?
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
- # TODO: whole word
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordDoctor
4
- VERSION = "1.9.0"
4
+ VERSION = "1.10.0"
5
5
  end
@@ -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
- index_name = Class.new.extend(ActiveRecord::ConnectionAdapters::SchemaStatements).index_name table, column
66
- # rubocop:disable Layout/LineLength
67
- if index_name.size > ActiveRecord::Base.connection.allowed_index_name_length
68
- " add_index :#{table}, :#{column}, name: '#{index_name.first ActiveRecord::Base.connection.allowed_index_name_length}'"
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 models have no validations and can be deleted in bulk
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