database_consistency 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) 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/processors/base_processor.rb +6 -1
  16. data/lib/database_consistency/processors/indexes_processor.rb +1 -1
  17. data/lib/database_consistency/report.rb +23 -0
  18. data/lib/database_consistency/version.rb +1 -1
  19. data/lib/database_consistency/writers/autofix/base.rb +15 -0
  20. data/lib/database_consistency/writers/autofix/helpers/migration.rb +28 -0
  21. data/lib/database_consistency/writers/autofix/migration_base.rb +21 -0
  22. data/lib/database_consistency/writers/autofix/missing_foreign_key.rb +19 -0
  23. data/lib/database_consistency/writers/autofix/null_constraint_missing.rb +19 -0
  24. data/lib/database_consistency/writers/autofix/templates/missing_foreign_key.tt +5 -0
  25. data/lib/database_consistency/writers/autofix/templates/null_constraint_missing.tt +5 -0
  26. data/lib/database_consistency/writers/autofix_writer.rb +40 -0
  27. data/lib/database_consistency/writers/base_writer.rb +0 -4
  28. data/lib/database_consistency/writers/helpers/pipes.rb +15 -0
  29. data/lib/database_consistency/writers/simple_writer.rb +30 -2
  30. data/lib/database_consistency/writers/todo_writer.rb +6 -6
  31. data/lib/database_consistency.rb +16 -2
  32. metadata +17 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5774c6b4064a081e7a285013456b39fef68f39ec7dd536063c236d54abec3e84
4
- data.tar.gz: 18ed6073af882e007ec6ea70a9a55fad8daa8fef755049a3070d9f68ce1377dd
3
+ metadata.gz: 977b169b47dd5335a327746945ddd512717db1305744f44884d81bb0c7e82645
4
+ data.tar.gz: 59eb6c6ddefd09e77ba3f51dfbc2d4474eb92f62ca7b92437a4d9d70778d0d85
5
5
  SHA512:
6
- metadata.gz: 575b90af564642b397b5a93d2c5a94a3b3b2fb8f6de12e40225361ebb96128df61a8d3e19fb4982986985c6bd1ae110539cc93f00688bab3b44205f7aa5f3716
7
- data.tar.gz: 051360a87facb2062451b17c59ff596156b0211dfbf73d6e3345c1b45d470f45177c280d6dd6fb59602faa719a959534252dc4f9615378724d2a321d39c64394
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
 
@@ -25,8 +25,13 @@ module DatabaseConsistency
25
25
  end
26
26
 
27
27
  # @return [Array<Hash>]
28
- def reports
28
+ def reports(catch_errors: true)
29
29
  @reports ||= check
30
+ rescue StandardError => e
31
+ raise e unless catch_errors
32
+
33
+ RescueError.call(e)
34
+ []
30
35
  end
31
36
 
32
37
  # @return [Array<Class>]
@@ -16,7 +16,7 @@ module DatabaseConsistency
16
16
  Helper.parent_models.flat_map do |model|
17
17
  next unless configuration.enabled?(model.name.to_s)
18
18
 
19
- indexes = ActiveRecord::Base.connection.indexes(model.table_name)
19
+ indexes = model.connection.indexes(model.table_name)
20
20
 
21
21
  indexes.flat_map do |index|
22
22
  enabled_checkers.map do |checker_class|
@@ -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.1'
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.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-09-08 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.3.21
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