active_record_doctor 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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