db_validator 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5b1e93e6a494c74779345e004eb7f6050fa5fd4e15cb27846d48fa8390a78ef
4
- data.tar.gz: 9ab575840ad1cb722db62b40162b527e4793305ed4255b38eb1166ba5e4786fe
3
+ metadata.gz: 60cba1d36771e30b0bafb5ac354f3d741b3bde0e5f37b267bcff4ebb9dd11bad
4
+ data.tar.gz: 1baa177ce745a107d5c064844b74cd8731b5cddd8624f2944c464812c9e136b0
5
5
  SHA512:
6
- metadata.gz: 20cc83d66461dff7faed7ec1eee28b4c5459152d723403c15b23718554e5ae208e965acde7b3ee4bb2ba030e19bdd86c1e4e63bd0072e7e2bfbfdd25048647e9
7
- data.tar.gz: b8e9a4175498cf0f96eec2ed624a9c1be35c6092206bfd7117ecad929f59760236d361cbf524cce37fb603feaafa4ed981f5cbf0308cb0b79a2b2adb2c39a5d5
6
+ metadata.gz: d3e414c08cf3496438f5a61941c579b072dd9a41330133df64bfe48c3e8d205cf3de933f52368622d3cd7cbcea8cea5844982f7fb95bf1eafca59bcb1b6f781a
7
+ data.tar.gz: 9a2d9de68775b836aec350ded2b88184c534e3dac4649d6b531e5f5575363f30ee60b1a10a9439cfbd2beb083ea2036a095a17d37c5593d9fe4e730bdb7349fe
@@ -2,7 +2,7 @@
2
2
 
3
3
  DbValidator.configure do |config|
4
4
  config.only_models = %w[Documents Users]
5
- config.batch_size = 500
5
+ config.batch_size = 100
6
6
  config.limit = 500
7
7
  config.model_limits = {
8
8
  "documents" => 500,
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require "tty-box"
5
+ require "tty-spinner"
6
+
7
+ module DbValidator
8
+ class CLI
9
+ def initialize
10
+ @prompt = TTY::Prompt.new
11
+ end
12
+
13
+ def select_models(available_models)
14
+ system "clear"
15
+ display_header
16
+
17
+ if available_models.empty?
18
+ @prompt.error("No models found in the application.")
19
+ exit
20
+ end
21
+
22
+ choices = available_models.map { |model| { name: model, value: model } }
23
+ choices.unshift({ name: "All Models", value: "all" })
24
+
25
+ @prompt.say("\n")
26
+ selected = @prompt.multi_select(
27
+ "Select models to validate:",
28
+ choices,
29
+ per_page: 10,
30
+ echo: false,
31
+ show_help: :always,
32
+ filter: true,
33
+ cycle: true
34
+ )
35
+
36
+ if selected.include?("all")
37
+ available_models
38
+ else
39
+ selected
40
+ end
41
+ end
42
+
43
+ def configure_options
44
+ options = {}
45
+
46
+ @prompt.say("\n")
47
+ if @prompt.yes?("Would you like to configure additional options?", default: false)
48
+ limit_input = @prompt.ask("Enter record limit (leave blank for no limit):") do |q|
49
+ q.validate(/^\d*$/, "Please enter a valid number")
50
+ q.convert(:int, nil)
51
+ end
52
+ options[:limit] = limit_input if limit_input.present?
53
+
54
+ batch_size = @prompt.ask("Enter batch size:", default: 1000, convert: :int) do |q|
55
+ q.validate(/^\d+$/, "Please enter a positive number")
56
+ q.messages[:valid?] = "Please enter a positive number"
57
+ end
58
+ options[:batch_size] = batch_size if batch_size.present?
59
+
60
+ options[:format] = @prompt.select("Select report format:", %w[text json], default: "text")
61
+ end
62
+
63
+ options
64
+ end
65
+
66
+ def display_progress(message)
67
+ spinner = TTY::Spinner.new("[:spinner] #{message}", format: :dots)
68
+ spinner.auto_spin
69
+ yield if block_given?
70
+ spinner.success
71
+ end
72
+
73
+ private
74
+
75
+ def display_header
76
+ title = TTY::Box.frame(
77
+ "DB Validator",
78
+ "Interactive Model Validation",
79
+ padding: 1,
80
+ align: :center,
81
+ border: :thick,
82
+ style: {
83
+ border: {
84
+ fg: :cyan
85
+ }
86
+ }
87
+ )
88
+ puts title
89
+ end
90
+ end
91
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module DbValidator
4
4
  class Configuration
5
- attr_accessor :only_models, :ignored_models, :ignored_attributes, :batch_size, :report_format, :auto_fix, :limit
5
+ attr_accessor :only_models, :ignored_models, :ignored_attributes, :batch_size, :report_format, :limit
6
6
 
7
7
  def initialize
8
8
  @only_models = []
@@ -10,7 +10,6 @@ module DbValidator
10
10
  @ignored_attributes = {}
11
11
  @batch_size = 1000
12
12
  @report_format = :text
13
- @auto_fix = false
14
13
  @limit = nil
15
14
  end
16
15
 
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "tty-box"
4
+ require "tty-spinner"
5
+
3
6
  module DbValidator
4
7
  class Reporter
5
8
  def initialize
@@ -10,11 +13,11 @@ module DbValidator
10
13
  enhanced_errors = record.errors.map do |error|
11
14
  field_value = record.send(error.attribute)
12
15
  message = error.message
13
-
16
+
14
17
  if error.options[:in].present?
15
18
  "#{error.attribute} #{message} (allowed values: #{error.options[:in].join(', ')}, actual value: #{field_value.inspect})"
16
19
  else
17
- "#{error.attribute} #{message} (actual value: #{field_value.inspect})"
20
+ "#{error.attribute} #{message} (actual value: #{format_value(field_value)})"
18
21
  end
19
22
  end
20
23
 
@@ -36,26 +39,68 @@ module DbValidator
36
39
 
37
40
  private
38
41
 
42
+ def format_value(value)
43
+ case value
44
+ when true, false
45
+ when Symbol
46
+ value.to_s
47
+ when String
48
+ "\"#{value}\""
49
+ when nil
50
+ "nil"
51
+ else
52
+ value
53
+ end
54
+ end
55
+
39
56
  def generate_text_report
40
57
  report = StringIO.new
41
- report.puts "DbValidator Report"
42
- report.puts "=================="
58
+
59
+ title_box = TTY::Box.frame(
60
+ width: 50,
61
+ align: :center,
62
+ padding: [1, 2],
63
+ title: { top_left: "DbValidator" },
64
+ style: {
65
+ fg: :cyan,
66
+ border: {
67
+ fg: :cyan
68
+ }
69
+ }
70
+ ) do
71
+ "Database Validation Report"
72
+ end
73
+
74
+ report.puts title_box
43
75
  report.puts
44
76
 
45
77
  if @invalid_records.empty?
46
- report.puts "No invalid records found."
78
+ report.puts "No invalid records found.".colorize(:green)
47
79
  else
48
- report.puts "Found invalid records:"
80
+ report.puts "Found #{@invalid_records.count} invalid records across #{@invalid_records.group_by { |r| r[:model] }.keys.count} models".colorize(:yellow)
49
81
  report.puts
50
82
 
51
83
  @invalid_records.group_by { |r| r[:model] }.each do |model, records|
52
- report.puts "#{model}: #{records.count} invalid records"
84
+ report.puts "#{model}: #{records.count} invalid records".colorize(:red)
85
+ report.puts
86
+
53
87
  records.each do |record|
54
- report.puts " ID: #{record[:id]}"
88
+ record_obj = record[:model].constantize.find_by(id: record[:id])
89
+ next unless record_obj
90
+
91
+ info = ["ID: #{record[:id]}"]
92
+ info << "Created: #{record_obj.created_at.strftime('%Y-%m-%d %H:%M:%S')}" if record_obj.respond_to?(:created_at)
93
+ info << "Updated: #{record_obj.updated_at.strftime('%Y-%m-%d %H:%M:%S')}" if record_obj.respond_to?(:updated_at)
94
+ info << "Name: #{record_obj.name}" if record_obj.respond_to?(:name)
95
+ info << "Title: #{record_obj.title}" if record_obj.respond_to?(:title)
96
+
97
+ report.puts " #{info.join(' | ')}".colorize(:white)
55
98
  record[:errors].each do |error|
56
- report.puts " - #{error}"
99
+ report.puts " ⚠️ #{error}".colorize(:white)
57
100
  end
101
+ report.puts
58
102
  end
103
+
59
104
  report.puts
60
105
  end
61
106
  end
@@ -5,20 +5,26 @@ require "colorize"
5
5
 
6
6
  module DbValidator
7
7
  class Validator
8
+ attr_reader :reporter
9
+
8
10
  def initialize(options = {})
9
11
  @options = options
10
12
  @reporter = Reporter.new
11
- @fixer = Fixer.new if DbValidator.configuration.auto_fix
13
+ configure_from_options(options)
12
14
  end
13
15
 
14
16
  def validate_all
15
17
  Rails.application.eager_load! if defined?(Rails)
16
18
 
17
19
  models = find_all_models
18
-
19
20
  models_to_validate = models.select { |model| should_validate_model?(model) }
20
21
  total_models = models_to_validate.size
21
22
 
23
+ if models_to_validate.empty?
24
+ Rails.logger.debug "No models selected for validation.".colorize(:yellow)
25
+ return @reporter.generate_report
26
+ end
27
+
22
28
  models_to_validate.each_with_index do |model, index|
23
29
  Rails.logger.debug "Validating model #{index + 1}/#{total_models}: #{model.name}".colorize(:cyan)
24
30
  validate_model(model)
@@ -29,8 +35,19 @@ module DbValidator
29
35
 
30
36
  private
31
37
 
38
+ def configure_from_options(options)
39
+ return unless options.is_a?(Hash)
40
+
41
+ if options[:only_models]
42
+ DbValidator.configuration.only_models = Array(options[:only_models])
43
+ end
44
+
45
+ DbValidator.configuration.limit = options[:limit] if options[:limit]
46
+ DbValidator.configuration.batch_size = options[:batch_size] if options[:batch_size]
47
+ DbValidator.configuration.report_format = options[:report_format] if options[:report_format]
48
+ end
49
+
32
50
  def find_all_models
33
- # Include all classes inheriting from ActiveRecord::Base
34
51
  ObjectSpace.each_object(Class).select do |klass|
35
52
  klass < ActiveRecord::Base
36
53
  end
@@ -42,45 +59,51 @@ module DbValidator
42
59
 
43
60
  config = DbValidator.configuration
44
61
  model_name = model.name.downcase
45
- return config.only_models.include?(model_name) if config.only_models.any?
62
+
63
+ if config.only_models.any?
64
+ return config.only_models.map(&:downcase).include?(model_name)
65
+ end
46
66
 
47
- config.ignored_models.exclude?(model_name)
67
+ !config.ignored_models.map(&:downcase).include?(model_name)
48
68
  end
49
69
 
50
70
  def validate_model(model)
51
- limit = DbValidator.configuration.limit
52
- total_records = limit || model.count
71
+ config = DbValidator.configuration
72
+ batch_size = config.batch_size || 1000
73
+ limit = config.limit
53
74
 
54
- if total_records.zero?
55
- Rails.logger.debug { "No records to validate for model #{model.name}." }
56
- return
57
- end
75
+ scope = model.all
76
+ scope = scope.limit(limit) if limit
58
77
 
59
- processed_records = 0
78
+ total_count = scope.count
79
+ return if total_count.zero?
60
80
 
61
- query = model.all
62
- query = query.limit(limit) if limit
81
+ progress_bar = create_progress_bar(model.name, total_count)
63
82
 
64
- query.find_in_batches(batch_size: DbValidator.configuration.batch_size) do |batch|
65
- batch.each do |record|
66
- validate_record(record)
67
- processed_records += 1
68
- if (processed_records % 100).zero? || processed_records == total_records
69
- Rails.logger.debug "Validated #{processed_records}/#{total_records} records for model #{model.name}".colorize(:green)
83
+ begin
84
+ scope.find_in_batches(batch_size: batch_size) do |batch|
85
+ batch.each do |record|
86
+ validate_record(record)
87
+ progress_bar.increment
70
88
  end
71
89
  end
72
90
  rescue StandardError => e
73
91
  Rails.logger.debug "Error validating #{model.name}: #{e.message}".colorize(:red)
74
92
  end
75
- rescue ActiveRecord::StatementInvalid => e
76
- Rails.logger.debug { "Skipping validation for #{model.name}: #{e.message}" }
93
+ end
94
+
95
+ def create_progress_bar(model_name, total)
96
+ ProgressBar.create(
97
+ title: "Validating #{model_name}",
98
+ total: total,
99
+ format: "%t: |%B| %p%% %e",
100
+ output: $stderr
101
+ )
77
102
  end
78
103
 
79
104
  def validate_record(record)
80
105
  return if record.valid?
81
-
82
106
  @reporter.add_invalid_record(record)
83
- @fixer&.attempt_fix(record)
84
107
  end
85
108
  end
86
109
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DbValidator
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/db_validator.rb CHANGED
@@ -5,7 +5,7 @@ require "db_validator/version"
5
5
  require "db_validator/configuration"
6
6
  require "db_validator/validator"
7
7
  require "db_validator/reporter"
8
- require "db_validator/fixer"
8
+ require "db_validator/cli"
9
9
 
10
10
  module DbValidator
11
11
  class Error < StandardError; end
@@ -14,11 +14,8 @@ DbValidator.configure do |config|
14
14
  # }
15
15
 
16
16
  # Set the batch size for processing records (default: 1000)
17
- # config.batch_size = 1000
17
+ # config.batch_size = 100
18
18
 
19
19
  # Set the report format (:text or :json)
20
20
  # config.report_format = :text
21
-
22
- # Enable automatic fixing of simple validation errors
23
- # config.auto_fix = false
24
21
  end
@@ -3,17 +3,39 @@
3
3
  namespace :db_validator do
4
4
  desc "Validate all records in the database"
5
5
  task validate: :environment do
6
- if ENV["models"]
7
- models = ENV["models"].split(",").map(&:strip).map(&:downcase)
6
+ cli = DbValidator::CLI.new
7
+
8
+ has_any_args = ENV["models"] || ENV["limit"] || ENV["batch_size"] || ENV["format"]
9
+
10
+ if has_any_args
11
+ models = ENV["models"].split(",").map(&:strip).map(&:downcase).map(&:singularize)
12
+
8
13
  DbValidator.configuration.only_models = models
9
- end
14
+ DbValidator.configuration.limit = ENV["limit"].to_i if ENV["limit"].present?
15
+ DbValidator.configuration.batch_size = ENV["batch_size"].to_i if ENV["batch_size"].present?
16
+ DbValidator.configuration.report_format = ENV["format"].to_sym if ENV["format"].present?
17
+ else
18
+ cli.display_progress("Loading models") do
19
+ Rails.application.eager_load!
20
+ end
21
+
22
+ available_models = ActiveRecord::Base.descendants
23
+ .reject(&:abstract_class?)
24
+ .select(&:table_exists?)
25
+ .map { |m| m.name }
26
+ .sort
10
27
 
11
- DbValidator.configuration.limit = ENV["limit"].to_i if ENV["limit"]
28
+ selected_models = cli.select_models(available_models)
29
+ options = cli.configure_options
12
30
 
13
- DbValidator.configuration.report_format = ENV["format"].to_sym if ENV["format"]
31
+ DbValidator.configuration.only_models = selected_models
32
+ DbValidator.configuration.limit = options[:limit] if options[:limit].present?
33
+ DbValidator.configuration.batch_size = options[:batch_size] if options[:batch_size].present?
34
+ DbValidator.configuration.report_format = options[:format].to_sym if options[:format].present?
35
+ end
14
36
 
15
37
  validator = DbValidator::Validator.new
16
38
  report = validator.validate_all
17
- puts report
39
+ puts "\n#{report}"
18
40
  end
19
41
  end
data/readme.md CHANGED
@@ -1,6 +1,9 @@
1
+ [![Gem Version](https://badge.fury.io/rb/db_validator.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/db_validator)
2
+ [![RSpec Tests](https://github.com/krzysztoff1/db-validator/actions/workflows/rspec.yml/badge.svg)](https://github.com/krzysztoff1/db-validator/actions/workflows/rspec.yml)
3
+
1
4
  # DbValidator
2
5
 
3
- DbValidator helps identify invalid records in your Rails application that don't meet model validation requirements.
6
+ DbValidator helps identify invalid records in your Rails application that don't meet model validation requirements. It finds records that became invalid after validation rule changes, and validates imported or manually edited data. You can use it to audit records before deploying new validations and catch any data that bypassed validation checks.
4
7
 
5
8
  ## Installation
6
9
 
@@ -16,24 +19,21 @@ Then execute:
16
19
  $ bundle install
17
20
  ```
18
21
 
19
- Or install it yourself:
20
-
21
- ```bash
22
- $ gem install db_validator
23
- ```
24
-
25
22
  ## Usage
26
23
 
27
24
  ### Rake Task
28
25
 
29
26
  The simplest way to run validation is using the provided rake task:
30
27
 
31
- #### Validate all models
28
+ #### Validate models in interactive mode
29
+ <img width="798" alt="Screenshot 2024-11-07 at 21 50 57" src="https://github.com/user-attachments/assets/33fbdb8b-b8ec-4284-9313-c1eeaf2eab2d">
32
30
 
33
31
  ```bash
34
32
  $ rake db_validator:validate
35
33
  ```
36
34
 
35
+ This will start an interactive mode where you can select which models to validate and adjust other options.
36
+
37
37
  #### Validate specific models
38
38
 
39
39
  ```bash
@@ -52,6 +52,14 @@ $ rake db_validator:validate limit=1000
52
52
  $ rake db_validator:validate format=json
53
53
  ```
54
54
 
55
+ ### Interactive Mode
56
+
57
+ Running the validation task without specifying models will start an interactive mode:
58
+
59
+ ```bash
60
+ $ rake db_validator:validate
61
+ ```
62
+
55
63
  ### Ruby Code
56
64
 
57
65
  You can also run validation from your Ruby code:
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: db_validator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Krzysztof Duda
@@ -94,8 +94,53 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '1.11'
97
- description: A comprehensive solution for validating existing database records in
98
- Rails applications
97
+ - !ruby/object:Gem::Dependency
98
+ name: tty-prompt
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.23.1
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.23.1
111
+ - !ruby/object:Gem::Dependency
112
+ name: tty-box
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.7.0
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.7.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: tty-spinner
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.9.3
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.9.3
139
+ description: DbValidator helps identify invalid records in your Rails application
140
+ that don't meet model validation requirements. It finds records that became invalid
141
+ after validation rule changes, and validates imported or manually edited data. You
142
+ can use it to audit records before deploying new validations and catch any data
143
+ that bypassed validation checks.
99
144
  email:
100
145
  - duda_krzysztof@outlook.com
101
146
  executables: []
@@ -104,8 +149,8 @@ extra_rdoc_files: []
104
149
  files:
105
150
  - config/initializers/db_validator.rb
106
151
  - lib/db_validator.rb
152
+ - lib/db_validator/cli.rb
107
153
  - lib/db_validator/configuration.rb
108
- - lib/db_validator/fixer.rb
109
154
  - lib/db_validator/railtie.rb
110
155
  - lib/db_validator/reporter.rb
111
156
  - lib/db_validator/validator.rb
@@ -114,7 +159,7 @@ files:
114
159
  - lib/generators/db_validator/templates/initializer.rb
115
160
  - lib/tasks/db_validator_tasks.rake
116
161
  - readme.md
117
- homepage: https://github.com/yourusername/db_validator
162
+ homepage: https://github.com/krzysztoff1/db-validator
118
163
  licenses:
119
164
  - MIT
120
165
  metadata:
@@ -137,5 +182,6 @@ requirements: []
137
182
  rubygems_version: 3.5.16
138
183
  signing_key:
139
184
  specification_version: 4
140
- summary: Database-wide validation for Rails applications
185
+ summary: DbValidator helps identify invalid records in your Rails application that
186
+ don't meet model validation requirements
141
187
  test_files: []
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DbValidator
4
- class Fixer
5
- def initialize
6
- @fixed_records = 0
7
- @failed_fixes = 0
8
- end
9
-
10
- def fix_record(record)
11
- return false unless record.invalid?
12
-
13
- success = attempt_fix(record)
14
- if success
15
- @fixed_records += 1
16
- else
17
- @failed_fixes += 1
18
- end
19
- success
20
- end
21
-
22
- def statistics
23
- {
24
- fixed_records: @fixed_records,
25
- failed_fixes: @failed_fixes
26
- }
27
- end
28
-
29
- private
30
-
31
- def attempt_fix(record)
32
- record.save
33
- end
34
- end
35
- end