activerecord-data_integrity 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 15651ea16d6fc6051e45163a15ff512472c440ee173e4c8c9acd9cf3e45e010f
4
+ data.tar.gz: 4badc1574b0c1c1329d1f0fdd14e10016a7097113cc6bf098751062d53f68cb3
5
+ SHA512:
6
+ metadata.gz: 698d90f68b8f72a07f2cbc3e5910d60418a3ac4a2aa881007ebccf5e656d3ee85c52d2140218a42396025cb79d782e6a36406b666ed22dfd6cacd4bbc1df0192
7
+ data.tar.gz: 41e5fd22054feeb45b1f53ce9df8d00a4d070dc944690d1e79b4c7f3a8e9c797956bb6e35c75c5bd26c5b0be44683a8da43680fc77516e41f49e4f19db46d819
data/.gitignore ADDED
@@ -0,0 +1,52 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ spec/internal/log/
10
+ /test/tmp/
11
+ /test/version_tmp/
12
+ /tmp/
13
+ Gemfile.lock
14
+
15
+ # Used by dotenv library to load environment variables.
16
+ # .env
17
+
18
+ ## Specific to RubyMotion:
19
+ .dat*
20
+ .repl_history
21
+ build/
22
+ *.bridgesupport
23
+ build-iPhoneOS/
24
+ build-iPhoneSimulator/
25
+
26
+ ## Specific to RubyMotion (use of CocoaPods):
27
+ #
28
+ # We recommend against adding the Pods directory to your .gitignore. However
29
+ # you should judge for yourself, the pros and cons are mentioned at:
30
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
31
+ #
32
+ # vendor/Pods/
33
+
34
+ ## Documentation cache and generated files:
35
+ /.yardoc/
36
+ /_yardoc/
37
+ /doc/
38
+ /rdoc/
39
+
40
+ ## Environment normalization:
41
+ /.bundle/
42
+ /vendor/bundle
43
+ /lib/bundler/man/
44
+
45
+ # for a library or gem, you might want to ignore these files since the code is
46
+ # intended to run in multiple environments; otherwise, check them in:
47
+ # Gemfile.lock
48
+ # .ruby-version
49
+ # .ruby-gemset
50
+
51
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
52
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ require: rubocop-rspec
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 2.5
5
+ TargetRailsVersion: 4.0
6
+ Exclude:
7
+ - 'activerecord-data_integrity.gemspec'
8
+ - 'spec/support/*'
9
+ - 'tmp/**/*'
10
+ - 'gemfiles/*'
11
+ - 'spec/internal/**/*'
12
+
13
+ Metrics/LineLength:
14
+ Max: 110
data/.travis.yml ADDED
@@ -0,0 +1,18 @@
1
+ sudo: require
2
+ services:
3
+ - docker
4
+ install:
5
+ - gem install dip
6
+ before_script:
7
+ - unset BUNDLE_GEMFILE
8
+ - dip provision
9
+ script:
10
+ - dip rspec
11
+ - dip rubocop
12
+ deploy:
13
+ provider: rubygems
14
+ api_key:
15
+ secure: ILU6srwid/tAV00OmpcR4hpyTHO8TWK4C1wIYxmt+v0f/g8BuzsaZ/iXLM8aXWu7uRGeyp9jV0kA8Dg4KTCq4FlTazo3u2al7wm840h2KhB/mj/QxN8GBIr6hySQaouwRfzVRpVr7KBcHp10ElBd9GXhVm5v5V/j14RYW9wELqwQSyBnAjv2AI8InHJZE+tGnioqqSQHv7bM3fEnOMyd7BGwnvmVIQS2RbW+65i1r8iTFOrR+cpL1lg+q1Kbk0tr1ciEyg0kkNvZ/DUu8AxNMIlZlSdcNL73BN5a/CtJaAclSHbwEr2yGg3fFT61SR20/RmhE45v+crE8sKP+xGxhqkg+GL+LQQ5ouDuHi6r2tLOTMyeVUs402AWBth8dWyd1Xcvbho7owSLlUirYD8clD4Kh+XyfKMC1p4cjJiR4Id1F0089o12GcrwQebaYphnbrCYEtcoC8gts9p1fDiiY9ekrMfh8GcpYIHDmKbmpx79bCRk4Z45qdyOP206df2/pDEqLCEs5YoCP/rNpTi0ft1nRomlBPoX6ZeSrle9f3wbmp49Jrn/j3jENth24jkDpdE7D0Tr8nyGARTR/RG3K3XuXTTkCWQRynSM/CbitnLaRFv4+1ngQnpmiYOzHzcwSpMul9tLnQj3GeMXUawBs/OYD61NgG+FBczIQ295JO8=
16
+ gem: activerecord-data_integrity
17
+ on:
18
+ tags: true
data/Appraisals ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise '5.0' do
4
+ gem 'activerecord', '~> 5.0'
5
+ end
6
+
7
+ appraise '5.1' do
8
+ gem 'activerecord', '~> 5.1'
9
+ end
10
+
11
+ appraise '5.2' do
12
+ gem 'activerecord', '~> 5.2'
13
+ end
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in activerecord-data_integrity.gemspec
8
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Salahutdinov Dmitry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Salahutdinov Dmitry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ [![Gem Version](https://badge.fury.io/rb/activerecord-data_integrity.svg)](https://badge.fury.io/rb/activerecord-data_integrity)
2
+ [![Build Status](https://travis-ci.org/dsalahutdinov/activerecord-data_integrity.svg?branch=master)](https://travis-ci.org/dsalahutdinov/activerecord-data_integrity.svg?branch=master)
3
+
4
+ # ActiveRecord::DataIntegrity
5
+
6
+ Checks your ActiveRecord models to match data integrity principles and rules.
7
+ Out of the box it enforces many issues such as the lack of foreign keys.
8
+
9
+ ## Intallation
10
+ ```ruby
11
+ group :development do
12
+ ...
13
+ gem 'activerecord-data_integrity', require: false
14
+ end
15
+ ```
16
+ ## Quickstart
17
+
18
+ Run `data_integrity` CLI-tool in your Rails project's folder:
19
+
20
+ ```bash
21
+ cd ~/amplifr
22
+ bundle exec data_integrity
23
+ ```
24
+
25
+ It will load the Rails application, check the data integrity issues and give the similar output:
26
+ ```
27
+ BelongsTo/ForeignKey: Label belongs_to project but has no foreign key to projects.id
28
+ Accordance/TablePresence: Stat::Hourly has no underlying table hourly_stats
29
+ ...
30
+ ```
31
+
32
+ ## Supported Issues
33
+
34
+ For now tool checks the following issues:
35
+ - [x] The lack of database foreign keys for belongs_to/has_many associations
36
+ - [x] The lack of not-null constraint for the columns with presence validation
37
+ - [x] Inclusion validated colums should have `enum` data type
38
+
39
+ ## Roadmap (TODO & Help Wanted)
40
+
41
+ 1) Support extra database issues, such as:
42
+ - [ ] presence of `dependend` option set for association and not confliction with underlying `ON DELETE` option of foreign key contraint
43
+ - [ ] check for foreign keys to have bigint data type
44
+ - [ ] presence of index for the foreign keys search
45
+ - [ ] checks for paranoia models and indexes excludion "removed" rows
46
+
47
+ 2) Config for exluding some rules for the specific models (rubocop like)
48
+
49
+ 3) Autofix for the fixing the issue, mostly by generating safe migrations
50
+
51
+ 4) Possibility to run with checking only specific rule
52
+
53
+ ## Contributing
54
+
55
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/activerecord-data_integrity.
56
+
57
+ ## Run tests
58
+
59
+ The easiest way to tests up and running is to use handy [dip](https://github.com/bibendi/dip) gem with Docker and Docker Compose:
60
+
61
+ ```bash
62
+ gem install dip
63
+ git checkout git@github.com:dsalahutdinov/activerecord-data_integrity.git
64
+ cd activerecord-data_integrity
65
+ dip provision
66
+ dip rspec
67
+ ```
68
+
69
+ Otherwise (without Docker) set up environment manually:
70
+ ```bash
71
+ git checkout git@github.com:dsalahutdinov/activerecord-data_integrity.git
72
+ cd activerecord-data_integrity
73
+ bundle install
74
+ bundle appraisal
75
+ DB_HOST=localhost DB_NAME=testdb DB_USERNAME=postgres bundle appraisal rspec
76
+ ```
77
+
78
+ ## License
79
+
80
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[rubocop spec]
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'active_record/data_integrity/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'activerecord-data_integrity'
9
+ spec.version = ActiveRecord::DataIntegrity::VERSION
10
+ spec.authors = ['Salahutdinov Dmitry']
11
+ spec.email = ['dsalahutdinov@gmail.com']
12
+
13
+ spec.summary = 'Checks your ActiveRecord models to match data integrity principles and rules'
14
+ spec.description = 'CLI-tool to check data integrity of the Rails-application project'
15
+ spec.homepage = 'https://github.com/dsalahutdinov/activerecord-data_integrity'
16
+ spec.license = 'MIT'
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
20
+ if spec.respond_to?(:metadata)
21
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
22
+ else
23
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
24
+ 'public gem pushes.'
25
+ end
26
+
27
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
28
+ f.match(%r{^(test|spec|features)/})
29
+ end
30
+ spec.bindir = 'exe'
31
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ['lib']
33
+
34
+ spec.add_runtime_dependency 'pg'
35
+ spec.add_runtime_dependency 'rails'
36
+ spec.add_runtime_dependency 'rainbow'
37
+
38
+ spec.add_development_dependency 'appraisal', '~> 2.2'
39
+ spec.add_development_dependency 'bundler', '~> 1.16'
40
+ spec.add_development_dependency 'combustion', '~> 1.0'
41
+ spec.add_development_dependency 'rake', '~> 10.0'
42
+ spec.add_development_dependency 'rspec', '~> 3.0'
43
+
44
+ spec.add_development_dependency 'rubocop'
45
+ spec.add_development_dependency 'rubocop-rspec'
46
+
47
+ spec.add_development_dependency 'byebug'
48
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'activerecord-data_integrity'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/dip.yml ADDED
@@ -0,0 +1,46 @@
1
+ version: '2'
2
+
3
+ environment:
4
+ BUNDLE_GEMFILE: /app/Gemfile
5
+
6
+ compose:
7
+ files:
8
+ - docker-compose.yml
9
+
10
+ interaction:
11
+ bash:
12
+ service: app
13
+ compose_run_options: [no-deps]
14
+
15
+ app:
16
+ service: app
17
+ subcommands:
18
+ console:
19
+ command: ./bin/console
20
+ clean:
21
+ command: rm -rf Gemfile.lock gemfiles/*.gemfile.*
22
+
23
+
24
+ bundle:
25
+ service: app
26
+ command: bundle
27
+ compose_run_options: [no-deps]
28
+
29
+ appraisal:
30
+ service: app
31
+ command: bundle exec appraisal
32
+ compose_run_options: [no-deps]
33
+
34
+ rspec:
35
+ service: app
36
+ command: bundle exec appraisal bundle exec rspec
37
+
38
+ rubocop:
39
+ service: app
40
+ command: bundle exec rubocop
41
+ compose_run_options: [no-deps]
42
+
43
+ provision:
44
+ - dip app clean
45
+ - dip bundle install
46
+ - dip appraisal install
@@ -0,0 +1,28 @@
1
+ version: '3.4'
2
+
3
+ services:
4
+ app:
5
+ image: ruby:2.5.1
6
+ environment:
7
+ - BUNDLE_PATH=/bundle
8
+ - BUNDLE_CONFIG=/app/.bundle/config
9
+ - DB_HOST=db
10
+ - DB_NAME=docker
11
+ - DB_USERNAME=postgres
12
+ command: bash
13
+ working_dir: /app
14
+ volumes:
15
+ - .:/app:cached
16
+ - bundler_data:/bundle
17
+ tmpfs:
18
+ - /tmp
19
+ depends_on:
20
+ - db
21
+
22
+ db:
23
+ image: postgres:10
24
+ environment:
25
+ - POSTGRES_DB=docker
26
+
27
+ volumes:
28
+ bundler_data:
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'activerecord/data_integrity'
6
+
7
+ ActiveRecord::DataIntegrity::CLI.new.run
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module DataIntegrity
5
+ # CLI application class
6
+ class CLI
7
+ def initialize; end
8
+
9
+ def run
10
+ require_rails
11
+
12
+ results = cops.map do |cop_class|
13
+ ActiveRecord::Base.descendants.each do |model|
14
+ cop_class.new(model).call
15
+ end
16
+ end
17
+
18
+ ActiveRecord::DataIntegrity::Collector.render
19
+
20
+ exit(1) if results.include?(false)
21
+ end
22
+
23
+ private
24
+
25
+ def cops
26
+ @cops ||= ActiveRecord::DataIntegrity::Cop.descendants
27
+ end
28
+
29
+ def require_rails
30
+ # Rails load ugly hack :)
31
+ require File.expand_path('config/environment', Dir.pwd)
32
+ Kernel.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))
33
+ Rails.application.eager_load!
34
+ Rails.logger.level = 0
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module DataIntegrity
5
+ # collects result info for rendering
6
+ class Collector
7
+ class << self
8
+ def log(cop, message)
9
+ data.push(cop: cop, message: message)
10
+ end
11
+
12
+ def progress(_cop, char)
13
+ print char
14
+ end
15
+
16
+ def render
17
+ group_data_by_cop_name.each do |cop_name, items|
18
+ items.each do |item|
19
+ puts "#{Rainbow(cop_name).red}:"\
20
+ " #{Rainbow(item[:cop].model.name).yellow}"\
21
+ " #{item[:message]}"
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def data
29
+ @data ||= []
30
+ end
31
+
32
+ def group_data_by_cop_name
33
+ data.each_with_object({}) do |item, hash|
34
+ hash[item[:cop].name] ||= []
35
+ hash[item[:cop].name] << item
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../cop'
4
+
5
+ module ActiveRecord
6
+ module DataIntegrity
7
+ module Accordance
8
+ # Checks the primary key integer has 8 bytes length
9
+ class PrimaryKey < ActiveRecord::DataIntegrity::Cop
10
+ def call
11
+ log('has short integer primary key') unless valid?
12
+ progress(valid?, 'P')
13
+ valid?
14
+ end
15
+
16
+ private
17
+
18
+ def valid?
19
+ return true unless connection.table_exists?(model.table_name)
20
+
21
+ @valid ||= !connection.primary_keys(model.table_name).map! do |pk|
22
+ column_valid?(pk)
23
+ end.include?(false)
24
+ end
25
+
26
+ def column_valid?(name)
27
+ column = connection.columns(model.table_name).find { |c| c.name == name }
28
+ column.type == :integer && column.limit.present? && column.limit >= 8
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../cop'
4
+
5
+ module ActiveRecord
6
+ module DataIntegrity
7
+ module Accordance
8
+ # Check the presence of the underlying table for the model
9
+ class TablePresence < ActiveRecord::DataIntegrity::Cop
10
+ def call
11
+ connection.table_exists?(model.table_name).tap do |result|
12
+ log("has no underlying table #{model.table_name}") unless result
13
+ progress(result, 'T')
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../cop'
4
+
5
+ module ActiveRecord
6
+ module DataIntegrity
7
+ module BelongsTo
8
+ # Checks foreign key presence to the parent table of belongs_to association
9
+ class ForeignKey < ActiveRecord::DataIntegrity::Cop
10
+ def call
11
+ results = associations.map do |association|
12
+ valid?(association)
13
+ end
14
+
15
+ results.none?(&:!)
16
+ end
17
+
18
+ private
19
+
20
+ def valid?(association)
21
+ success = foreign_key?(association)
22
+ unless success
23
+ to_table = association.class_name.constantize.table_name
24
+ log("belongs_to #{association.name} but has no foreign key to #{to_table}.id")
25
+ end
26
+ progress(success, 'F')
27
+
28
+ success
29
+ rescue NameError
30
+ log("Error processing #{model.name}.#{association.name}")
31
+ end
32
+
33
+ def associations
34
+ model
35
+ ._reflections
36
+ .values
37
+ .select { |association| association.is_a?(ActiveRecord::Reflection::BelongsToReflection) }
38
+ .reject(&:polymorphic?)
39
+ end
40
+
41
+ def foreign_key?(association)
42
+ to_table = association.class_name.constantize.table_name
43
+ connection.foreign_keys(model.table_name).any? do |foreign_key|
44
+ foreign_key.to_table == to_table && foreign_key.options.fetch(:primary_key) == 'id'
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module DataIntegrity
5
+ # Checking cop base class
6
+ class Cop
7
+ attr_reader :model
8
+ delegate :connection, to: :model
9
+
10
+ def initialize(model)
11
+ @model = model
12
+ end
13
+
14
+ def name
15
+ self.class.name.gsub('ActiveRecord::DataIntegrity::', '').gsub('::', '/')
16
+ end
17
+
18
+ def log(msg)
19
+ ActiveRecord::DataIntegrity::Collector.log(self, msg)
20
+ end
21
+
22
+ def progress(subresult, false_char = 'E')
23
+ ActiveRecord::DataIntegrity::Collector.progress(self, subresult ? '.' : false_char)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../cop'
4
+
5
+ module ActiveRecord
6
+ module DataIntegrity
7
+ module HasMany
8
+ # Checks foreign key presence to the parent table of belongs_to association
9
+ class ForeignKey < ActiveRecord::DataIntegrity::Cop
10
+ def call
11
+ results = associations.map do |association|
12
+ valid?(association)
13
+ end
14
+
15
+ results.none?(&:!)
16
+ end
17
+
18
+ private
19
+
20
+ def valid?(association)
21
+ success = foreign_key?(association)
22
+ unless success
23
+ from_table = association.class_name.constantize.table_name
24
+ log("has_many #{association.name} but has no foreign key from #{from_table}.id")
25
+ end
26
+ progress(success, 'F')
27
+
28
+ success
29
+ rescue NameError
30
+ log("Error processing #{model.name}.#{association.name}")
31
+ end
32
+
33
+ def associations
34
+ model
35
+ ._reflections
36
+ .values
37
+ .select { |association| association.is_a?(ActiveRecord::Reflection::HasManyReflection) }
38
+ .reject(&:polymorphic?)
39
+ end
40
+
41
+ def foreign_key?(association)
42
+ to_table = model.table_name
43
+ connection.foreign_keys(association.class_name.constantize.table_name).any? do |foreign_key|
44
+ foreign_key.to_table == to_table && foreign_key.options.fetch(:primary_key) == 'id'
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../cop'
4
+
5
+ module ActiveRecord
6
+ module DataIntegrity
7
+ module Validation
8
+ # Checks foreign key presence to the parent table of belongs_to association
9
+ class Inclusion < ActiveRecord::DataIntegrity::Cop
10
+ def call
11
+ results = validators.map do |validator|
12
+ validator.attributes.map do |attribute|
13
+ valid?(attribute)
14
+ end
15
+ end.flatten
16
+
17
+ results.none?(&:!)
18
+ end
19
+
20
+ private
21
+
22
+ def valid?(attribute)
23
+ column = find_column(attribute)
24
+ return true if column.nil?
25
+
26
+ result = (column.type == :enum)
27
+ progress(result, 'D')
28
+ !column && log("has column #{attribute} with inclusion which is not enum") unless result
29
+ result
30
+ end
31
+
32
+ def validators
33
+ model
34
+ .validators
35
+ .select { |v| v.is_a?(ActiveModel::Validations::InclusionValidator) }
36
+ end
37
+
38
+ def find_column(attribute)
39
+ return nil unless connection.table_exists?(model.table_name)
40
+
41
+ connection
42
+ .columns(model.table_name)
43
+ .find { |col| col.name == attribute.to_s }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../cop'
4
+
5
+ module ActiveRecord
6
+ module DataIntegrity
7
+ module Validation
8
+ # Checks foreign key presence to the parent table of belongs_to association
9
+ class Presence < ActiveRecord::DataIntegrity::Cop
10
+ def call
11
+ results = validators.map do |validator|
12
+ validator.attributes.map do |attribute|
13
+ valid?(attribute)
14
+ end
15
+ end.flatten
16
+
17
+ results.none?(&:!)
18
+ end
19
+
20
+ private
21
+
22
+ def valid?(attribute)
23
+ column = find_column(attribute)
24
+ return true if column.nil?
25
+
26
+ result = !column.null
27
+ progress(result, 'D')
28
+ !column && log("has nullable column #{attribute} with presence validation") unless result
29
+ result
30
+ end
31
+
32
+ def validators
33
+ model
34
+ .validators
35
+ .select { |v| v.is_a?(ActiveRecord::Validations::PresenceValidator) }
36
+ end
37
+
38
+ def find_column(attribute)
39
+ connection
40
+ .columns(model.table_name)
41
+ .find { |col| col.name == attribute.to_s }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module DataIntegrity
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/data_integrity/version'
4
+
5
+ require 'rails/all'
6
+ require 'active_record/data_integrity/cli'
7
+ require 'rainbow'
8
+
9
+ require 'active_record/data_integrity/collector'
10
+
11
+ require 'active_record/data_integrity/cop/accordance/table_presence'
12
+ require 'active_record/data_integrity/cop/accordance/primary_key'
13
+ require 'active_record/data_integrity/cop/belongs_to/foreign_key'
14
+ require 'active_record/data_integrity/cop/validation/presence'
15
+ require 'active_record/data_integrity/cop/validation/inclusion'
16
+ require 'active_record/data_integrity/cop/has_many/foreign_key'
17
+
18
+ module ActiveRecord
19
+ # Base module for the gem
20
+ module DataIntegrity
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/data_integrity'
metadata ADDED
@@ -0,0 +1,228 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-data_integrity
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Salahutdinov Dmitry
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-11-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pg
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rainbow
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: appraisal
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.16'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.16'
83
+ - !ruby/object:Gem::Dependency
84
+ name: combustion
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: byebug
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: CLI-tool to check data integrity of the Rails-application project
168
+ email:
169
+ - dsalahutdinov@gmail.com
170
+ executables:
171
+ - data_integrity
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - ".gitignore"
176
+ - ".rspec"
177
+ - ".rubocop.yml"
178
+ - ".travis.yml"
179
+ - Appraisals
180
+ - Gemfile
181
+ - LICENSE
182
+ - LICENSE.txt
183
+ - README.md
184
+ - Rakefile
185
+ - activerecord-data_integrity.gemspec
186
+ - bin/console
187
+ - bin/setup
188
+ - dip.yml
189
+ - docker-compose.yml
190
+ - exe/data_integrity
191
+ - lib/active_record/data_integrity.rb
192
+ - lib/active_record/data_integrity/cli.rb
193
+ - lib/active_record/data_integrity/collector.rb
194
+ - lib/active_record/data_integrity/cop/accordance/primary_key.rb
195
+ - lib/active_record/data_integrity/cop/accordance/table_presence.rb
196
+ - lib/active_record/data_integrity/cop/belongs_to/foreign_key.rb
197
+ - lib/active_record/data_integrity/cop/cop.rb
198
+ - lib/active_record/data_integrity/cop/has_many/foreign_key.rb
199
+ - lib/active_record/data_integrity/cop/validation/inclusion.rb
200
+ - lib/active_record/data_integrity/cop/validation/presence.rb
201
+ - lib/active_record/data_integrity/version.rb
202
+ - lib/activerecord/data_integrity.rb
203
+ homepage: https://github.com/dsalahutdinov/activerecord-data_integrity
204
+ licenses:
205
+ - MIT
206
+ metadata:
207
+ allowed_push_host: https://rubygems.org
208
+ post_install_message:
209
+ rdoc_options: []
210
+ require_paths:
211
+ - lib
212
+ required_ruby_version: !ruby/object:Gem::Requirement
213
+ requirements:
214
+ - - ">="
215
+ - !ruby/object:Gem::Version
216
+ version: '0'
217
+ required_rubygems_version: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ version: '0'
222
+ requirements: []
223
+ rubyforge_project:
224
+ rubygems_version: 2.7.7
225
+ signing_key:
226
+ specification_version: 4
227
+ summary: Checks your ActiveRecord models to match data integrity principles and rules
228
+ test_files: []