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.
- checksums.yaml +4 -4
- data/README.md +73 -6
- data/lib/active_record_doctor/printers/io_printer.rb +35 -6
- data/lib/active_record_doctor/railtie.rb +1 -1
- data/lib/active_record_doctor/tasks.rb +3 -0
- data/lib/active_record_doctor/tasks/base.rb +64 -0
- data/lib/active_record_doctor/tasks/extraneous_indexes.rb +4 -27
- data/lib/active_record_doctor/tasks/missing_foreign_keys.rb +6 -29
- data/lib/active_record_doctor/tasks/missing_non_null_constraint.rb +42 -0
- data/lib/active_record_doctor/tasks/missing_presence_validation.rb +39 -0
- data/lib/active_record_doctor/tasks/missing_unique_indexes.rb +51 -0
- data/lib/active_record_doctor/tasks/undefined_table_references.rb +6 -22
- data/lib/active_record_doctor/tasks/unindexed_deleted_at.rb +4 -25
- data/lib/active_record_doctor/tasks/unindexed_foreign_keys.rb +6 -29
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/tasks/active_record_doctor.rake +31 -0
- data/test/active_record_doctor/printers/io_printer_test.rb +4 -4
- data/test/active_record_doctor/tasks/extraneous_indexes_test.rb +70 -16
- data/test/active_record_doctor/tasks/missing_foreign_keys_test.rb +16 -8
- data/test/active_record_doctor/tasks/missing_non_null_constraint_test.rb +89 -0
- data/test/active_record_doctor/tasks/missing_presence_validation_test.rb +49 -0
- data/test/active_record_doctor/tasks/missing_unique_indexes_test.rb +95 -0
- data/test/active_record_doctor/tasks/unindexed_deleted_at_test.rb +23 -8
- data/test/active_record_doctor/tasks/unindexed_foreign_keys_test.rb +16 -8
- data/test/dummy/app/models/application_record.rb +1 -1
- data/test/dummy/db/schema.rb +1 -50
- data/test/dummy/log/development.log +38 -498
- data/test/dummy/log/test.log +54108 -1571
- data/test/support/assertions.rb +11 -0
- data/test/support/forking_test.rb +28 -0
- data/test/support/temping.rb +25 -0
- data/test/test_helper.rb +0 -7
- metadata +34 -27
- data/lib/active_record_doctor/compatibility.rb +0 -11
- data/lib/tasks/active_record_doctor_tasks.rake +0 -27
- data/test/active_record_doctor/tasks/undefined_table_references_test.rb +0 -19
- data/test/dummy/app/models/comment.rb +0 -3
- data/test/dummy/app/models/contract.rb +0 -3
- data/test/dummy/app/models/employer.rb +0 -2
- data/test/dummy/app/models/profile.rb +0 -2
- data/test/dummy/app/models/user.rb +0 -3
- data/test/dummy/db/migrate/20160213101213_create_employers.rb +0 -15
- data/test/dummy/db/migrate/20160213101221_create_users.rb +0 -23
- data/test/dummy/db/migrate/20160213101232_create_profiles.rb +0 -15
- data/test/dummy/db/migrate/20160604081452_create_comments.rb +0 -11
- 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/
|
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
|
-
|
19
|
-
undefined_table_references.present? ? 1 : 0
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
7
|
+
eager_load!
|
23
8
|
|
24
|
-
|
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/
|
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
|
-
|
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/
|
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
|
-
|
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
|
@@ -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
|
7
|
-
assert_equal(<<EOF,
|
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
|
15
|
+
def unindexed_foreign_keys(argument)
|
16
16
|
io = StringIO.new
|
17
|
-
ActiveRecordDoctor::Printers::IOPrinter.new(io
|
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
|
7
|
-
|
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
|
-
|
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
|
-
|
12
|
-
[
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
7
|
-
|
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' => ['
|
14
|
+
assert_equal({'users' => ['company_id']}, run_task)
|
10
15
|
end
|
11
16
|
|
12
|
-
|
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
|
-
|
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
|