active_record_doctor 1.5.0 → 1.6.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -6
  3. data/lib/active_record_doctor/printers/io_printer.rb +35 -6
  4. data/lib/active_record_doctor/railtie.rb +1 -1
  5. data/lib/active_record_doctor/tasks.rb +3 -0
  6. data/lib/active_record_doctor/tasks/base.rb +64 -0
  7. data/lib/active_record_doctor/tasks/extraneous_indexes.rb +4 -27
  8. data/lib/active_record_doctor/tasks/missing_foreign_keys.rb +6 -29
  9. data/lib/active_record_doctor/tasks/missing_non_null_constraint.rb +42 -0
  10. data/lib/active_record_doctor/tasks/missing_presence_validation.rb +39 -0
  11. data/lib/active_record_doctor/tasks/missing_unique_indexes.rb +51 -0
  12. data/lib/active_record_doctor/tasks/undefined_table_references.rb +6 -22
  13. data/lib/active_record_doctor/tasks/unindexed_deleted_at.rb +4 -25
  14. data/lib/active_record_doctor/tasks/unindexed_foreign_keys.rb +6 -29
  15. data/lib/active_record_doctor/version.rb +1 -1
  16. data/lib/tasks/active_record_doctor.rake +31 -0
  17. data/test/active_record_doctor/printers/io_printer_test.rb +4 -4
  18. data/test/active_record_doctor/tasks/extraneous_indexes_test.rb +70 -16
  19. data/test/active_record_doctor/tasks/missing_foreign_keys_test.rb +16 -8
  20. data/test/active_record_doctor/tasks/missing_non_null_constraint_test.rb +89 -0
  21. data/test/active_record_doctor/tasks/missing_presence_validation_test.rb +49 -0
  22. data/test/active_record_doctor/tasks/missing_unique_indexes_test.rb +95 -0
  23. data/test/active_record_doctor/tasks/unindexed_deleted_at_test.rb +23 -8
  24. data/test/active_record_doctor/tasks/unindexed_foreign_keys_test.rb +16 -8
  25. data/test/dummy/app/models/application_record.rb +1 -1
  26. data/test/dummy/db/schema.rb +1 -50
  27. data/test/dummy/log/development.log +38 -498
  28. data/test/dummy/log/test.log +54108 -1571
  29. data/test/support/assertions.rb +11 -0
  30. data/test/support/forking_test.rb +28 -0
  31. data/test/support/temping.rb +25 -0
  32. data/test/test_helper.rb +0 -7
  33. metadata +34 -27
  34. data/lib/active_record_doctor/compatibility.rb +0 -11
  35. data/lib/tasks/active_record_doctor_tasks.rake +0 -27
  36. data/test/active_record_doctor/tasks/undefined_table_references_test.rb +0 -19
  37. data/test/dummy/app/models/comment.rb +0 -3
  38. data/test/dummy/app/models/contract.rb +0 -3
  39. data/test/dummy/app/models/employer.rb +0 -2
  40. data/test/dummy/app/models/profile.rb +0 -2
  41. data/test/dummy/app/models/user.rb +0 -3
  42. data/test/dummy/db/migrate/20160213101213_create_employers.rb +0 -15
  43. data/test/dummy/db/migrate/20160213101221_create_users.rb +0 -23
  44. data/test/dummy/db/migrate/20160213101232_create_profiles.rb +0 -15
  45. data/test/dummy/db/migrate/20160604081452_create_comments.rb +0 -11
  46. data/test/support/spy_printer.rb +0 -52
@@ -0,0 +1,39 @@
1
+ require "active_record_doctor/tasks/base"
2
+
3
+ module ActiveRecordDoctor
4
+ module Tasks
5
+ class MissingPresenceValidation < Base
6
+ def run
7
+ eager_load!
8
+
9
+ success(hash_from_pairs(models.reject do |model|
10
+ model.table_name.nil? || model.table_name == 'schema_migrations'
11
+ end.map do |model|
12
+ [
13
+ model.name,
14
+ connection.columns(model.table_name).select do |column|
15
+ validator_needed?(model, column) &&
16
+ !column.null &&
17
+ !has_presence_validator?(model, column)
18
+ end.map(&:name)
19
+ ]
20
+ end.reject do |model_name, columns|
21
+ columns.empty?
22
+ end))
23
+ end
24
+
25
+ private
26
+
27
+ def validator_needed?(model, column)
28
+ ![model.primary_key, 'created_at', 'updated_at'].include?(column.name)
29
+ end
30
+
31
+ def has_presence_validator?(model, column)
32
+ model.validators.any? do |validator|
33
+ validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
34
+ validator.attributes.include?(column.name.to_sym)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,51 @@
1
+ require "active_record_doctor/tasks/base"
2
+
3
+ module ActiveRecordDoctor
4
+ module Tasks
5
+ class MissingUniqueIndexes < Base
6
+ def run
7
+ eager_load!
8
+
9
+ success(hash_from_pairs(models.reject do |model|
10
+ model.table_name.nil?
11
+ end.map do |model|
12
+ [
13
+ model.table_name,
14
+ model.validators.select do |validator|
15
+ table_name = model.table_name
16
+ attributes = validator.attributes
17
+ scope = validator.options.fetch(:scope, [])
18
+
19
+ validator.is_a?(ActiveRecord::Validations::UniquenessValidator) &&
20
+ supported_validator?(validator) &&
21
+ !unique_index?(table_name, attributes, scope)
22
+ end.map do |validator|
23
+ scope = Array(validator.options.fetch(:scope, []))
24
+ attributes = validator.attributes
25
+ (scope + attributes).map(&:to_s)
26
+ end
27
+ ]
28
+ end.reject do |_table_name, indexes|
29
+ indexes.empty?
30
+ end))
31
+ end
32
+
33
+ private
34
+
35
+ def supported_validator?(validator)
36
+ validator.options[:if].nil? &&
37
+ validator.options[:unless].nil? &&
38
+ validator.options[:conditions].nil? &&
39
+ validator.options[:case_sensitive]
40
+ end
41
+
42
+ def unique_index?(table_name, columns, scope)
43
+ columns = (Array(scope) + columns).map(&:to_s)
44
+
45
+ indexes(table_name).any? do |index|
46
+ index.columns == columns && index.unique
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,33 +1,17 @@
1
- require "active_record_doctor/compatibility"
2
- require "active_record_doctor/printers/io_printer"
1
+ require "active_record_doctor/tasks/base"
3
2
 
4
3
  module ActiveRecordDoctor
5
4
  module Tasks
6
- class UndefinedTableReferences
7
- include Compatibility
8
-
9
- def self.run
10
- new.run
11
- end
12
-
13
- def initialize(printer: ActiveRecordDoctor::Printers::IOPrinter.new)
14
- @printer = printer
15
- end
16
-
5
+ class UndefinedTableReferences < Base
17
6
  def run
18
- @printer.print_undefined_table_references(undefined_table_references)
19
- undefined_table_references.present? ? 1 : 0
20
- end
21
-
22
- private
7
+ eager_load!
23
8
 
24
- def undefined_table_references
25
- Rails.application.eager_load!
26
-
27
- ActiveRecord::Base.descendants.select do |model|
9
+ offending_models = models.select do |model|
28
10
  model.table_name.present? &&
29
11
  !model.connection.tables.include?(model.table_name)
30
12
  end
13
+
14
+ [offending_models, offending_models.blank?]
31
15
  end
32
16
  end
33
17
  end
@@ -1,27 +1,10 @@
1
- require "active_record_doctor/compatibility"
2
- require "active_record_doctor/printers/io_printer"
1
+ require "active_record_doctor/tasks/base"
3
2
 
4
3
  module ActiveRecordDoctor
5
4
  module Tasks
6
- class UnindexedDeletedAt
7
- include Compatibility
8
-
9
- def self.run
10
- new.run
11
- end
12
-
13
- def initialize(printer: ActiveRecordDoctor::Printers::IOPrinter.new)
14
- @printer = printer
15
- end
16
-
5
+ class UnindexedDeletedAt < Base
17
6
  def run
18
- @printer.print_unindexed_deleted_at(unindexed_deleted_at)
19
- end
20
-
21
- private
22
-
23
- def unindexed_deleted_at
24
- connection.tables.select do |table|
7
+ success(connection.tables.select do |table|
25
8
  connection.columns(table).map(&:name).include?('deleted_at')
26
9
  end.flat_map do |table|
27
10
  connection.indexes(table).reject do |index|
@@ -29,11 +12,7 @@ module ActiveRecordDoctor
29
12
  end.map do |index|
30
13
  index.name
31
14
  end
32
- end
33
- end
34
-
35
- def connection
36
- @connection ||= ActiveRecord::Base.connection
15
+ end)
37
16
  end
38
17
  end
39
18
  end
@@ -1,27 +1,10 @@
1
- require "active_record_doctor/compatibility"
2
- require "active_record_doctor/printers/io_printer"
1
+ require "active_record_doctor/tasks/base"
3
2
 
4
3
  module ActiveRecordDoctor
5
4
  module Tasks
6
- class UnindexedForeignKeys
7
- include Compatibility
8
-
9
- def self.run
10
- new.run
11
- end
12
-
13
- def initialize(printer: ActiveRecordDoctor::Printers::IOPrinter.new)
14
- @printer = printer
15
- end
16
-
5
+ class UnindexedForeignKeys < Base
17
6
  def run
18
- @printer.print_unindexed_foreign_keys(unindexed_foreign_keys)
19
- end
20
-
21
- private
22
-
23
- def unindexed_foreign_keys
24
- hash_from_pairs(connection_tables.select do |table|
7
+ success(hash_from_pairs(tables.select do |table|
25
8
  "schema_migrations" != table
26
9
  end.map do |table|
27
10
  [
@@ -34,9 +17,11 @@ module ActiveRecordDoctor
34
17
  ]
35
18
  end.select do |table, columns|
36
19
  !columns.empty?
37
- end)
20
+ end))
38
21
  end
39
22
 
23
+ private
24
+
40
25
  def foreign_key?(table, column)
41
26
  column.name.end_with?("_id")
42
27
  end
@@ -53,14 +38,6 @@ module ActiveRecordDoctor
53
38
  index.columns == [type_column_name, column.name]
54
39
  end
55
40
  end
56
-
57
- def connection
58
- @connection ||= ActiveRecord::Base.connection
59
- end
60
-
61
- def hash_from_pairs(pairs)
62
- Hash[*pairs.flatten(1)]
63
- end
64
41
  end
65
42
  end
66
43
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordDoctor
2
- VERSION = "1.5.0"
2
+ VERSION = "1.6.0"
3
3
  end
@@ -0,0 +1,31 @@
1
+ require "active_record_doctor/tasks"
2
+ require "active_record_doctor/tasks/unindexed_foreign_keys"
3
+ require "active_record_doctor/tasks/extraneous_indexes"
4
+ require "active_record_doctor/tasks/missing_foreign_keys"
5
+ require "active_record_doctor/tasks/undefined_table_references"
6
+ require "active_record_doctor/tasks/unindexed_deleted_at"
7
+ require "active_record_doctor/tasks/missing_unique_indexes"
8
+ require "active_record_doctor/tasks/missing_presence_validation"
9
+ require "active_record_doctor/tasks/missing_non_null_constraint"
10
+
11
+ namespace :active_record_doctor do
12
+ def mount(task_class)
13
+ name = task_class.name.demodulize.underscore.to_sym
14
+
15
+ task name => :environment do
16
+ result, success = task_class.run
17
+ success = true if success.nil?
18
+
19
+ printer = ActiveRecordDoctor::Printers::IOPrinter.new
20
+ printer.public_send(name, result)
21
+
22
+ # nil doesn't indicate a failure but rather no explicit result. We assume
23
+ # success by default hence only false results in an erroneous exit code.
24
+ exit(1) if success == false
25
+ end
26
+ end
27
+
28
+ ActiveRecordDoctor::Tasks.all.each do |task|
29
+ mount task
30
+ end
31
+ end
@@ -3,8 +3,8 @@ require 'test_helper'
3
3
  require 'active_record_doctor/printers/io_printer'
4
4
 
5
5
  class ActiveRecordDoctor::Printers::IOPrinterTest < ActiveSupport::TestCase
6
- def test_print_unindexed_foreign_keys
7
- assert_equal(<<EOF, print_unindexed_foreign_keys({ "users" => ["profile_id", "account_id"], "account" => ["group_id"] }))
6
+ def test_unindexed_foreign_keys
7
+ assert_equal(<<EOF, unindexed_foreign_keys({ "users" => ["profile_id", "account_id"], "account" => ["group_id"] }))
8
8
  account group_id
9
9
  users account_id profile_id
10
10
  EOF
@@ -12,9 +12,9 @@ EOF
12
12
 
13
13
  private
14
14
 
15
- def print_unindexed_foreign_keys(argument)
15
+ def unindexed_foreign_keys(argument)
16
16
  io = StringIO.new
17
- ActiveRecordDoctor::Printers::IOPrinter.new(io: io).print_unindexed_foreign_keys(argument)
17
+ ActiveRecordDoctor::Printers::IOPrinter.new(io).unindexed_foreign_keys(argument)
18
18
  io.string
19
19
  end
20
20
  end
@@ -1,27 +1,81 @@
1
1
  require 'test_helper'
2
2
 
3
3
  require 'active_record_doctor/tasks/extraneous_indexes'
4
-
4
+
5
5
  class ActiveRecordDoctor::Tasks::ExtraneousIndexesTest < ActiveSupport::TestCase
6
- def test_extraneous_indexes_are_reported
7
- result = run_task
6
+ def test_index_on_primary_key_is_duplicate
7
+ Temping.create(:user, temporary: false) do
8
+ with_columns do |t|
9
+ t.index :id
10
+ end
11
+ end
12
+
13
+ assert_result([['index_users_on_id', [:primary_key, 'users']]])
14
+ end
15
+
16
+ def test_non_unique_version_of_index_is_duplicate
17
+ Temping.create(:user, temporary: false) do
18
+ with_columns do |t|
19
+ t.string :email
20
+ t.index :email, name: 'index_users_on_email'
21
+ t.index :email, unique: true, name: 'unique_index_on_users_email'
22
+ end
23
+ end
8
24
 
9
- assert_equal(
25
+ assert_result([
26
+ ['index_users_on_email', [:multi_column, 'unique_index_on_users_email']]
27
+ ])
28
+ end
29
+
30
+ def test_single_column_covered_by_unique_and_non_unique_multi_column_is_duplicate
31
+ Temping.create(:user, temporary: false) do
32
+ with_columns do |t|
33
+ t.string :first_name
34
+ t.string :last_name
35
+ t.string :email
36
+ t.index [:last_name, :first_name, :email]
37
+ t.index [:last_name, :first_name],
38
+ unique: true,
39
+ name: 'unique_index_on_users_last_name_and_first_name'
40
+ t.index :last_name
41
+ end
42
+ end
43
+
44
+ assert_result([
10
45
  [
11
- ["index_employers_on_id", [:primary_key, "employers"]],
12
- ["index_users_on_last_name", [:multi_column, "index_users_on_last_name_and_first_name_and_email", "unique_index_on_users_last_name_and_first_name"]],
13
- ["index_users_on_last_name_and_first_name", [:multi_column, "index_users_on_last_name_and_first_name_and_email", "unique_index_on_users_last_name_and_first_name"]],
14
- ["index_users_on_email", [:multi_column, "unique_index_on_users_email"]],
15
- ].sort,
16
- result.sort
17
- )
46
+ 'index_users_on_last_name',
47
+ [
48
+ :multi_column,
49
+ 'index_users_on_last_name_and_first_name_and_email',
50
+ 'unique_index_on_users_last_name_and_first_name'
51
+ ]
52
+ ]
53
+ ])
18
54
  end
19
55
 
20
- private
56
+ def test_multi_column_covered_by_unique_and_non_unique_multi_column_is_duplicate
57
+ Temping.create(:user, temporary: false) do
58
+ with_columns do |t|
59
+ t.string :first_name
60
+ t.string :last_name
61
+ t.string :email
62
+ t.index [:last_name, :first_name, :email]
63
+ t.index [:last_name, :first_name],
64
+ unique: true,
65
+ name: 'unique_index_on_users_last_name_and_first_name'
66
+ t.index [:last_name, :first_name]
67
+ end
68
+ end
21
69
 
22
- def run_task
23
- printer = SpyPrinter.new
24
- ActiveRecordDoctor::Tasks::ExtraneousIndexes.new(printer: printer).run
25
- printer.extraneous_indexes
70
+ assert_result([
71
+ [
72
+ 'index_users_on_last_name_and_first_name',
73
+ [
74
+ :multi_column,
75
+ 'index_users_on_last_name_and_first_name_and_email',
76
+ 'unique_index_on_users_last_name_and_first_name'
77
+ ]
78
+ ]
79
+ ])
26
80
  end
27
81
  end
@@ -3,17 +3,25 @@ require 'test_helper'
3
3
  require 'active_record_doctor/tasks/missing_foreign_keys'
4
4
 
5
5
  class ActiveRecordDoctor::Tasks::MissingForeignKeysTest < ActiveSupport::TestCase
6
- def test_missing_foreign_keys_are_reported
7
- result = run_task
6
+ def test_missing_foreign_key_is_reported
7
+ Temping.create(:companies, temporary: false)
8
+ Temping.create(:users, temporary: false) do
9
+ with_columns do |t|
10
+ t.references :company, foreign_key: false
11
+ end
12
+ end
8
13
 
9
- assert_equal({'users' => ['profile_id']}, result)
14
+ assert_equal({'users' => ['company_id']}, run_task)
10
15
  end
11
16
 
12
- private
17
+ def test_present_foreign_key_is_not_reported
18
+ Temping.create(:companies, temporary: false)
19
+ Temping.create(:users, temporary: false) do
20
+ with_columns do |t|
21
+ t.references :company, foreign_key: true
22
+ end
23
+ end
13
24
 
14
- def run_task
15
- printer = SpyPrinter.new
16
- ActiveRecordDoctor::Tasks::MissingForeignKeys.new(printer: printer).run
17
- printer.missing_foreign_keys
25
+ assert_equal({}, run_task)
18
26
  end
19
27
  end
@@ -0,0 +1,89 @@
1
+ require 'test_helper'
2
+
3
+ require 'active_record_doctor/tasks/missing_non_null_constraint'
4
+
5
+ class ActiveRecordDoctor::Tasks::MissingNonNullConstraintTest < ActiveSupport::TestCase
6
+ def test_presence_true_and_null_true
7
+ Temping.create(:users, temporary: false) do
8
+ validates :name, presence: true
9
+
10
+ with_columns do |t|
11
+ t.string :name, null: true
12
+ end
13
+ end
14
+
15
+ assert_equal({ 'users' => ['name'] }, run_task)
16
+ end
17
+
18
+ def test_presence_true_and_null_false
19
+ Temping.create(:users, temporary: false) do
20
+ validates :name, presence: true
21
+
22
+ with_columns do |t|
23
+ t.string :name, null: false
24
+ end
25
+ end
26
+
27
+ assert_equal({}, run_task)
28
+ end
29
+
30
+ def test_presence_false_and_null_true
31
+ Temping.create(:users, temporary: false) do
32
+ validates :name, presence: false
33
+
34
+ with_columns do |t|
35
+ t.string :name, null: true
36
+ end
37
+ end
38
+
39
+ assert_equal({}, run_task)
40
+ end
41
+
42
+ def test_presence_false_and_null_false
43
+ Temping.create(:users, temporary: false) do
44
+ validates :name, presence: false
45
+
46
+ with_columns do |t|
47
+ t.string :name, null: false
48
+ end
49
+ end
50
+
51
+ assert_equal({}, run_task)
52
+ end
53
+
54
+ def test_presence_true_with_if
55
+ Temping.create(:users, temporary: false) do
56
+ validates :name, presence: true, if: -> { false }
57
+
58
+ with_columns do |t|
59
+ t.string :name, null: true
60
+ end
61
+ end
62
+
63
+ assert_equal({}, run_task)
64
+ end
65
+
66
+ def test_presence_true_with_unless
67
+ Temping.create(:users, temporary: false) do
68
+ validates :name, presence: true, unless: -> { false }
69
+
70
+ with_columns do |t|
71
+ t.string :name, null: true
72
+ end
73
+ end
74
+
75
+ assert_equal({}, run_task)
76
+ end
77
+
78
+ def test_presence_true_with_allow_nil
79
+ Temping.create(:users, temporary: false) do
80
+ validates :name, presence: true, allow_nil: true
81
+
82
+ with_columns do |t|
83
+ t.string :name, null: true
84
+ end
85
+ end
86
+
87
+ assert_equal({}, run_task)
88
+ end
89
+ end