db_validator 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d5b1e93e6a494c74779345e004eb7f6050fa5fd4e15cb27846d48fa8390a78ef
4
+ data.tar.gz: 9ab575840ad1cb722db62b40162b527e4793305ed4255b38eb1166ba5e4786fe
5
+ SHA512:
6
+ metadata.gz: 20cc83d66461dff7faed7ec1eee28b4c5459152d723403c15b23718554e5ae208e965acde7b3ee4bb2ba030e19bdd86c1e4e63bd0072e7e2bfbfdd25048647e9
7
+ data.tar.gz: b8e9a4175498cf0f96eec2ed624a9c1be35c6092206bfd7117ecad929f59760236d361cbf524cce37fb603feaafa4ed981f5cbf0308cb0b79a2b2adb2c39a5d5
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ DbValidator.configure do |config|
4
+ config.only_models = %w[Documents Users]
5
+ config.batch_size = 500
6
+ config.limit = 500
7
+ config.model_limits = {
8
+ "documents" => 500,
9
+ "users" => 1000
10
+ }
11
+ config.report_format = :json
12
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbValidator
4
+ class Configuration
5
+ attr_accessor :only_models, :ignored_models, :ignored_attributes, :batch_size, :report_format, :auto_fix, :limit
6
+
7
+ def initialize
8
+ @only_models = []
9
+ @ignored_models = []
10
+ @ignored_attributes = {}
11
+ @batch_size = 1000
12
+ @report_format = :text
13
+ @auto_fix = false
14
+ @limit = nil
15
+ end
16
+
17
+ def only_models=(models)
18
+ @only_models = models.map(&:downcase)
19
+ end
20
+
21
+ def ignored_models
22
+ @ignored_models ||= []
23
+ end
24
+
25
+ def ignored_models=(models)
26
+ @ignored_models = models.map(&:downcase)
27
+ end
28
+ end
29
+
30
+ class << self
31
+ def configuration
32
+ @configuration ||= Configuration.new
33
+ end
34
+
35
+ def configure
36
+ yield(configuration)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbValidator
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :db_validator
6
+
7
+ rake_tasks do
8
+ load "tasks/db_validator_tasks.rake"
9
+ end
10
+
11
+ generators do
12
+ require "generators/db_validator/install_generator"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbValidator
4
+ class Reporter
5
+ def initialize
6
+ @invalid_records = []
7
+ end
8
+
9
+ def add_invalid_record(record)
10
+ enhanced_errors = record.errors.map do |error|
11
+ field_value = record.send(error.attribute)
12
+ message = error.message
13
+
14
+ if error.options[:in].present?
15
+ "#{error.attribute} #{message} (allowed values: #{error.options[:in].join(', ')}, actual value: #{field_value.inspect})"
16
+ else
17
+ "#{error.attribute} #{message} (actual value: #{field_value.inspect})"
18
+ end
19
+ end
20
+
21
+ @invalid_records << {
22
+ model: record.class.name,
23
+ id: record.id,
24
+ errors: enhanced_errors
25
+ }
26
+ end
27
+
28
+ def generate_report
29
+ case DbValidator.configuration.report_format
30
+ when :json
31
+ generate_json_report
32
+ else
33
+ generate_text_report
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def generate_text_report
40
+ report = StringIO.new
41
+ report.puts "DbValidator Report"
42
+ report.puts "=================="
43
+ report.puts
44
+
45
+ if @invalid_records.empty?
46
+ report.puts "No invalid records found."
47
+ else
48
+ report.puts "Found invalid records:"
49
+ report.puts
50
+
51
+ @invalid_records.group_by { |r| r[:model] }.each do |model, records|
52
+ report.puts "#{model}: #{records.count} invalid records"
53
+ records.each do |record|
54
+ report.puts " ID: #{record[:id]}"
55
+ record[:errors].each do |error|
56
+ report.puts " - #{error}"
57
+ end
58
+ end
59
+ report.puts
60
+ end
61
+ end
62
+
63
+ report.string
64
+ end
65
+
66
+ def generate_json_report
67
+ @invalid_records.to_json
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby-progressbar"
4
+ require "colorize"
5
+
6
+ module DbValidator
7
+ class Validator
8
+ def initialize(options = {})
9
+ @options = options
10
+ @reporter = Reporter.new
11
+ @fixer = Fixer.new if DbValidator.configuration.auto_fix
12
+ end
13
+
14
+ def validate_all
15
+ Rails.application.eager_load! if defined?(Rails)
16
+
17
+ models = find_all_models
18
+
19
+ models_to_validate = models.select { |model| should_validate_model?(model) }
20
+ total_models = models_to_validate.size
21
+
22
+ models_to_validate.each_with_index do |model, index|
23
+ Rails.logger.debug "Validating model #{index + 1}/#{total_models}: #{model.name}".colorize(:cyan)
24
+ validate_model(model)
25
+ end
26
+
27
+ @reporter.generate_report
28
+ end
29
+
30
+ private
31
+
32
+ def find_all_models
33
+ # Include all classes inheriting from ActiveRecord::Base
34
+ ObjectSpace.each_object(Class).select do |klass|
35
+ klass < ActiveRecord::Base
36
+ end
37
+ end
38
+
39
+ def should_validate_model?(model)
40
+ return false if model.abstract_class?
41
+ return false unless model.table_exists?
42
+
43
+ config = DbValidator.configuration
44
+ model_name = model.name.downcase
45
+ return config.only_models.include?(model_name) if config.only_models.any?
46
+
47
+ config.ignored_models.exclude?(model_name)
48
+ end
49
+
50
+ def validate_model(model)
51
+ limit = DbValidator.configuration.limit
52
+ total_records = limit || model.count
53
+
54
+ if total_records.zero?
55
+ Rails.logger.debug { "No records to validate for model #{model.name}." }
56
+ return
57
+ end
58
+
59
+ processed_records = 0
60
+
61
+ query = model.all
62
+ query = query.limit(limit) if limit
63
+
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)
70
+ end
71
+ end
72
+ rescue StandardError => e
73
+ Rails.logger.debug "Error validating #{model.name}: #{e.message}".colorize(:red)
74
+ end
75
+ rescue ActiveRecord::StatementInvalid => e
76
+ Rails.logger.debug { "Skipping validation for #{model.name}: #{e.message}" }
77
+ end
78
+
79
+ def validate_record(record)
80
+ return if record.valid?
81
+
82
+ @reporter.add_invalid_record(record)
83
+ @fixer&.attempt_fix(record)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbValidator
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "db_validator/version"
5
+ require "db_validator/configuration"
6
+ require "db_validator/validator"
7
+ require "db_validator/reporter"
8
+ require "db_validator/fixer"
9
+
10
+ module DbValidator
11
+ class Error < StandardError; end
12
+
13
+ class << self
14
+ def validate(options = {})
15
+ validator = Validator.new(options)
16
+ validator.validate_all
17
+ end
18
+ end
19
+ end
20
+
21
+ require "db_validator/railtie" if defined?(Rails)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/base"
5
+
6
+ module DbValidator
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ def create_initializer
12
+ template "initializer.rb", "config/initializers/db_validator.rb"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ DbValidator.configure do |config|
4
+ # Specify specific models to validate
5
+ config.only_models = %w[User Post]
6
+
7
+ # Ignore specific models from validation
8
+ # config.ignored_models = ["AdminUser"]
9
+
10
+ # Ignore specific attributes for specific models
11
+ # config.ignored_attributes = {
12
+ # "User" => ["encrypted_password", "reset_password_token"],
13
+ # "Post" => ["cached_votes"]
14
+ # }
15
+
16
+ # Set the batch size for processing records (default: 1000)
17
+ # config.batch_size = 1000
18
+
19
+ # Set the report format (:text or :json)
20
+ # config.report_format = :text
21
+
22
+ # Enable automatic fixing of simple validation errors
23
+ # config.auto_fix = false
24
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :db_validator do
4
+ desc "Validate all records in the database"
5
+ task validate: :environment do
6
+ if ENV["models"]
7
+ models = ENV["models"].split(",").map(&:strip).map(&:downcase)
8
+ DbValidator.configuration.only_models = models
9
+ end
10
+
11
+ DbValidator.configuration.limit = ENV["limit"].to_i if ENV["limit"]
12
+
13
+ DbValidator.configuration.report_format = ENV["format"].to_sym if ENV["format"]
14
+
15
+ validator = DbValidator::Validator.new
16
+ report = validator.validate_all
17
+ puts report
18
+ end
19
+ end
data/readme.md ADDED
@@ -0,0 +1,111 @@
1
+ # DbValidator
2
+
3
+ DbValidator helps identify invalid records in your Rails application that don't meet model validation requirements.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'db_validator'
11
+ ```
12
+
13
+ Then execute:
14
+
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ Or install it yourself:
20
+
21
+ ```bash
22
+ $ gem install db_validator
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### Rake Task
28
+
29
+ The simplest way to run validation is using the provided rake task:
30
+
31
+ #### Validate all models
32
+
33
+ ```bash
34
+ $ rake db_validator:validate
35
+ ```
36
+
37
+ #### Validate specific models
38
+
39
+ ```bash
40
+ $ rake db_validator:validate models=user,post
41
+ ```
42
+
43
+ #### Limit the number of records to validate
44
+
45
+ ```bash
46
+ $ rake db_validator:validate limit=1000
47
+ ```
48
+
49
+ #### Generate JSON report
50
+
51
+ ```bash
52
+ $ rake db_validator:validate format=json
53
+ ```
54
+
55
+ ### Ruby Code
56
+
57
+ You can also run validation from your Ruby code:
58
+
59
+ #### Validate all models
60
+
61
+ ```ruby
62
+ report = DbValidator.validate
63
+ ```
64
+
65
+ #### Validate with options
66
+
67
+ ```ruby
68
+ report = DbValidator.validate(
69
+ only_models: ['User', 'Post'],
70
+ limit: 1000,
71
+ report_format: :json
72
+ )
73
+ ```
74
+
75
+ ## Report Format
76
+
77
+ ### Text Format (Default)
78
+
79
+ ```
80
+ DbValidator Report
81
+ ==================
82
+ Found invalid records:
83
+
84
+ User: 2 invalid records
85
+ ID: 1
86
+ email is invalid (actual value: "invalid-email")
87
+ ID: 2
88
+ name can't be blank (actual value: "")
89
+
90
+ Post: 1 invalid record
91
+ ID: 5
92
+ title can't be blank (actual value: "")
93
+ category is not included in the list (allowed values: news, blog, actual value: "invalid")
94
+ ```
95
+
96
+ ### JSON Format
97
+
98
+ ```json
99
+ [
100
+ {
101
+ "model": "User",
102
+ "id": 1,
103
+ "errors": ["email is invalid (actual value: \"invalid-email\")"]
104
+ },
105
+ {
106
+ "model": "User",
107
+ "id": 2,
108
+ "errors": ["name can't be blank (actual value: \"\")"]
109
+ }
110
+ ]
111
+ ```
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: db_validator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Krzysztof Duda
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-11-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: database_cleaner-active_record
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '6.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '6.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sqlite3
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: colorize
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.8.1
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.8.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: ruby-progressbar
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.11'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.11'
97
+ description: A comprehensive solution for validating existing database records in
98
+ Rails applications
99
+ email:
100
+ - duda_krzysztof@outlook.com
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - config/initializers/db_validator.rb
106
+ - lib/db_validator.rb
107
+ - lib/db_validator/configuration.rb
108
+ - lib/db_validator/fixer.rb
109
+ - lib/db_validator/railtie.rb
110
+ - lib/db_validator/reporter.rb
111
+ - lib/db_validator/validator.rb
112
+ - lib/db_validator/version.rb
113
+ - lib/generators/db_validator/install_generator.rb
114
+ - lib/generators/db_validator/templates/initializer.rb
115
+ - lib/tasks/db_validator_tasks.rake
116
+ - readme.md
117
+ homepage: https://github.com/yourusername/db_validator
118
+ licenses:
119
+ - MIT
120
+ metadata:
121
+ rubygems_mfa_required: 'true'
122
+ post_install_message:
123
+ rdoc_options: []
124
+ require_paths:
125
+ - lib
126
+ required_ruby_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: 2.7.0
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ requirements: []
137
+ rubygems_version: 3.5.16
138
+ signing_key:
139
+ specification_version: 4
140
+ summary: Database-wide validation for Rails applications
141
+ test_files: []