yeet_dba 0.1.1 → 0.1.2

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: 32599ee97b8b627e2a093054e31eed282e2141e93569723b98b40357781d1399
4
- data.tar.gz: 2ee79b2e47bee2c889ccee622be08de60f84d5f8daff92585b1cf52e5880e98c
3
+ metadata.gz: 8d312f90603469c805f7d69d49ae5ed3fe8f825ef64c0a324f74a49557934804
4
+ data.tar.gz: faf652821f509fee8db20df69f426ac5e80af5e4be0d8bea4a578ce2d4130839
5
5
  SHA512:
6
- metadata.gz: 8d36e9b4683ea1a9f09238db879e29383ec53fa0e6c28dc4e722738831264833d361a30f2cc6647ad84904e695da4033d7fe0ef5c2a7abc86c59e9a73b30c1f2
7
- data.tar.gz: 22fb42465caa8ec621aa472924eeb27c5f14e78a5abe9a858a6f48064d19af3651a42f5d65bd7170608bc8b4078cb0baa970204592cb04942d5993925ab84240
6
+ metadata.gz: 94aa0ebb9b45927eacb6ec29ce75678ea83fbbb26b48503d72cf77e9581e3d8e8d321f1b360ada978bd54aabd54d3cb94023241e61fd6968908f86def82e0b8c
7
+ data.tar.gz: fb1e94beb1c6a44d9debac764069cbdfceecf490c3f5d5fc24396a0f4c5529b5b065cf8575a698db0c9e11193df01a00065070ba7ae01180ebbb6ec31f08644d
data/.DS_Store CHANGED
Binary file
data/.rubocop ADDED
@@ -0,0 +1,159 @@
1
+ AllCops:
2
+ DisplayCopNames: true
3
+ TargetRubyVersion: 2.5
4
+ TargetRailsVersion: 5.1
5
+
6
+ Rails:
7
+ Enabled: true
8
+
9
+ Style/FormatStringToken:
10
+ Enabled: false
11
+
12
+ Style/FrozenStringLiteralComment:
13
+ Enabled: false
14
+
15
+ Metrics/LineLength:
16
+ Max: 100
17
+
18
+ Metrics/AbcSize:
19
+ Max: 40
20
+
21
+ Rails/SkipsModelValidations:
22
+ Enabled: true
23
+
24
+ Style/GuardClause:
25
+ Enabled: false
26
+
27
+ Style/AsciiComments:
28
+ Enabled: false
29
+
30
+ Metrics/MethodLength:
31
+ Max: 25
32
+
33
+ Metrics/CyclomaticComplexity:
34
+ Max: 7
35
+
36
+ Metrics/PerceivedComplexity:
37
+ Max: 8
38
+
39
+ # Relaxed.Ruby.Style
40
+
41
+ Style/Alias:
42
+ Enabled: false
43
+ StyleGuide: http://relaxed.ruby.style/#stylealias
44
+
45
+ Metrics/BlockLength:
46
+ Enabled: true
47
+
48
+ Style/BeginBlock:
49
+ Enabled: false
50
+ StyleGuide: http://relaxed.ruby.style/#stylebeginblock
51
+
52
+ Style/BlockDelimiters:
53
+ Enabled: false
54
+ StyleGuide: http://relaxed.ruby.style/#styleblockdelimiters
55
+
56
+ Style/Documentation:
57
+ Enabled: false
58
+ StyleGuide: http://relaxed.ruby.style/#styledocumentation
59
+
60
+ Layout/DotPosition:
61
+ Enabled: false
62
+ StyleGuide: http://relaxed.ruby.style/#styledotposition
63
+
64
+ Style/DoubleNegation:
65
+ Enabled: false
66
+ StyleGuide: http://relaxed.ruby.style/#styledoublenegation
67
+
68
+ Style/EndBlock:
69
+ Enabled: false
70
+ StyleGuide: http://relaxed.ruby.style/#styleendblock
71
+
72
+ Style/FormatString:
73
+ Enabled: false
74
+ StyleGuide: http://relaxed.ruby.style/#styleformatstring
75
+
76
+ Style/IfUnlessModifier:
77
+ Enabled: false
78
+ StyleGuide: http://relaxed.ruby.style/#styleifunlessmodifier
79
+
80
+ Style/Lambda:
81
+ Enabled: false
82
+ StyleGuide: http://relaxed.ruby.style/#stylelambda
83
+
84
+ Style/ModuleFunction:
85
+ Enabled: false
86
+ StyleGuide: http://relaxed.ruby.style/#stylemodulefunction
87
+
88
+ Style/MultilineBlockChain:
89
+ Enabled: false
90
+ StyleGuide: http://relaxed.ruby.style/#stylemultilineblockchain
91
+
92
+ Style/NegatedIf:
93
+ Enabled: false
94
+ StyleGuide: http://relaxed.ruby.style/#stylenegatedif
95
+
96
+ Style/NegatedWhile:
97
+ Enabled: false
98
+ StyleGuide: http://relaxed.ruby.style/#stylenegatedwhile
99
+
100
+ Style/ParallelAssignment:
101
+ Enabled: false
102
+ StyleGuide: http://relaxed.ruby.style/#styleparallelassignment
103
+
104
+ Style/PercentLiteralDelimiters:
105
+ Enabled: false
106
+ StyleGuide: http://relaxed.ruby.style/#stylepercentliteraldelimiters
107
+
108
+ Style/PerlBackrefs:
109
+ Enabled: false
110
+ StyleGuide: http://relaxed.ruby.style/#styleperlbackrefs
111
+
112
+ Style/Semicolon:
113
+ Enabled: false
114
+ StyleGuide: http://relaxed.ruby.style/#stylesemicolon
115
+
116
+ Style/SignalException:
117
+ Enabled: false
118
+ StyleGuide: http://relaxed.ruby.style/#stylesignalexception
119
+
120
+ Style/SingleLineBlockParams:
121
+ Enabled: false
122
+ StyleGuide: http://relaxed.ruby.style/#stylesinglelineblockparams
123
+
124
+ Style/SingleLineMethods:
125
+ Enabled: false
126
+ StyleGuide: http://relaxed.ruby.style/#stylesinglelinemethods
127
+
128
+ Layout/SpaceBeforeBlockBraces:
129
+ Enabled: false
130
+ StyleGuide: http://relaxed.ruby.style/#stylespacebeforeblockbraces
131
+
132
+ Layout/SpaceInsideParens:
133
+ Enabled: false
134
+ StyleGuide: http://relaxed.ruby.style/#stylespaceinsideparens
135
+
136
+ Style/SpecialGlobalVars:
137
+ Enabled: false
138
+ StyleGuide: http://relaxed.ruby.style/#stylespecialglobalvars
139
+
140
+ Style/TrailingCommaInArrayLiteral:
141
+ Enabled: false
142
+
143
+ Style/TrailingCommaInHashLiteral:
144
+ Enabled: false
145
+
146
+ Style/WhileUntilModifier:
147
+ Enabled: false
148
+ StyleGuide: http://relaxed.ruby.style/#stylewhileuntilmodifier
149
+
150
+ Style/RegexpLiteral:
151
+ Enabled: false
152
+
153
+ Lint/AmbiguousRegexpLiteral:
154
+ Enabled: false
155
+ StyleGuide: http://relaxed.ruby.style/#lintambiguousregexpliteral
156
+
157
+ Lint/AssignmentInCondition:
158
+ Enabled: false
159
+ StyleGuide: http://relaxed.ruby.style/#lintassignmentincondition
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # 0.1.2
2
+
3
+ ## Major changes
4
+
5
+ Added rake task to find invalid data
6
+ Added rake task to nullify and delete invalid data
7
+
8
+ ## Minor changes
9
+
10
+ Add rubocop
11
+ Add rspec tests
12
+
13
+ # 0.1.1
14
+
15
+ Patch bug with skipping invalid columns that have orphaned data
16
+
1
17
  # 0.1.0
2
18
 
3
19
  Initial release
data/Gemfile CHANGED
@@ -1,6 +1,15 @@
1
- source "https://rubygems.org"
1
+ source 'https://rubygems.org'
2
2
 
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in yeet_dba.gemspec
6
6
  gemspec
7
+
8
+ gem 'actionpack'
9
+ gem 'activerecord'
10
+ gem 'railties'
11
+ gem 'rspec-rails'
12
+ gem 'sqlite3'
13
+ gem 'test-unit'
14
+
15
+ gem 'pry', require: true
data/Gemfile.lock ADDED
@@ -0,0 +1,118 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ yeet_dba (0.1.2)
5
+ actionpack (>= 3.0, < 6.0)
6
+ activerecord (>= 3.0, < 6.0)
7
+ railties (>= 3.0, < 6.0)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ actionpack (5.2.3)
13
+ actionview (= 5.2.3)
14
+ activesupport (= 5.2.3)
15
+ rack (~> 2.0)
16
+ rack-test (>= 0.6.3)
17
+ rails-dom-testing (~> 2.0)
18
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
19
+ actionview (5.2.3)
20
+ activesupport (= 5.2.3)
21
+ builder (~> 3.1)
22
+ erubi (~> 1.4)
23
+ rails-dom-testing (~> 2.0)
24
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
25
+ activemodel (5.2.3)
26
+ activesupport (= 5.2.3)
27
+ activerecord (5.2.3)
28
+ activemodel (= 5.2.3)
29
+ activesupport (= 5.2.3)
30
+ arel (>= 9.0)
31
+ activesupport (5.2.3)
32
+ concurrent-ruby (~> 1.0, >= 1.0.2)
33
+ i18n (>= 0.7, < 2)
34
+ minitest (~> 5.1)
35
+ tzinfo (~> 1.1)
36
+ arel (9.0.0)
37
+ builder (3.2.3)
38
+ coderay (1.1.2)
39
+ concurrent-ruby (1.1.5)
40
+ crass (1.0.4)
41
+ diff-lcs (1.3)
42
+ erubi (1.8.0)
43
+ i18n (1.6.0)
44
+ concurrent-ruby (~> 1.0)
45
+ loofah (2.2.3)
46
+ crass (~> 1.0.2)
47
+ nokogiri (>= 1.5.9)
48
+ method_source (0.9.2)
49
+ mini_portile2 (2.4.0)
50
+ minitest (5.11.3)
51
+ nokogiri (1.10.2)
52
+ mini_portile2 (~> 2.4.0)
53
+ power_assert (1.1.4)
54
+ pry (0.12.2)
55
+ coderay (~> 1.1.0)
56
+ method_source (~> 0.9.0)
57
+ rack (2.0.6)
58
+ rack-test (1.1.0)
59
+ rack (>= 1.0, < 3)
60
+ rails-dom-testing (2.0.3)
61
+ activesupport (>= 4.2.0)
62
+ nokogiri (>= 1.6)
63
+ rails-html-sanitizer (1.0.4)
64
+ loofah (~> 2.2, >= 2.2.2)
65
+ railties (5.2.3)
66
+ actionpack (= 5.2.3)
67
+ activesupport (= 5.2.3)
68
+ method_source
69
+ rake (>= 0.8.7)
70
+ thor (>= 0.19.0, < 2.0)
71
+ rake (10.5.0)
72
+ rspec (3.8.0)
73
+ rspec-core (~> 3.8.0)
74
+ rspec-expectations (~> 3.8.0)
75
+ rspec-mocks (~> 3.8.0)
76
+ rspec-core (3.8.0)
77
+ rspec-support (~> 3.8.0)
78
+ rspec-expectations (3.8.2)
79
+ diff-lcs (>= 1.2.0, < 2.0)
80
+ rspec-support (~> 3.8.0)
81
+ rspec-mocks (3.8.0)
82
+ diff-lcs (>= 1.2.0, < 2.0)
83
+ rspec-support (~> 3.8.0)
84
+ rspec-rails (3.8.2)
85
+ actionpack (>= 3.0)
86
+ activesupport (>= 3.0)
87
+ railties (>= 3.0)
88
+ rspec-core (~> 3.8.0)
89
+ rspec-expectations (~> 3.8.0)
90
+ rspec-mocks (~> 3.8.0)
91
+ rspec-support (~> 3.8.0)
92
+ rspec-support (3.8.0)
93
+ sqlite3 (1.4.0)
94
+ test-unit (3.3.1)
95
+ power_assert
96
+ thor (0.20.3)
97
+ thread_safe (0.3.6)
98
+ tzinfo (1.2.5)
99
+ thread_safe (~> 0.1)
100
+
101
+ PLATFORMS
102
+ ruby
103
+
104
+ DEPENDENCIES
105
+ actionpack
106
+ activerecord
107
+ bundler (~> 1.17)
108
+ pry
109
+ railties
110
+ rake (~> 10.0)
111
+ rspec (~> 3.0)
112
+ rspec-rails
113
+ sqlite3
114
+ test-unit
115
+ yeet_dba!
116
+
117
+ BUNDLED WITH
118
+ 1.17.3
data/README.md CHANGED
@@ -1,12 +1,26 @@
1
1
  ![Foreign Key by Ary Prasetyo from the Noun Project](./yeet_dba.png)
2
2
 
3
3
  # yeet_dba - find missing foreign key constraints
4
+ [![Gem Version](https://badge.fury.io/rb/yeet_dba.svg)](https://badge.fury.io/rb/yeet_dba) <a href="https://codeclimate.com/github/KevinColemanInc/yeet_dba/maintainability"><img src="https://api.codeclimate.com/v1/badges/a0baa6373d4be7f0d630/maintainability" /></a>[![Build Status](https://travis-ci.com/KevinColemanInc/yeet_dba.svg?branch=master)](https://travis-ci.com/KevinColemanInc/yeet_dba)
4
5
 
5
6
  yeet_dba scans your rails tables for missing foreign key constraints. If there are no dangling records, it will create a migration to add the foreign key constraints on all the table it is safe.
6
7
 
7
8
  If you have dangling migrations, check the generator logs to see where you have invalid orphaned rows. Orphaned row meaning a row with an id that doesn't exist in the associated table.
8
9
 
9
- but [why should I use foreign keys?](https://softwareengineering.stackexchange.com/questions/375704/why-should-i-use-foreign-keys-in-database)
10
+ ### But why should I use foreign keys?
11
+
12
+ You can save yourself an N+1 call by checking if the id has a value instead of loading up the object.
13
+
14
+ ```ruby
15
+ user.company.id # bad - N+1
16
+ user.company_id # good
17
+ ```
18
+
19
+ But this doesn't work if you don't nullify the `company_id` when the company is deleted. Foreign key constraints prevent you from deleting a record without cleaning out the associated tables.
20
+
21
+ ### But what is the difference between yeet_db and [lol_dba](https://github.com/plentz/lol_dba)?
22
+
23
+ lol_dba will only add indexes for RoR models. yeet_dba looks at every table (including join tables) to add foreign key constraints, which also add indexes.
10
24
 
11
25
  ## Installation
12
26
 
@@ -22,6 +36,8 @@ And then execute:
22
36
 
23
37
  ## Usage
24
38
 
39
+ ### Foriegn keys migration
40
+
25
41
  This probably should run against the production database so you can know if there are dangling records. If there are records with a value, but not the corresponding table does not have an id, then the migration will fail.
26
42
 
27
43
  ```
@@ -30,27 +46,97 @@ $ RAILS_ENV=production rails g yeet_dba:foreign_key_migration
30
46
 
31
47
  This will create a new migration with for every foreign_key that can safely be added without running into orphaned data errors. We also warn you if active_record models that are missing association declarations (`has_many`, `belongs_to`, etc.)`
32
48
 
33
- `WARNING - cannot find association for alternative_housings . supplier_id | suppliers`
49
+ `WARNING - cannot find an association for alternative_housings . supplier_id | suppliers`
34
50
 
35
- We also warn if we have tables that don't have existing models attached to them. This can be safe to ignore, because join tables on many to many relations don't need models, but ideally everything should have an AR model backing it.
51
+ We also warn if we have tables that don't have existing models attached to them. This can be safe to ignore because join tables on many to many relations don't need models, but ideally, everything should have an AR model backing it.
36
52
 
37
- `WARNING - cannot find model for alternative_housings . supplier_id | suppliers`
53
+ `WARNING - cannot find a model for alternative_housings . supplier_id | suppliers`
38
54
 
39
55
  Finnally, if there is a table that we think should have a foreign key constraint, but there are dangling values we warn you against that too.
40
56
 
41
57
  `WARNING - orphaned rows alternative_housings . supplier_id | suppliers`
42
58
 
43
- ## Requirements
59
+ ### Invalid rows
60
+
61
+ If a row has an id, but there doesn't exist an id the expected associated table, then the row has bad data and should either be fixed by nulling the orphaned row or assigning it to an existing row.
62
+
63
+ This rake task will scan every column for orphaned rows.
64
+
65
+ ```
66
+ $ RAILS_ENV=production rake yeet_dba:find_invalid_columns
67
+ ```
68
+
69
+ Sample output:
70
+
71
+ ```
72
+ ---RESULTS---
73
+
74
+ 🚨Houston, we have a problem 🚨. We found 1 invalid column.
75
+
76
+ -> notifications.primary_image_id
77
+ Invalid rows: 83
78
+ Foreign table: active_storage_attachments
79
+
80
+ This query should return no results:
81
+ SELECT "notifications".* FROM "notifications" left join active_storage_attachments as association_table on association_table.id = notifications.primary_image_id WHERE "notifications"."primary_image_id" IS NOT NULL AND (association_table.id is null)
82
+
83
+ ```
84
+
85
+ ### Fix invalid rows
86
+
87
+ If a row has an id, but there doesn't exist an id the expected associated table, then the row has bad data and should either be fixed by nulling the orphaned row or assigning it to an existing row.
88
+
89
+ This rake task will scan every column for orphaned rows.
90
+
91
+ ```
92
+ $ RAILS_ENV=production rake yeet_dba:fix_invalid_columns
93
+ ```
94
+
95
+ Sample output:
96
+
97
+ ```
98
+ ---RESULTS---
99
+
100
+ 🚨Houston, we have a problem 🚨. We found 1 invalid column.
101
+
102
+ -> notifications.primary_image_id
103
+ Invalid rows: 83
104
+ Foreign table: active_storage_attachments
44
105
 
45
- Rails 5.2 (but it may work with 5.0+)
106
+ This query should return no results:
107
+ SELECT "notifications".* FROM "notifications" left join active_storage_attachments as association_table on association_table.id = notifications.primary_image_id WHERE "notifications"."primary_image_id" IS NOT NULL AND (association_table.id is null)
108
+
109
+ ```
110
+
111
+ ### Add missing foriegn keys as a rake task
112
+
113
+ You might want to add foreign keys outside of your regular deployment flow in case there are failures and deployment would be blocked by bad data. This would be especially obnoxious for MySql users since you can't rollback migrations.
114
+
115
+ ```
116
+ $ RAILS_ENV=production rake yeet_dba:add_foreign_keys
117
+ ```
118
+
119
+ Sample output
120
+
121
+ ```
122
+ ERROR - users . profile_id failed to add key
123
+ ```
124
+
125
+ This rake task is idempotent (safe to run as many times as you need).
126
+
127
+ ## Compatibility
128
+
129
+ - Rails 5.2 (but it may work with 5.0+)
130
+ - Ruby 2.4+
46
131
 
47
132
  ## Road map to v1
48
133
 
49
- - [ ] rspec tests
50
- - [ ] add rake task identify all dangling records
51
- - [ ] add rake task to automatically nullify or destroy dangling records
52
- - [ ] run as a rake task
134
+ - [x] rspec tests
135
+ - [x] add rake task identify all dangling records
136
+ - [x] add rake task to automatically nullify or destroy dangling records
137
+ - [x] run adding foreign keys as rake task instead of generating a migration
53
138
  - [ ] support "soft delete" gems
139
+ - [ ] Use rails associations to find columns that should be "not null" to [improve performance](https://stackoverflow.com/questions/1017239/how-do-null-values-affect-performance-in-a-database-search)
54
140
 
55
141
 
56
142
  ## Development
@@ -72,4 +158,12 @@ The gem is available as open source under the terms of the [MIT License](https:/
72
158
  Everyone interacting in the YeetDb project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/kevincolemaninc/yeet_dba/blob/master/CODE_OF_CONDUCT.md).
73
159
 
74
160
  ## Logo design attribute
75
- Foreign Key by Ary Prasetyo from the Noun Project
161
+ Foreign Key by Ary Prasetyo from the Noun Project
162
+
163
+ ## Thanks
164
+
165
+ [AvoVietnam - Chat with Vietnamese](https://www.avovietnam.com)
166
+
167
+ ## Author
168
+
169
+ Kevin Coleman, [https://kcoleman.me/](https://kcoleman.me)
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task default: :spec
data/bin/console CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "yeet_dbaa"
3
+ require 'bundler/setup'
4
+ require 'yeet_dba'
5
5
 
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +10,5 @@ require "yeet_dbaa"
10
10
  # require "pry"
11
11
  # Pry.start
12
12
 
13
- require "irb"
13
+ require 'irb'
14
14
  IRB.start(__FILE__)
data/lib/.DS_Store CHANGED
Binary file
Binary file
@@ -4,11 +4,11 @@ module YeetDba
4
4
  # Custom scaffolding generator
5
5
  class ForeignKeyMigrationGenerator < Rails::Generators::Base
6
6
  include Rails::Generators::Migration
7
- source_root File.expand_path('../templates', __FILE__)
8
- desc "Generates migration for adding foreign key constraints."
7
+ source_root File.expand_path('templates', __dir__)
8
+ desc 'Generates migration for adding foreign key constraints.'
9
9
 
10
10
  def copy_migration_and_spec_files
11
- migration_template "add_foreign_keys_yeet_dba.rb",
11
+ migration_template 'add_foreign_keys_yeet_dba.rb',
12
12
  migration_file,
13
13
  migration_version: migration_version
14
14
  end
@@ -16,15 +16,15 @@ module YeetDba
16
16
  private
17
17
 
18
18
  def migration_file
19
- File.join("db/migrate", "add_foreign_keys_yeet_dba.rb")
19
+ File.join('db/migrate', 'add_foreign_keys_yeet_dba.rb')
20
20
  end
21
21
 
22
22
  def migration_version
23
23
  "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
24
24
  end
25
25
 
26
- def self.next_migration_number(path)
27
- Time.now.utc.strftime("%Y%m%d%H%M%S%L")
26
+ def self.next_migration_number(_path)
27
+ Time.now.utc.strftime('%Y%m%d%H%M%S%L')
28
28
  end
29
29
  end
30
- end
30
+ end
@@ -0,0 +1,5 @@
1
+ # lib/Rakefile
2
+ require 'yeet_dba'
3
+
4
+ path = File.expand_path(__dir__)
5
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| import f }
@@ -1,19 +1,19 @@
1
1
  module YeetDba
2
- class ArColumn
3
- attr_accessor :column_name, :table_name, :tables
2
+ class Column
3
+ attr_accessor :db_column, :table_name, :tables
4
4
 
5
- def initialize(column_name:, table_name:, tables:)
6
- @column_name = column_name
5
+ def initialize(db_column:, table_name:, tables:)
6
+ @db_column = db_column
7
7
  @table_name = table_name
8
8
  @tables = tables
9
9
  end
10
10
 
11
11
  def is_association?
12
- column_name.name =~ /_id\z/
12
+ db_column.name =~ /_id\z/
13
13
  end
14
14
 
15
15
  def association_klass
16
- model && model.reflections[association_name]&.klass
16
+ association&.klass
17
17
  end
18
18
 
19
19
  def association_table_name
@@ -21,7 +21,7 @@ module YeetDba
21
21
  end
22
22
 
23
23
  def association_name
24
- column_name.name.gsub(/_id\z/, '')
24
+ db_column.name.gsub(/_id\z/, '')
25
25
  end
26
26
 
27
27
  def model
@@ -37,7 +37,7 @@ module YeetDba
37
37
  end
38
38
 
39
39
  def foreign_key_exists?
40
- ActiveRecord::Migration.foreign_key_exists?(table_name, column: column_name.name)
40
+ ActiveRecord::Migration.foreign_key_exists?(table_name, column: db_column.name)
41
41
  end
42
42
 
43
43
  def guessed_table_name
@@ -0,0 +1,27 @@
1
+ module YeetDba
2
+ class MissingForeignKeys
3
+ def self.foreign_keys
4
+ eager_load!
5
+ tables.map do |table_name|
6
+ Table.new(table_name: table_name,
7
+ tables: tables).missing_keys
8
+ end.flatten
9
+ end
10
+
11
+ def self.invalid_columns
12
+ eager_load!
13
+ tables.map do |table_name|
14
+ Table.new(table_name: table_name,
15
+ tables: tables).invalid_columns
16
+ end.flatten
17
+ end
18
+
19
+ def self.eager_load!
20
+ Rails.application.eager_load! if defined?(Rails) && !Rails.env.test?
21
+ end
22
+
23
+ def self.tables
24
+ ActiveRecord::Base.connection.tables
25
+ end
26
+ end
27
+ end
@@ -10,4 +10,4 @@ module YeetDba
10
10
  @column = column
11
11
  end
12
12
  end
13
- end
13
+ end
@@ -0,0 +1,20 @@
1
+ module YeetDba
2
+ class InvalidColumn
3
+ attr_accessor :table_name,
4
+ :column,
5
+ :verify_data
6
+
7
+ def initialize(table_name:, column:, verify_data:)
8
+ @table_name = table_name
9
+ @column = column
10
+ @verify_data = verify_data
11
+ end
12
+
13
+ delegate :association_table_name, :db_column, :association, to: :column
14
+ delegate :orphaned_rows_count, :query, to: :verify_data
15
+
16
+ def to_s
17
+ "#{table_name} . #{db_column.name} has #{orphaned_rows_count} invalid rows with foreign table #{association_table_name}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ require 'rails'
2
+
3
+ module YeetDba
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :yeet_dba
6
+
7
+ rake_tasks do
8
+ path = File.expand_path(__dir__)
9
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,67 @@
1
+ module YeetDba
2
+ class Table
3
+ attr_accessor :table_name, :tables
4
+
5
+ def initialize(table_name:, tables:)
6
+ @table_name = table_name
7
+ @tables = tables
8
+ end
9
+
10
+ def invalid_columns
11
+ missing_keys_array = []
12
+ columns.each do |db_column|
13
+ column = Column.new(db_column: db_column, table_name: table_name, tables: tables)
14
+ next unless column.is_association?
15
+ next if column.polymorphic_association?
16
+ next if column.foreign_key_exists?
17
+ next if column.association_table_name.blank?
18
+ verify_data = VerifyData.new(column: column)
19
+ next unless verify_data.orphaned_rows?
20
+
21
+ invalid_column = InvalidColumn.new(table_name: table_name,
22
+ column: column,
23
+ verify_data: verify_data)
24
+ missing_keys_array.push(invalid_column)
25
+
26
+ end
27
+ missing_keys_array
28
+ end
29
+
30
+ def missing_keys
31
+ missing_keys_array = []
32
+ columns.each do |db_column|
33
+ column = Column.new(db_column: db_column, table_name: table_name, tables: tables)
34
+ next unless column.is_association?
35
+
36
+ unless column.model
37
+ puts "WARNING - cannot find a model for #{table_name} . #{db_column.name} | #{column&.association_table_name}"
38
+ end
39
+
40
+ unless column.association
41
+ puts "WARNING - cannot find an association for #{table_name} . #{db_column.name} | #{column&.association_table_name}"
42
+ end
43
+
44
+ next if column.polymorphic_association?
45
+ next if column.foreign_key_exists?
46
+ next if column.association_table_name.blank?
47
+
48
+ if VerifyData.new(column: column).orphaned_rows?
49
+ puts "YeetDba - orphaned rows. Skipping #{table_name} . #{db_column.name} | #{column&.association_table_name}"
50
+ next
51
+ end
52
+
53
+ foreign_key = ForeignKey.new(table_a: table_name,
54
+ table_b: column&.association_table_name,
55
+ column: db_column.name)
56
+ missing_keys_array.push(foreign_key)
57
+ end
58
+ missing_keys_array
59
+ end
60
+
61
+ private
62
+
63
+ def columns
64
+ ActiveRecord::Base.connection.columns(table_name)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,20 @@
1
+ namespace :yeet_dba do
2
+ desc 'Add foreign keys in a rake migration'
3
+ task add_foreign_keys: :environment do
4
+ foreign_keys = YeetDba::MissingForeignKeys.foreign_keys
5
+
6
+ puts "Trying to add #{foreign_keys.length}"
7
+ puts
8
+ foreign_keys.each do |foreign_key|
9
+
10
+ begin
11
+ ActiveRecord::Migration.add_foreign_key(foreign_key.table_a,
12
+ foreign_key.table_b,
13
+ column: foreign_key.column)
14
+
15
+ rescue ActiveRecord::InvalidForeignKey
16
+ puts "ERROR - #{foreign_key.table_a} . #{foreign_key.column} failed to add key"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,55 @@
1
+ namespace :yeet_dba do
2
+ desc 'Show all of the tables.columns with bad data'
3
+ task find_invalid_columns: :environment do
4
+ columns = YeetDba::MissingForeignKeys.invalid_columns
5
+ puts
6
+ puts '---RESULTS---'
7
+ puts
8
+ if columns.empty?
9
+ puts 'All good here. 👍'
10
+ else
11
+ puts "🚨Houston, we have a problem 🚨. We found #{columns.length} invalid column#{columns.length == 1 ? '' : 's'}."
12
+ puts
13
+ columns.each do |invalid_column|
14
+ puts "-> #{invalid_column.table}.#{invalid_column.column}"
15
+ puts "Invalid rows: #{invalid_column.orphaned_rows_count}"
16
+ puts "Foreign table: #{invalid_column.association_table_name}"
17
+ puts
18
+ puts 'This query should return no results:'
19
+ puts invalid_column.query
20
+ puts
21
+ end
22
+ end
23
+ end
24
+
25
+ desc 'Set all of the rows to null if there is bad data'
26
+ task nullify_or_destroy_invalid_rows: :environment do
27
+ columns = YeetDba::MissingForeignKeys.invalid_columns
28
+ next puts "Your data looks good!" if columns.empty?
29
+
30
+ columns.each do |column|
31
+ puts column.to_s
32
+ end
33
+ puts
34
+ puts "WARNING - THIS MAY CAUSE PERM DATA LOSS"
35
+ puts
36
+ puts "I am going to give you 8s to change your mind"
37
+ sleep 8
38
+ puts "ok, here we go..."
39
+ sleep 1
40
+
41
+ columns.each do |invalid_column|
42
+ required = invalid_column.column.association.options&.key?(:optional) ? !invalid_column.column.association.options[:optional] : invalid_column.column.model.belongs_to_required_by_default
43
+ nullable = invalid_column.column.db_column.null
44
+ if required
45
+ # delete
46
+ invalid_column.verify_data.orphaned_rows.destroy_all
47
+ elsif nullable
48
+ # null it out
49
+ invalid_column.verify_data.orphaned_rows.update_all(invalid_column.db_column.name => nil)
50
+ else
51
+ puts "WARNING - #{invalid_column.table_name} . #{invalid_column.db_column.name} is not nullable"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,37 @@
1
+ require 'pry'
2
+
3
+ module YeetDba
4
+ class VerifyData
5
+ attr_accessor :column
6
+
7
+ def initialize(column:)
8
+ @column = column
9
+ end
10
+
11
+ def orphaned_rows?
12
+ orphaned_rows.first
13
+ end
14
+
15
+ def orphaned_rows_count
16
+ orphaned_rows.count
17
+ end
18
+
19
+ def query
20
+ orphaned_rows.to_sql
21
+ end
22
+
23
+ def orphaned_rows
24
+ association = column.association
25
+
26
+ column_name = column.db_column.name
27
+ table_name = column.table_name
28
+ association_table = column.association_table_name
29
+ model = column.model
30
+
31
+ # Check to see there could be rows with bad data
32
+ model.joins("left join #{association_table} as association_table on association_table.id = #{table_name}.#{column_name}")
33
+ .where.not(column_name => nil)
34
+ .where('association_table.id is null')
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ module YeetDba
2
+ VERSION = '0.1.2'.freeze
3
+ end
data/lib/yeet_dba.rb CHANGED
@@ -1,9 +1,11 @@
1
- require "yeet_dba/version"
2
- require "yeet_dba/ar_column"
3
- require "yeet_dba/ar_table"
4
- require "yeet_dba/foreign_key"
5
- require "yeet_dba/missing_foreign_keys"
6
- require "yeet_dba/verify_data"
1
+ require 'yeet_dba/version'
2
+ require 'yeet_dba/table'
3
+ require 'yeet_dba/missing_foreign_keys'
4
+ require 'yeet_dba/verify_data'
5
+ require 'yeet_dba/railtie' if defined?(Rails)
6
+ require 'yeet_dba/column'
7
+ require 'yeet_dba/models/foreign_key'
8
+ require 'yeet_dba/models/invalid_column'
7
9
 
8
10
  module YeetDba
9
11
  class Error < StandardError; end
data/yeet_dba.gemspec CHANGED
@@ -1,40 +1,45 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "./yeet_dba/version"
3
+ require 'yeet_dba/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "yeet_dba"
6
+ spec.name = 'yeet_dba'
8
7
  spec.version = YeetDba::VERSION
9
- spec.authors = ["Kevin Coleman"]
10
- spec.email = ["kevin.coleman@sparkstart.io"]
8
+ spec.platform = Gem::Platform::RUBY
9
+ spec.authors = ['Kevin Coleman']
10
+ spec.email = ['kevin.coleman@sparkstart.io']
11
11
 
12
- spec.summary = %q{Generates foreign key constraint migrations for rails databases}
13
- spec.description = %q{This scan every ActiveRecord model looking for relationships ('has_many', 'belongs_to', etc.) and adds foreign key constraints.}
12
+ spec.summary = 'Generates foreign key constraint migrations for rails databases'
13
+ spec.description = "This scan every ActiveRecord model looking for relationships ('has_many', 'belongs_to', etc.) and adds foreign key constraints."
14
14
  spec.homepage = 'http://rubygems.org/gems/yeet_dba'
15
- spec.license = "MIT"
15
+ spec.license = 'MIT'
16
16
 
17
17
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
18
  # to allow pushing to a single host or delete this section to allow pushing to any host.
19
19
  if spec.respond_to?(:metadata)
20
- spec.metadata["homepage_uri"] = spec.homepage
21
- spec.metadata["source_code_uri"] = "https://github.com/kevincolemaninc/yeet_dba"
22
- spec.metadata["changelog_uri"] = "https://github.com/kevincolemaninc/yeet_dba/master/CHANGELOG.md"
20
+ spec.metadata['homepage_uri'] = spec.homepage
21
+ spec.metadata['source_code_uri'] = 'https://github.com/kevincolemaninc/yeet_dba'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/kevincolemaninc/yeet_dba/master/CHANGELOG.md'
23
23
  else
24
- raise "RubyGems 2.0 or newer is required to protect against " \
25
- "public gem pushes."
24
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
25
+ 'public gem pushes.'
26
26
  end
27
27
 
28
28
  # Specify which files should be added to the gem when it is released.
29
29
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
30
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
31
31
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
32
  end
33
- spec.bindir = "exe"
33
+ spec.bindir = 'exe'
34
34
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
- spec.require_paths = ["lib"]
35
+ spec.require_paths = ['lib']
36
+
37
+ spec.add_development_dependency 'bundler', '~> 1.17'
38
+ spec.add_development_dependency 'rake', '~> 10.0'
39
+ spec.add_development_dependency 'rspec', '~> 3.0'
36
40
 
37
- spec.add_development_dependency "bundler", "~> 1.17"
38
- spec.add_development_dependency "rake", "~> 10.0"
39
- spec.add_development_dependency "rspec", "~> 3.0"
41
+ spec.required_ruby_version = '>= 2.4.0'
42
+ spec.add_dependency 'actionpack', '>= 3.0', '< 6.0'
43
+ spec.add_dependency 'activerecord', '>= 3.0', '< 6.0'
44
+ spec.add_dependency 'railties', '>= 3.0', '< 6.0'
40
45
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeet_dba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Coleman
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-03-29 00:00:00.000000000 Z
11
+ date: 2019-04-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,66 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: actionpack
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '6.0'
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '3.0'
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '6.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: activerecord
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ - - "<"
83
+ - !ruby/object:Gem::Version
84
+ version: '6.0'
85
+ type: :runtime
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '3.0'
92
+ - - "<"
93
+ - !ruby/object:Gem::Version
94
+ version: '6.0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: railties
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '3.0'
102
+ - - "<"
103
+ - !ruby/object:Gem::Version
104
+ version: '6.0'
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '3.0'
112
+ - - "<"
113
+ - !ruby/object:Gem::Version
114
+ version: '6.0'
55
115
  description: This scan every ActiveRecord model looking for relationships ('has_many',
56
116
  'belongs_to', etc.) and adds foreign key constraints.
57
117
  email:
@@ -63,28 +123,35 @@ files:
63
123
  - ".DS_Store"
64
124
  - ".gitignore"
65
125
  - ".rspec"
126
+ - ".rubocop"
66
127
  - ".travis.yml"
67
128
  - CHANGELOG.md
68
129
  - CODE_OF_CONDUCT.md
69
130
  - Gemfile
131
+ - Gemfile.lock
70
132
  - LICENSE.txt
71
133
  - README.md
72
134
  - Rakefile
73
135
  - bin/console
74
136
  - bin/setup
75
137
  - lib/.DS_Store
138
+ - lib/generators/.DS_Store
76
139
  - lib/generators/yeet_dba/foreign_key_migration_generator.rb
77
- - lib/generators/yeet_dba/generator_helpers.rb
78
140
  - lib/generators/yeet_dba/templates/add_foreign_keys_yeet_dba.rb
79
141
  - lib/yeet_dba.rb
142
+ - lib/yeet_dba/Rakefile
143
+ - lib/yeet_dba/column.rb
144
+ - lib/yeet_dba/missing_foreign_keys.rb
145
+ - lib/yeet_dba/models/foreign_key.rb
146
+ - lib/yeet_dba/models/invalid_column.rb
147
+ - lib/yeet_dba/railtie.rb
148
+ - lib/yeet_dba/table.rb
149
+ - lib/yeet_dba/tasks/add_foreign_keys.rake
150
+ - lib/yeet_dba/tasks/bad_data/find_orphaned_rows.rake
151
+ - lib/yeet_dba/verify_data.rb
152
+ - lib/yeet_dba/version.rb
80
153
  - yeet_dba.gemspec
81
154
  - yeet_dba.png
82
- - yeet_dba/ar_column.rb
83
- - yeet_dba/ar_table.rb
84
- - yeet_dba/foreign_key.rb
85
- - yeet_dba/missing_foreign_keys.rb
86
- - yeet_dba/verify_data.rb
87
- - yeet_dba/version.rb
88
155
  homepage: http://rubygems.org/gems/yeet_dba
89
156
  licenses:
90
157
  - MIT
@@ -100,7 +167,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
100
167
  requirements:
101
168
  - - ">="
102
169
  - !ruby/object:Gem::Version
103
- version: '0'
170
+ version: 2.4.0
104
171
  required_rubygems_version: !ruby/object:Gem::Requirement
105
172
  requirements:
106
173
  - - ">="
@@ -1,5 +0,0 @@
1
- module YeetDba
2
- module GeneratorHelpers
3
-
4
- end
5
- end
data/yeet_dba/ar_table.rb DELETED
@@ -1,47 +0,0 @@
1
- module YeetDba
2
- class ArTable
3
- attr_accessor :table_name, :tables
4
-
5
- def initialize(table_name:, tables:)
6
- @table_name = table_name
7
- @tables = tables
8
- end
9
-
10
- def missing_keys
11
- missing_keys_array = []
12
- columns.each do |column_name|
13
- column = ArColumn.new(column_name: column_name, table_name: table_name, tables: tables)
14
- next unless column.is_association?
15
-
16
- unless column.model
17
- puts "YeetDba - cannot find model for #{table_name} . #{column_name.name} | #{column&.association_table_name}"
18
- end
19
-
20
- unless column.association
21
- puts "YeetDba - cannot find association for #{table_name} . #{column_name.name} | #{column&.association_table_name}"
22
- end
23
-
24
- next if column.polymorphic_association?
25
- next if column.foreign_key_exists?
26
- next if column.association_table_name.blank?
27
-
28
- if VerifyData.new(column: column).orphaned_rows?
29
- puts "YeetDba - orphaned rows. Skipping #{table_name} . #{column_name.name} | #{column&.association_table_name}"
30
- next
31
- end
32
-
33
- foreign_key = ForeignKey.new(table_a: table_name,
34
- table_b: column&.association_table_name,
35
- column: column_name.name)
36
- missing_keys_array.push(foreign_key)
37
- end
38
- missing_keys_array
39
- end
40
-
41
- private
42
-
43
- def columns
44
- ActiveRecord::Base.connection.columns(table_name)
45
- end
46
- end
47
- end
@@ -1,11 +0,0 @@
1
- module YeetDba
2
- class MissingForeignKeys
3
- def self.foreign_keys
4
- Rails.application.eager_load!
5
- tables = ActiveRecord::Base.connection.tables
6
- tables.map do |table_name|
7
- ArTable.new(table_name: table_name, tables: tables).missing_keys
8
- end.flatten
9
- end
10
- end
11
- end
@@ -1,35 +0,0 @@
1
- module YeetDba
2
- class VerifyData
3
- attr_accessor :column
4
-
5
- def initialize(column:)
6
- @column = column
7
- end
8
-
9
- def orphaned_rows?
10
- orphaned_rows.first
11
- end
12
-
13
- private
14
-
15
- def orphaned_rows
16
- association = column.association
17
- binding.pry if column.column_name.is_a?(String)
18
-
19
- column_name = column.column_name.name
20
- table_name = column.table_name
21
- association_table = column.association_table_name
22
- model = column.model
23
-
24
- # Check to see there could be rows with bad data
25
- bad_ids = model.joins("left join #{association_table} as association_table on association_table.id = #{table_name}.#{column_name}")
26
- .where.not(column_name => nil)
27
- .where('association_table.id is null')
28
- .pluck(:id)
29
-
30
- # select the rows with the invalid ids
31
- # AR doesn't support joins with update, so the bad_ids are selected in a different query (above ^)
32
- model.where(id: bad_ids)
33
- end
34
- end
35
- end
data/yeet_dba/version.rb DELETED
@@ -1,3 +0,0 @@
1
- module YeetDba
2
- VERSION = "0.1.1"
3
- end