database_consistency 1.2.2 → 1.3.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.
- checksums.yaml +4 -4
- data/bin/database_consistency +5 -1
- data/lib/database_consistency/checkers/association_checkers/foreign_key_checker.rb +31 -3
- data/lib/database_consistency/checkers/association_checkers/foreign_key_type_checker.rb +39 -11
- data/lib/database_consistency/checkers/association_checkers/missing_index_checker.rb +2 -6
- data/lib/database_consistency/checkers/base_checker.rb +14 -7
- data/lib/database_consistency/checkers/column_checkers/length_constraint_checker.rb +3 -8
- data/lib/database_consistency/checkers/column_checkers/null_constraint_checker.rb +21 -7
- data/lib/database_consistency/checkers/column_checkers/primary_key_type_checker.rb +1 -4
- data/lib/database_consistency/checkers/index_checkers/redundant_index_checker.rb +19 -7
- data/lib/database_consistency/checkers/index_checkers/redundant_unique_index_checker.rb +19 -7
- data/lib/database_consistency/checkers/index_checkers/unique_index_checker.rb +1 -4
- data/lib/database_consistency/checkers/validator_checkers/missing_unique_index_checker.rb +1 -3
- data/lib/database_consistency/checkers/validators_fraction_checkers/column_presence_checker.rb +33 -9
- data/lib/database_consistency/report.rb +23 -0
- data/lib/database_consistency/version.rb +1 -1
- data/lib/database_consistency/writers/autofix/base.rb +15 -0
- data/lib/database_consistency/writers/autofix/helpers/migration.rb +28 -0
- data/lib/database_consistency/writers/autofix/migration_base.rb +21 -0
- data/lib/database_consistency/writers/autofix/missing_foreign_key.rb +19 -0
- data/lib/database_consistency/writers/autofix/null_constraint_missing.rb +19 -0
- data/lib/database_consistency/writers/autofix/templates/missing_foreign_key.tt +5 -0
- data/lib/database_consistency/writers/autofix/templates/null_constraint_missing.tt +5 -0
- data/lib/database_consistency/writers/autofix_writer.rb +40 -0
- data/lib/database_consistency/writers/base_writer.rb +0 -4
- data/lib/database_consistency/writers/helpers/pipes.rb +15 -0
- data/lib/database_consistency/writers/simple_writer.rb +30 -2
- data/lib/database_consistency/writers/todo_writer.rb +6 -6
- data/lib/database_consistency.rb +16 -2
- metadata +17 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 977b169b47dd5335a327746945ddd512717db1305744f44884d81bb0c7e82645
|
4
|
+
data.tar.gz: 59eb6c6ddefd09e77ba3f51dfbc2d4474eb92f62ca7b92437a4d9d70778d0d85
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b2b83f4ca288c7173f7abacc04c80f07b4d78c45f0fc949a801d1eb08b173c217ee524462eade7f443d9582243e593faa2730dd33e77e6d985f86c8ba86f6b4
|
7
|
+
data.tar.gz: e461a6da7caab598150602dac3f02075f4da00b9e68b481419e5717bb33fd3390d5e3f5b90c9ec315c2445af06812e435dafb3db81a7c2ced6cda4b957b035d0
|
data/bin/database_consistency
CHANGED
@@ -35,10 +35,14 @@ opt_parser = OptionParser.new do |opts|
|
|
35
35
|
config << f
|
36
36
|
end
|
37
37
|
|
38
|
-
opts.on('-g', '--generate-todo', 'Generate TODO file with every failing check disabled. You can pass existing configurations so the generated file will have only new failures.') do
|
38
|
+
opts.on('-g', '--generate-todo', 'Generate TODO file with every failing check disabled. You can pass existing configurations so the generated file will have only new failures.') do
|
39
39
|
options[:todo] = true
|
40
40
|
end
|
41
41
|
|
42
|
+
opts.on('-fix', '--autofix', 'Automatically fixes issues by adjusting the code or generating missing migrations.') do
|
43
|
+
options[:autofix] = true
|
44
|
+
end
|
45
|
+
|
42
46
|
opts.on('-h', '--help', 'Prints this help.') do
|
43
47
|
puts opts
|
44
48
|
exit
|
@@ -4,7 +4,26 @@ module DatabaseConsistency
|
|
4
4
|
module Checkers
|
5
5
|
# This class checks if non polymorphic +belongs_to+ association has foreign key constraint
|
6
6
|
class ForeignKeyChecker < AssociationChecker
|
7
|
-
|
7
|
+
class Report < DatabaseConsistency::Report # :nodoc:
|
8
|
+
attr_reader :primary_table, :primary_key, :foreign_table, :foreign_key
|
9
|
+
|
10
|
+
def initialize(primary_table:, foreign_table:, primary_key:, foreign_key:, **args)
|
11
|
+
super(**args)
|
12
|
+
@primary_table = primary_table
|
13
|
+
@primary_key = primary_key
|
14
|
+
@foreign_table = foreign_table
|
15
|
+
@foreign_key = foreign_key
|
16
|
+
end
|
17
|
+
|
18
|
+
def attributes
|
19
|
+
super.merge(
|
20
|
+
primary_table: primary_table,
|
21
|
+
primary_key: primary_key,
|
22
|
+
foreign_table: foreign_table,
|
23
|
+
foreign_key: foreign_key
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
8
27
|
|
9
28
|
private
|
10
29
|
|
@@ -33,11 +52,20 @@ module DatabaseConsistency
|
|
33
52
|
# | ----------- | ------ |
|
34
53
|
# | persisted | ok |
|
35
54
|
# | missing | fail |
|
36
|
-
def check
|
55
|
+
def check # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
37
56
|
if model.connection.foreign_keys(model.table_name).find { |fk| fk.column == association.foreign_key.to_s }
|
38
57
|
report_template(:ok)
|
39
58
|
else
|
40
|
-
|
59
|
+
Report.new(
|
60
|
+
status: :fail,
|
61
|
+
error_message: nil,
|
62
|
+
error_slug: :missing_foreign_key,
|
63
|
+
primary_table: association.table_name.to_s,
|
64
|
+
primary_key: association.association_primary_key.to_s,
|
65
|
+
foreign_table: association.active_record.table_name.to_s,
|
66
|
+
foreign_key: association.foreign_key.to_s,
|
67
|
+
**report_attributes
|
68
|
+
)
|
41
69
|
end
|
42
70
|
end
|
43
71
|
end
|
@@ -4,7 +4,26 @@ module DatabaseConsistency
|
|
4
4
|
module Checkers
|
5
5
|
# This class checks if association's foreign key type covers associated model's primary key (same or bigger)
|
6
6
|
class ForeignKeyTypeChecker < AssociationChecker
|
7
|
-
|
7
|
+
class Report < DatabaseConsistency::Report # :nodoc:
|
8
|
+
attr_reader :pk_name, :pk_type, :fk_name, :fk_type
|
9
|
+
|
10
|
+
def initialize(fk_name: nil, fk_type: nil, pk_name: nil, pk_type: nil, **args)
|
11
|
+
super(**args)
|
12
|
+
@fk_name = fk_name
|
13
|
+
@fk_type = fk_type
|
14
|
+
@pk_name = pk_name
|
15
|
+
@pk_type = pk_type
|
16
|
+
end
|
17
|
+
|
18
|
+
def attributes
|
19
|
+
super.merge(
|
20
|
+
fk_name: fk_name,
|
21
|
+
fk_type: fk_type,
|
22
|
+
pk_name: pk_name,
|
23
|
+
pk_type: pk_type
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
8
27
|
|
9
28
|
private
|
10
29
|
|
@@ -27,23 +46,32 @@ module DatabaseConsistency
|
|
27
46
|
# | ------------- | ------ |
|
28
47
|
# | covers | ok |
|
29
48
|
# | doesn't cover | fail |
|
30
|
-
def check
|
49
|
+
def check # rubocop:disable Metrics/MethodLength
|
31
50
|
if converted_type(associated_column).cover?(converted_type(primary_column))
|
32
51
|
report_template(:ok)
|
33
52
|
else
|
34
|
-
report_template(:fail,
|
53
|
+
report_template(:fail, error_slug: :inconsistent_types)
|
35
54
|
end
|
36
55
|
rescue Errors::MissingField => e
|
37
|
-
|
56
|
+
Report.new(
|
57
|
+
status: :fail,
|
58
|
+
error_slug: nil,
|
59
|
+
error_message: e.message,
|
60
|
+
**report_attributes
|
61
|
+
)
|
38
62
|
end
|
39
63
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
64
|
+
def report_template(status, error_slug: nil)
|
65
|
+
Report.new(
|
66
|
+
status: status,
|
67
|
+
error_slug: error_slug,
|
68
|
+
error_message: nil,
|
69
|
+
fk_type: converted_type(associated_column).type,
|
70
|
+
fk_name: associated_key,
|
71
|
+
pk_type: converted_type(primary_column).type,
|
72
|
+
pk_name: primary_key,
|
73
|
+
**report_attributes
|
74
|
+
)
|
47
75
|
end
|
48
76
|
|
49
77
|
# @return [String]
|
@@ -4,10 +4,6 @@ module DatabaseConsistency
|
|
4
4
|
module Checkers
|
5
5
|
# This class checks if association's foreign key has index in the database
|
6
6
|
class MissingIndexChecker < AssociationChecker
|
7
|
-
# Message templates
|
8
|
-
MISSING_INDEX = 'associated model should have proper index in the database'
|
9
|
-
MISSING_UNIQUE_INDEX = 'associated model should have proper unique index in the database'
|
10
|
-
|
11
7
|
private
|
12
8
|
|
13
9
|
# We skip check when:
|
@@ -40,7 +36,7 @@ module DatabaseConsistency
|
|
40
36
|
if unique_index
|
41
37
|
report_template(:ok)
|
42
38
|
else
|
43
|
-
report_template(:fail,
|
39
|
+
report_template(:fail, error_slug: :has_one_missing_unique_index)
|
44
40
|
end
|
45
41
|
end
|
46
42
|
|
@@ -48,7 +44,7 @@ module DatabaseConsistency
|
|
48
44
|
if index
|
49
45
|
report_template(:ok)
|
50
46
|
else
|
51
|
-
report_template(:fail,
|
47
|
+
report_template(:fail, error_slug: :association_missing_index)
|
52
48
|
end
|
53
49
|
end
|
54
50
|
|
@@ -66,16 +66,23 @@ module DatabaseConsistency
|
|
66
66
|
raise NotImplementedError
|
67
67
|
end
|
68
68
|
|
69
|
-
# @return [
|
70
|
-
def report_template(status,
|
71
|
-
|
72
|
-
checker_name: checker_name,
|
73
|
-
table_or_model_name: table_or_model_name,
|
74
|
-
column_or_attribute_name: column_or_attribute_name,
|
69
|
+
# @return [DatabaseConsistency::Report]
|
70
|
+
def report_template(status, error_slug: nil, error_message: nil)
|
71
|
+
Report.new(
|
75
72
|
status: status,
|
76
|
-
|
73
|
+
error_slug: error_slug,
|
74
|
+
error_message: error_message,
|
75
|
+
**report_attributes
|
77
76
|
)
|
78
77
|
end
|
78
|
+
|
79
|
+
def report_attributes
|
80
|
+
{
|
81
|
+
checker_name: checker_name,
|
82
|
+
table_or_model_name: table_or_model_name,
|
83
|
+
column_or_attribute_name: column_or_attribute_name
|
84
|
+
}
|
85
|
+
end
|
79
86
|
end
|
80
87
|
end
|
81
88
|
end
|
@@ -4,11 +4,6 @@ module DatabaseConsistency
|
|
4
4
|
module Checkers
|
5
5
|
# This class checks missing presence validator
|
6
6
|
class LengthConstraintChecker < ColumnChecker
|
7
|
-
# Message templates
|
8
|
-
VALIDATOR_MISSING = 'column has limit in the database but do not have length validator'
|
9
|
-
GREATER_LIMIT = 'column has greater limit in the database than in length validator'
|
10
|
-
LOWER_LIMIT = 'column has lower limit in the database than in length validator'
|
11
|
-
|
12
7
|
VALIDATOR_CLASS =
|
13
8
|
if defined?(ActiveRecord::Validations::LengthValidator)
|
14
9
|
ActiveRecord::Validations::LengthValidator
|
@@ -38,14 +33,14 @@ module DatabaseConsistency
|
|
38
33
|
# | small | warning |
|
39
34
|
# | missing | fail |
|
40
35
|
def check
|
41
|
-
return report_template(:fail,
|
36
|
+
return report_template(:fail, error_slug: :length_validator_missing) unless validator
|
42
37
|
|
43
38
|
if valid?(:==)
|
44
39
|
report_template(:ok)
|
45
40
|
elsif valid?(:<)
|
46
|
-
report_template(:warning,
|
41
|
+
report_template(:warning, error_slug: :length_validator_greater_limit)
|
47
42
|
else
|
48
|
-
report_template(:fail,
|
43
|
+
report_template(:fail, error_slug: :length_validator_lower_limit)
|
49
44
|
end
|
50
45
|
end
|
51
46
|
|
@@ -4,10 +4,18 @@ module DatabaseConsistency
|
|
4
4
|
module Checkers
|
5
5
|
# This class checks missing presence validator
|
6
6
|
class NullConstraintChecker < ColumnChecker
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
class Report < DatabaseConsistency::Report # :nodoc:
|
8
|
+
attr_reader :association_name
|
9
|
+
|
10
|
+
def initialize(association_name:, **args)
|
11
|
+
super(**args)
|
12
|
+
@association_name = association_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def attributes
|
16
|
+
super.merge(association_name: association_name)
|
17
|
+
end
|
18
|
+
end
|
11
19
|
|
12
20
|
private
|
13
21
|
|
@@ -29,13 +37,19 @@ module DatabaseConsistency
|
|
29
37
|
#
|
30
38
|
# We consider PresenceValidation, InclusionValidation, ExclusionValidation, NumericalityValidator with nil,
|
31
39
|
# or required BelongsTo association using this column
|
32
|
-
def check
|
40
|
+
def check # rubocop:disable Metrics/MethodLength
|
33
41
|
if valid?
|
34
42
|
report_template(:ok)
|
35
43
|
elsif belongs_to_association
|
36
|
-
|
44
|
+
Report.new(
|
45
|
+
status: :fail,
|
46
|
+
error_slug: :null_constraint_association_misses_validator,
|
47
|
+
error_message: nil,
|
48
|
+
association_name: belongs_to_association.name.to_s,
|
49
|
+
**report_attributes
|
50
|
+
)
|
37
51
|
else
|
38
|
-
report_template(:fail,
|
52
|
+
report_template(:fail, error_slug: :null_constraint_misses_validator)
|
39
53
|
end
|
40
54
|
end
|
41
55
|
|
@@ -4,9 +4,6 @@ module DatabaseConsistency
|
|
4
4
|
module Checkers
|
5
5
|
# This class checks missing presence validator
|
6
6
|
class PrimaryKeyTypeChecker < ColumnChecker
|
7
|
-
# Message templates
|
8
|
-
VALIDATOR_MISSING = 'column has int/serial type but recommended to have bigint/bigserial/uuid'
|
9
|
-
|
10
7
|
private
|
11
8
|
|
12
9
|
VALID_TYPES = %w[bigserial bigint uuid].freeze
|
@@ -28,7 +25,7 @@ module DatabaseConsistency
|
|
28
25
|
if valid?
|
29
26
|
report_template(:ok)
|
30
27
|
else
|
31
|
-
report_template(:fail,
|
28
|
+
report_template(:fail, error_slug: :small_primary_key)
|
32
29
|
end
|
33
30
|
end
|
34
31
|
|
@@ -4,8 +4,18 @@ module DatabaseConsistency
|
|
4
4
|
module Checkers
|
5
5
|
# This class checks redundant database indexes
|
6
6
|
class RedundantIndexChecker < IndexChecker
|
7
|
-
#
|
8
|
-
|
7
|
+
class Report < DatabaseConsistency::Report # :nodoc:
|
8
|
+
attr_reader :index_name
|
9
|
+
|
10
|
+
def initialize(index_name:, **args)
|
11
|
+
super(**args)
|
12
|
+
@index_name = index_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def attributes
|
16
|
+
super.merge(index_name: index_name)
|
17
|
+
end
|
18
|
+
end
|
9
19
|
|
10
20
|
private
|
11
21
|
|
@@ -23,16 +33,18 @@ module DatabaseConsistency
|
|
23
33
|
#
|
24
34
|
def check
|
25
35
|
if covered_by_index
|
26
|
-
|
36
|
+
Report.new(
|
37
|
+
status: :fail,
|
38
|
+
error_slug: :redundant_index,
|
39
|
+
error_message: nil,
|
40
|
+
index_name: covered_by_index.name,
|
41
|
+
**report_attributes
|
42
|
+
)
|
27
43
|
else
|
28
44
|
report_template(:ok)
|
29
45
|
end
|
30
46
|
end
|
31
47
|
|
32
|
-
def render_message
|
33
|
-
REDUNDANT_INDEX.sub('%index', covered_by_index.name)
|
34
|
-
end
|
35
|
-
|
36
48
|
def covered_by_index
|
37
49
|
@covered_by_index ||=
|
38
50
|
model.connection.indexes(model.table_name).find do |another_index|
|
@@ -4,8 +4,18 @@ module DatabaseConsistency
|
|
4
4
|
module Checkers
|
5
5
|
# This class checks redundant database indexes
|
6
6
|
class RedundantUniqueIndexChecker < IndexChecker
|
7
|
-
#
|
8
|
-
|
7
|
+
class Report < DatabaseConsistency::Report # :nodoc:
|
8
|
+
attr_reader :index_name
|
9
|
+
|
10
|
+
def initialize(index_name:, **args)
|
11
|
+
super(**args)
|
12
|
+
@index_name = index_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def attributes
|
16
|
+
super.merge(index_name: index_name)
|
17
|
+
end
|
18
|
+
end
|
9
19
|
|
10
20
|
private
|
11
21
|
|
@@ -23,16 +33,18 @@ module DatabaseConsistency
|
|
23
33
|
#
|
24
34
|
def check
|
25
35
|
if covered_by_index
|
26
|
-
|
36
|
+
Report.new(
|
37
|
+
status: :fail,
|
38
|
+
error_slug: :redundant_unique_index,
|
39
|
+
error_message: nil,
|
40
|
+
index_name: covered_by_index.name,
|
41
|
+
**report_attributes
|
42
|
+
)
|
27
43
|
else
|
28
44
|
report_template(:ok)
|
29
45
|
end
|
30
46
|
end
|
31
47
|
|
32
|
-
def render_message
|
33
|
-
REDUNDANT_UNIQUE_INDEX.sub('%index', covered_by_index.name)
|
34
|
-
end
|
35
|
-
|
36
48
|
def covered_by_index
|
37
49
|
@covered_by_index ||=
|
38
50
|
model.connection.indexes(model.table_name).find do |another_index|
|
@@ -4,9 +4,6 @@ module DatabaseConsistency
|
|
4
4
|
module Checkers
|
5
5
|
# This class checks missing uniqueness validator
|
6
6
|
class UniqueIndexChecker < IndexChecker
|
7
|
-
# Message templates
|
8
|
-
VALIDATOR_MISSING = 'index is unique in the database but do not have uniqueness validator'
|
9
|
-
|
10
7
|
private
|
11
8
|
|
12
9
|
# We skip check when:
|
@@ -25,7 +22,7 @@ module DatabaseConsistency
|
|
25
22
|
if valid?
|
26
23
|
report_template(:ok)
|
27
24
|
else
|
28
|
-
report_template(:fail,
|
25
|
+
report_template(:fail, error_slug: :missing_uniqueness_validation)
|
29
26
|
end
|
30
27
|
end
|
31
28
|
|
@@ -4,8 +4,6 @@ module DatabaseConsistency
|
|
4
4
|
module Checkers
|
5
5
|
# This class checks if uniqueness validator has unique index in the database
|
6
6
|
class MissingUniqueIndexChecker < ValidatorChecker
|
7
|
-
MISSING_INDEX = 'model should have proper unique index in the database'
|
8
|
-
|
9
7
|
def column_or_attribute_name
|
10
8
|
@column_or_attribute_name ||= Helper.uniqueness_validator_columns(attribute, validator, model).join('+')
|
11
9
|
end
|
@@ -27,7 +25,7 @@ module DatabaseConsistency
|
|
27
25
|
if unique_index
|
28
26
|
report_template(:ok)
|
29
27
|
else
|
30
|
-
report_template(:fail,
|
28
|
+
report_template(:fail, error_slug: :missing_unique_index)
|
31
29
|
end
|
32
30
|
end
|
33
31
|
|
data/lib/database_consistency/checkers/validators_fraction_checkers/column_presence_checker.rb
CHANGED
@@ -5,10 +5,20 @@ module DatabaseConsistency
|
|
5
5
|
# This class checks if presence validator has non-null constraint in the database
|
6
6
|
class ColumnPresenceChecker < ValidatorsFractionChecker
|
7
7
|
WEAK_OPTIONS = %i[allow_nil allow_blank if unless on].freeze
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
|
9
|
+
class Report < DatabaseConsistency::Report # :nodoc:
|
10
|
+
attr_reader :table_name, :column_name
|
11
|
+
|
12
|
+
def initialize(table_name:, column_name:, **args)
|
13
|
+
super(**args)
|
14
|
+
@table_name = table_name
|
15
|
+
@column_name = column_name
|
16
|
+
end
|
17
|
+
|
18
|
+
def attributes
|
19
|
+
super.merge(table_name: table_name, column_name: column_name)
|
20
|
+
end
|
21
|
+
end
|
12
22
|
|
13
23
|
private
|
14
24
|
|
@@ -35,24 +45,38 @@ module DatabaseConsistency
|
|
35
45
|
def check
|
36
46
|
report_message
|
37
47
|
rescue Errors::MissingField => e
|
38
|
-
report_template(:fail, e.message)
|
48
|
+
report_template(:fail, error_message: e.message)
|
39
49
|
end
|
40
50
|
|
41
51
|
def weak_option?
|
42
52
|
validators.all? { |validator| validator.options.slice(*WEAK_OPTIONS).any? }
|
43
53
|
end
|
44
54
|
|
45
|
-
def report_message
|
55
|
+
def report_message # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
46
56
|
can_be_null = column.null
|
47
57
|
has_weak_option = weak_option?
|
48
58
|
|
49
59
|
return report_template(:ok) if can_be_null == has_weak_option
|
50
|
-
return report_template(:fail,
|
60
|
+
return report_template(:fail, error_slug: :possible_null) unless can_be_null
|
51
61
|
|
52
62
|
if regular_column
|
53
|
-
|
63
|
+
Report.new(
|
64
|
+
status: :fail,
|
65
|
+
error_slug: :null_constraint_missing,
|
66
|
+
error_message: nil,
|
67
|
+
table_name: model.table_name.to_s,
|
68
|
+
column_name: attribute.to_s,
|
69
|
+
**report_attributes
|
70
|
+
)
|
54
71
|
else
|
55
|
-
|
72
|
+
Report.new(
|
73
|
+
status: :fail,
|
74
|
+
error_slug: :association_missing_null_constraint,
|
75
|
+
error_message: nil,
|
76
|
+
table_name: model.table_name.to_s,
|
77
|
+
column_name: association_reflection.foreign_key.to_s,
|
78
|
+
**report_attributes
|
79
|
+
)
|
56
80
|
end
|
57
81
|
end
|
58
82
|
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DatabaseConsistency
|
4
|
+
class Report # :nodoc:
|
5
|
+
attr_reader :checker_name, :table_or_model_name, :column_or_attribute_name, :status, :error_slug, :error_message
|
6
|
+
|
7
|
+
def initialize(checker_name:, table_or_model_name:, column_or_attribute_name:, status:, error_slug:, error_message:) # rubocop:disable Metrics/ParameterLists
|
8
|
+
@checker_name = checker_name
|
9
|
+
@table_or_model_name = table_or_model_name
|
10
|
+
@column_or_attribute_name = column_or_attribute_name
|
11
|
+
@status = status
|
12
|
+
@error_slug = error_slug
|
13
|
+
@error_message = error_message
|
14
|
+
end
|
15
|
+
|
16
|
+
def attributes
|
17
|
+
{
|
18
|
+
error_slug: error_slug,
|
19
|
+
error_message: error_message
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DatabaseConsistency
|
4
|
+
module Writers
|
5
|
+
module Autofix
|
6
|
+
module Helpers
|
7
|
+
module Migration # :nodoc:
|
8
|
+
def migration_path(name)
|
9
|
+
migration_paths = ActiveRecord::Migrator.migrations_paths
|
10
|
+
schema_migration = ActiveRecord::Base.connection.schema_migration
|
11
|
+
|
12
|
+
last = ActiveRecord::MigrationContext.new(migration_paths, schema_migration).migrations.last
|
13
|
+
version = ActiveRecord::Migration.next_migration_number(last&.version.to_i + 1)
|
14
|
+
|
15
|
+
"db/migrate/#{version}_#{name.underscore}.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
def migration_configuration(name)
|
19
|
+
{
|
20
|
+
migration_name: name.camelcase,
|
21
|
+
migration_version: ActiveRecord::Migration.current_version
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DatabaseConsistency
|
4
|
+
module Writers
|
5
|
+
module Autofix
|
6
|
+
class MigrationBase < Base # :nodoc:
|
7
|
+
include Helpers::Migration
|
8
|
+
|
9
|
+
def fix!
|
10
|
+
File.write(migration_path(migration_name), migration)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def migration
|
16
|
+
File.read(template_path) % report.attributes.merge(migration_configuration(migration_name))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DatabaseConsistency
|
4
|
+
module Writers
|
5
|
+
module Autofix
|
6
|
+
class MissingForeignKey < MigrationBase # :nodoc:
|
7
|
+
private
|
8
|
+
|
9
|
+
def migration_name
|
10
|
+
"add_#{report.primary_table}_#{report.foreign_table}_foreign_key"
|
11
|
+
end
|
12
|
+
|
13
|
+
def template_path
|
14
|
+
File.join(__dir__, 'templates', 'missing_foreign_key.tt')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DatabaseConsistency
|
4
|
+
module Writers
|
5
|
+
module Autofix
|
6
|
+
class NullConstraintMissing < MigrationBase # :nodoc:
|
7
|
+
private
|
8
|
+
|
9
|
+
def migration_name
|
10
|
+
"change_#{report.table_name}_#{report.column_name}_null_constraint"
|
11
|
+
end
|
12
|
+
|
13
|
+
def template_path
|
14
|
+
File.join(__dir__, 'templates', 'null_constraint_missing.tt')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DatabaseConsistency
|
4
|
+
# The module contains formatters
|
5
|
+
module Writers
|
6
|
+
# The simplest formatter
|
7
|
+
class AutofixWriter < BaseWriter
|
8
|
+
SLUG_TO_GENERATOR = {
|
9
|
+
missing_foreign_key: Autofix::MissingForeignKey,
|
10
|
+
null_constraint_missing: Autofix::NullConstraintMissing,
|
11
|
+
association_missing_null_constraint: Autofix::NullConstraintMissing
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
def write
|
15
|
+
reports.each do |report|
|
16
|
+
next unless fix?(report)
|
17
|
+
|
18
|
+
fix(report)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def reports
|
25
|
+
results.then(&Helpers::Pipes.method(:unique))
|
26
|
+
end
|
27
|
+
|
28
|
+
def fix?(report)
|
29
|
+
report.status == :fail
|
30
|
+
end
|
31
|
+
|
32
|
+
def fix(report)
|
33
|
+
klass = SLUG_TO_GENERATOR[report.error_slug]
|
34
|
+
return unless klass
|
35
|
+
|
36
|
+
klass.new(report).fix!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -18,6 +18,26 @@ module DatabaseConsistency
|
|
18
18
|
fail: :red
|
19
19
|
}.freeze
|
20
20
|
|
21
|
+
SLUG_TO_MESSAGE = {
|
22
|
+
missing_foreign_key: 'should have foreign key in the database',
|
23
|
+
inconsistent_types: "foreign key %<fk_name>s with type %<fk_type>s doesn't cover primary key %<pk_name>s with type %<pk_type>s", # rubocop:disable Layout/LineLength
|
24
|
+
has_one_missing_unique_index: 'associated model should have proper unique index in the database',
|
25
|
+
association_missing_index: 'associated model should have proper index in the database',
|
26
|
+
length_validator_missing: 'column has limit in the database but do not have length validator',
|
27
|
+
length_validator_greater_limit: 'column has greater limit in the database than in length validator',
|
28
|
+
length_validator_lower_limit: 'column has lower limit in the database than in length validator',
|
29
|
+
null_constraint_association_misses_validator: 'column is required in the database but do not have presence validator for association %<association_name>s', # rubocop:disable Layout/LineLength
|
30
|
+
null_constraint_misses_validator: 'column is required in the database but do not have presence validator',
|
31
|
+
small_primary_key: 'column has int/serial type but recommended to have bigint/bigserial/uuid',
|
32
|
+
redundant_index: 'index is redundant as %<index_name>s covers it',
|
33
|
+
redundant_unique_index: 'index uniqueness is redundant as %<index_name>s covers it',
|
34
|
+
missing_uniqueness_validation: 'index is unique in the database but do not have uniqueness validator',
|
35
|
+
missing_unique_index: 'model should have proper unique index in the database',
|
36
|
+
possible_null: 'column is required but there is possible null value insert',
|
37
|
+
null_constraint_missing: 'column should be required in the database',
|
38
|
+
association_missing_null_constraint: 'association foreign key column should be required in the database'
|
39
|
+
}.freeze
|
40
|
+
|
21
41
|
def write
|
22
42
|
results.each do |result|
|
23
43
|
next unless write?(result.status)
|
@@ -26,11 +46,19 @@ module DatabaseConsistency
|
|
26
46
|
end
|
27
47
|
end
|
28
48
|
|
49
|
+
private
|
50
|
+
|
29
51
|
def msg(result)
|
30
|
-
"#{result.checker_name} #{status_text(result)} #{key_text(result)} #{result
|
52
|
+
"#{result.checker_name} #{status_text(result)} #{key_text(result)} #{message_text(result)}"
|
31
53
|
end
|
32
54
|
|
33
|
-
|
55
|
+
def write?(status)
|
56
|
+
status == :fail || config.debug?
|
57
|
+
end
|
58
|
+
|
59
|
+
def message_text(result)
|
60
|
+
(SLUG_TO_MESSAGE[result.error_slug] || result.error_message || '') % result.attributes
|
61
|
+
end
|
34
62
|
|
35
63
|
def key_text(result)
|
36
64
|
"#{colorize(result.table_or_model_name, :blue)} #{colorize(result.column_or_attribute_name, :yellow)}"
|
@@ -6,32 +6,32 @@ module DatabaseConsistency
|
|
6
6
|
# The writer that generates to-do file
|
7
7
|
class TodoWriter < BaseWriter
|
8
8
|
def write
|
9
|
-
|
9
|
+
h = results.each_with_object({}) do |result, hash|
|
10
10
|
next unless write?(result.status)
|
11
11
|
|
12
12
|
assign_result(hash, result)
|
13
13
|
end
|
14
14
|
|
15
|
-
File.write(file_name,
|
15
|
+
File.write(file_name, h.to_yaml)
|
16
16
|
end
|
17
17
|
|
18
|
+
private
|
19
|
+
|
18
20
|
def write?(status)
|
19
21
|
status == :fail
|
20
22
|
end
|
21
23
|
|
22
|
-
private
|
23
|
-
|
24
24
|
def assign_result(hash, result)
|
25
25
|
hash[result.table_or_model_name] ||= {}
|
26
26
|
hash[result.table_or_model_name][result.column_or_attribute_name] ||= {}
|
27
|
-
hash[result.table_or_model_name][result.column_or_attribute_name][result.checker_name] = {'enabled' => false}
|
27
|
+
hash[result.table_or_model_name][result.column_or_attribute_name][result.checker_name] = { 'enabled' => false }
|
28
28
|
end
|
29
29
|
|
30
30
|
def file_name
|
31
31
|
[nil, *(1..100)].each do |number|
|
32
32
|
name = generate_file_name(number)
|
33
33
|
|
34
|
-
return name unless File.
|
34
|
+
return name unless File.exist?(name)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
data/lib/database_consistency.rb
CHANGED
@@ -7,11 +7,21 @@ require 'database_consistency/helper'
|
|
7
7
|
require 'database_consistency/configuration'
|
8
8
|
require 'database_consistency/rescue_error'
|
9
9
|
require 'database_consistency/errors'
|
10
|
+
require 'database_consistency/report'
|
11
|
+
|
12
|
+
require 'database_consistency/writers/helpers/pipes'
|
10
13
|
|
11
14
|
require 'database_consistency/writers/base_writer'
|
12
15
|
require 'database_consistency/writers/simple_writer'
|
13
16
|
require 'database_consistency/writers/todo_writer'
|
14
17
|
|
18
|
+
require 'database_consistency/writers/autofix/helpers/migration'
|
19
|
+
require 'database_consistency/writers/autofix/base'
|
20
|
+
require 'database_consistency/writers/autofix/migration_base'
|
21
|
+
require 'database_consistency/writers/autofix/missing_foreign_key'
|
22
|
+
require 'database_consistency/writers/autofix/null_constraint_missing'
|
23
|
+
require 'database_consistency/writers/autofix_writer'
|
24
|
+
|
15
25
|
require 'database_consistency/databases/factory'
|
16
26
|
require 'database_consistency/databases/types/base'
|
17
27
|
require 'database_consistency/databases/types/sqlite'
|
@@ -49,11 +59,15 @@ require 'database_consistency/processors/indexes_processor'
|
|
49
59
|
# The root module
|
50
60
|
module DatabaseConsistency
|
51
61
|
class << self
|
52
|
-
def run(*args, **opts)
|
62
|
+
def run(*args, **opts) # rubocop:disable Metrics/MethodLength
|
53
63
|
configuration = Configuration.new(*args)
|
54
64
|
reports = Processors.reports(configuration)
|
55
65
|
|
56
|
-
if opts[:
|
66
|
+
if opts[:autofix]
|
67
|
+
Writers::AutofixWriter.write(reports, config: configuration)
|
68
|
+
|
69
|
+
0
|
70
|
+
elsif opts[:todo]
|
57
71
|
Writers::TodoWriter.write(reports, config: configuration)
|
58
72
|
|
59
73
|
0
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: database_consistency
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evgeniy Demin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-11-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -175,17 +175,30 @@ files:
|
|
175
175
|
- lib/database_consistency/processors/indexes_processor.rb
|
176
176
|
- lib/database_consistency/processors/validators_fractions_processor.rb
|
177
177
|
- lib/database_consistency/processors/validators_processor.rb
|
178
|
+
- lib/database_consistency/report.rb
|
178
179
|
- lib/database_consistency/rescue_error.rb
|
179
180
|
- lib/database_consistency/templates/rails_defaults.yml
|
180
181
|
- lib/database_consistency/version.rb
|
182
|
+
- lib/database_consistency/writers/autofix/base.rb
|
183
|
+
- lib/database_consistency/writers/autofix/helpers/migration.rb
|
184
|
+
- lib/database_consistency/writers/autofix/migration_base.rb
|
185
|
+
- lib/database_consistency/writers/autofix/missing_foreign_key.rb
|
186
|
+
- lib/database_consistency/writers/autofix/null_constraint_missing.rb
|
187
|
+
- lib/database_consistency/writers/autofix/templates/missing_foreign_key.tt
|
188
|
+
- lib/database_consistency/writers/autofix/templates/null_constraint_missing.tt
|
189
|
+
- lib/database_consistency/writers/autofix_writer.rb
|
181
190
|
- lib/database_consistency/writers/base_writer.rb
|
191
|
+
- lib/database_consistency/writers/helpers/pipes.rb
|
182
192
|
- lib/database_consistency/writers/simple_writer.rb
|
183
193
|
- lib/database_consistency/writers/todo_writer.rb
|
184
194
|
homepage: https://github.com/djezzzl/database_consistency
|
185
195
|
licenses:
|
186
196
|
- MIT
|
187
197
|
metadata: {}
|
188
|
-
post_install_message:
|
198
|
+
post_install_message: "Thank you for using the gem!\n\nIf the project helps you or
|
199
|
+
your organization, I would be very grateful if you contribute or donate. \nYour
|
200
|
+
support is an incredible motivation and the biggest reward for my hard work.\n\nhttps://github.com/djezzzl/database_consistency#contributing\nhttps://opencollective.com/database_consistency#support\n\nThank
|
201
|
+
you for your attention,\nEvgeniy Demin\n"
|
189
202
|
rdoc_options: []
|
190
203
|
require_paths:
|
191
204
|
- lib
|
@@ -200,7 +213,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
200
213
|
- !ruby/object:Gem::Version
|
201
214
|
version: '0'
|
202
215
|
requirements: []
|
203
|
-
rubygems_version: 3.
|
216
|
+
rubygems_version: 3.2.33
|
204
217
|
signing_key:
|
205
218
|
specification_version: 4
|
206
219
|
summary: Provide an easy way to check the consistency of the database constraints
|