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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/bin/database_consistency +5 -1
  3. data/lib/database_consistency/checkers/association_checkers/foreign_key_checker.rb +31 -3
  4. data/lib/database_consistency/checkers/association_checkers/foreign_key_type_checker.rb +39 -11
  5. data/lib/database_consistency/checkers/association_checkers/missing_index_checker.rb +2 -6
  6. data/lib/database_consistency/checkers/base_checker.rb +14 -7
  7. data/lib/database_consistency/checkers/column_checkers/length_constraint_checker.rb +3 -8
  8. data/lib/database_consistency/checkers/column_checkers/null_constraint_checker.rb +21 -7
  9. data/lib/database_consistency/checkers/column_checkers/primary_key_type_checker.rb +1 -4
  10. data/lib/database_consistency/checkers/index_checkers/redundant_index_checker.rb +19 -7
  11. data/lib/database_consistency/checkers/index_checkers/redundant_unique_index_checker.rb +19 -7
  12. data/lib/database_consistency/checkers/index_checkers/unique_index_checker.rb +1 -4
  13. data/lib/database_consistency/checkers/validator_checkers/missing_unique_index_checker.rb +1 -3
  14. data/lib/database_consistency/checkers/validators_fraction_checkers/column_presence_checker.rb +33 -9
  15. data/lib/database_consistency/report.rb +23 -0
  16. data/lib/database_consistency/version.rb +1 -1
  17. data/lib/database_consistency/writers/autofix/base.rb +15 -0
  18. data/lib/database_consistency/writers/autofix/helpers/migration.rb +28 -0
  19. data/lib/database_consistency/writers/autofix/migration_base.rb +21 -0
  20. data/lib/database_consistency/writers/autofix/missing_foreign_key.rb +19 -0
  21. data/lib/database_consistency/writers/autofix/null_constraint_missing.rb +19 -0
  22. data/lib/database_consistency/writers/autofix/templates/missing_foreign_key.tt +5 -0
  23. data/lib/database_consistency/writers/autofix/templates/null_constraint_missing.tt +5 -0
  24. data/lib/database_consistency/writers/autofix_writer.rb +40 -0
  25. data/lib/database_consistency/writers/base_writer.rb +0 -4
  26. data/lib/database_consistency/writers/helpers/pipes.rb +15 -0
  27. data/lib/database_consistency/writers/simple_writer.rb +30 -2
  28. data/lib/database_consistency/writers/todo_writer.rb +6 -6
  29. data/lib/database_consistency.rb +16 -2
  30. metadata +17 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f2d13b99e730f1045b98f63239a77820a81908d2ec05232461bf12275489d5d
4
- data.tar.gz: 7f92339d4b0241a15f422f64fdd80eab8db3f64c7cd8c405f91d787851fa3400
3
+ metadata.gz: 977b169b47dd5335a327746945ddd512717db1305744f44884d81bb0c7e82645
4
+ data.tar.gz: 59eb6c6ddefd09e77ba3f51dfbc2d4474eb92f62ca7b92437a4d9d70778d0d85
5
5
  SHA512:
6
- metadata.gz: 4970958fca4670dda454bdaf699e96c013fc443d9bfeea4e11638ed2ad38f567f1f07ddec0785310bfac9e7afc58dd014df94872519c6ed4b7bfb3a405cc5a75
7
- data.tar.gz: b3656fa0f395f2e87914b4d217eeea445b9fca195d5db0a4ca9ac596bdf5a81a5662c18dc714a1191a9f61fe714814706428cff6c0816e295ffa15650ffd7917
6
+ metadata.gz: 8b2b83f4ca288c7173f7abacc04c80f07b4d78c45f0fc949a801d1eb08b173c217ee524462eade7f443d9582243e593faa2730dd33e77e6d985f86c8ba86f6b4
7
+ data.tar.gz: e461a6da7caab598150602dac3f02075f4da00b9e68b481419e5717bb33fd3390d5e3f5b90c9ec315c2445af06812e435dafb3db81a7c2ced6cda4b957b035d0
@@ -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 |f|
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
- MISSING_FOREIGN_KEY = 'should have foreign key in the database'
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
- report_template(:fail, MISSING_FOREIGN_KEY)
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
- INCONSISTENT_TYPE = "foreign key (%a_f) with type (%a_t) doesn't cover primary key (%b_f) with type (%b_t)"
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, render_text)
53
+ report_template(:fail, error_slug: :inconsistent_types)
35
54
  end
36
55
  rescue Errors::MissingField => e
37
- report_template(:fail, e.message)
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
- # @return [String]
41
- def render_text
42
- INCONSISTENT_TYPE
43
- .gsub('%a_t', type(associated_column))
44
- .gsub('%a_f', associated_key)
45
- .gsub('%b_t', type(primary_column))
46
- .gsub('%b_f', primary_key)
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, MISSING_UNIQUE_INDEX)
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, MISSING_INDEX)
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 [OpenStruct]
70
- def report_template(status, message = nil)
71
- OpenStruct.new(
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
- message: message
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, VALIDATOR_MISSING) unless validator
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, GREATER_LIMIT)
41
+ report_template(:warning, error_slug: :length_validator_greater_limit)
47
42
  else
48
- report_template(:fail, LOWER_LIMIT)
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
- # Message templates
8
- VALIDATOR_MISSING = 'column is required in the database but do not have presence validator'
9
- ASSOCIATION_VALIDATOR_MISSING = 'column is required in the database but do '\
10
- 'not have presence validator for association (%a_n)'
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
- report_template(:fail, ASSOCIATION_VALIDATOR_MISSING.gsub('%a_n', belongs_to_association.name.to_s))
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, VALIDATOR_MISSING)
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, VALIDATOR_MISSING)
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
- # Message templates
8
- REDUNDANT_INDEX = 'index is redundant as (%index) covers it'
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
- report_template(:fail, render_message)
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
- # Message templates
8
- REDUNDANT_UNIQUE_INDEX = 'index uniqueness is redundant as (%index) covers it'
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
- report_template(:fail, render_message)
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, VALIDATOR_MISSING)
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, MISSING_INDEX)
28
+ report_template(:fail, error_slug: :missing_unique_index)
31
29
  end
32
30
  end
33
31
 
@@ -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
- # Message templates
9
- CONSTRAINT_MISSING = 'column should be required in the database'
10
- ASSOCIATION_FOREIGN_KEY_CONSTRAINT_MISSING = 'association foreign key column should be required in the database'
11
- POSSIBLE_NULL = 'column is required but there is possible null value insert'
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, POSSIBLE_NULL) unless can_be_null
60
+ return report_template(:fail, error_slug: :possible_null) unless can_be_null
51
61
 
52
62
  if regular_column
53
- report_template(:fail, CONSTRAINT_MISSING)
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
- report_template(:fail, ASSOCIATION_FOREIGN_KEY_CONSTRAINT_MISSING)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DatabaseConsistency
4
- VERSION = '1.2.2'
4
+ VERSION = '1.3.0'
5
5
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ module Writers
5
+ module Autofix
6
+ class Base # :nodoc:
7
+ attr_reader :report
8
+
9
+ def initialize(report)
10
+ @report = report
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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,5 @@
1
+ class %<migration_name>s < ActiveRecord::Migration[%<migration_version>s]
2
+ def change
3
+ add_foreign_key :%<foreign_table>s, :%<primary_table>s, column: :%<foreign_key>s, primary_key: :%<primary_key>s
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class %<migration_name>s < ActiveRecord::Migration[%<migration_version>s]
2
+ def change
3
+ change_column_null(:%<table_name>s, :%<column_name>s, false)
4
+ end
5
+ 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
@@ -11,10 +11,6 @@ module DatabaseConsistency
11
11
  @config = config
12
12
  end
13
13
 
14
- def write?(status)
15
- status == :fail || config.debug?
16
- end
17
-
18
14
  def self.write(results, config: Configuration.new)
19
15
  new(results, config: config).write
20
16
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ module Writers
5
+ module Helpers
6
+ module Pipes # :nodoc:
7
+ module_function
8
+
9
+ def unique(reports)
10
+ reports.uniq(&:attributes)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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.message}"
52
+ "#{result.checker_name} #{status_text(result)} #{key_text(result)} #{message_text(result)}"
31
53
  end
32
54
 
33
- private
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
- hash = results.each_with_object({}) do |result, hash|
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, hash.to_yaml)
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.exists?(name)
34
+ return name unless File.exist?(name)
35
35
  end
36
36
  end
37
37
 
@@ -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[:todo]
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.2.2
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-09-10 00:00:00.000000000 Z
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.0.3.1
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