├── .DS_Store ├── .gitignore ├── .rspec ├── .rubocop ├── .ruby-version ├── .travis.yml ├── .yeet_dba.example.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── .DS_Store ├── generators │ ├── .DS_Store │ └── yeet_dba │ │ ├── foreign_key_migration_generator.rb │ │ └── templates │ │ └── add_foreign_keys_yeet_dba.rb ├── yeet_dba.rb └── yeet_dba │ ├── Rakefile │ ├── column.rb │ ├── missing_foreign_keys.rb │ ├── models │ ├── foreign_key.rb │ └── invalid_column.rb │ ├── railtie.rb │ ├── table.rb │ ├── tasks │ ├── add_foreign_keys.rake │ └── bad_data │ │ └── find_orphaned_rows.rake │ ├── verify_data.rb │ └── version.rb ├── spec ├── fixtures │ ├── .yeet_dba.yml │ ├── app │ │ └── models │ │ │ ├── company.rb │ │ │ ├── profile.rb │ │ │ └── user.rb │ └── schema.rb ├── spec_helper.rb └── yeet_dba │ ├── column_spec.rb │ ├── missing_foreign_keys_spec.rb │ ├── models │ ├── foreign_key_spec.rb │ └── invalid_column_spec.rb │ └── version_spec.rb ├── yeet_dba.gemspec └── yeet_dba.png /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinColemanInc/yeet_dba/f8c5aec73f3a9eed28167322fbeceabc6eff2040/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | *.gem -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop: -------------------------------------------------------------------------------- 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 160 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: ruby 4 | cache: bundler 5 | rvm: 6 | - 2.5.3 7 | before_install: gem install bundler -v 1.17.3 8 | -------------------------------------------------------------------------------- /.yeet_dba.example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_tables: 3 | - table_to_be_ignored 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.3 (unreleased, master) 2 | 3 | # 1.0.2 4 | 5 | ## Minor changes 6 | 7 | Support Rails 7 8 | Bump ruby versions 9 | Bump bundler versions 10 | 11 | # 1.0.1 12 | 13 | ## Minor changes 14 | 15 | Support Rails 6 16 | Update gems and remove pry 17 | Fix bug in `find_orphaned_rows` rake task for [Issue #10](https://github.com/KevinColemanInc/yeet_dba/issues/10) 18 | 19 | # 1.0.0 20 | 21 | ## Minor changes 22 | 23 | Improved test coverage 24 | 25 | # 0.1.2 26 | 27 | ## Major changes 28 | 29 | Added rake task to find invalid data 30 | Added rake task to nullify and delete invalid data 31 | 32 | ## Minor changes 33 | 34 | Add rubocop 35 | Add rspec tests 36 | 37 | # 0.1.1 38 | 39 | Patch bug with skipping invalid columns that have orphaned data 40 | 41 | # 0.1.0 42 | 43 | Initial release 44 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kevin.coleman@sparkstart.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in yeet_dba.gemspec 6 | gemspec 7 | 8 | gem 'actionpack' 9 | gem 'activerecord' 10 | gem 'railties' 11 | gem 'rspec-rails' 12 | gem 'sqlite3' 13 | gem 'test-unit' 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | yeet_dba (1.0.2) 5 | actionpack (>= 3.0, < 8.0) 6 | activerecord (>= 3.0, < 8.0) 7 | railties (>= 3.0, < 8.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | actionpack (6.0.0) 13 | actionview (= 6.0.0) 14 | activesupport (= 6.0.0) 15 | rack (~> 2.0) 16 | rack-test (>= 0.6.3) 17 | rails-dom-testing (~> 2.0) 18 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 19 | actionview (6.0.0) 20 | activesupport (= 6.0.0) 21 | builder (~> 3.1) 22 | erubi (~> 1.4) 23 | rails-dom-testing (~> 2.0) 24 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 25 | activemodel (6.0.0) 26 | activesupport (= 6.0.0) 27 | activerecord (6.0.0) 28 | activemodel (= 6.0.0) 29 | activesupport (= 6.0.0) 30 | activesupport (6.0.0) 31 | concurrent-ruby (~> 1.0, >= 1.0.2) 32 | i18n (>= 0.7, < 2) 33 | minitest (~> 5.1) 34 | tzinfo (~> 1.1) 35 | zeitwerk (~> 2.1, >= 2.1.8) 36 | builder (3.2.3) 37 | concurrent-ruby (1.1.5) 38 | crass (1.0.5) 39 | diff-lcs (1.3) 40 | erubi (1.8.0) 41 | i18n (1.6.0) 42 | concurrent-ruby (~> 1.0) 43 | loofah (2.3.1) 44 | crass (~> 1.0.2) 45 | nokogiri (>= 1.5.9) 46 | method_source (0.9.2) 47 | mini_portile2 (2.6.1) 48 | minitest (5.11.3) 49 | nokogiri (1.12.5) 50 | mini_portile2 (~> 2.6.1) 51 | racc (~> 1.4) 52 | power_assert (1.1.5) 53 | racc (1.5.2) 54 | rack (2.2.3) 55 | rack-test (1.1.0) 56 | rack (>= 1.0, < 3) 57 | rails-dom-testing (2.0.3) 58 | activesupport (>= 4.2.0) 59 | nokogiri (>= 1.6) 60 | rails-html-sanitizer (1.2.0) 61 | loofah (~> 2.2, >= 2.2.2) 62 | railties (6.0.0) 63 | actionpack (= 6.0.0) 64 | activesupport (= 6.0.0) 65 | method_source 66 | rake (>= 0.8.7) 67 | thor (>= 0.20.3, < 2.0) 68 | rake (13.0.1) 69 | rspec (3.8.0) 70 | rspec-core (~> 3.8.0) 71 | rspec-expectations (~> 3.8.0) 72 | rspec-mocks (~> 3.8.0) 73 | rspec-core (3.8.2) 74 | rspec-support (~> 3.8.0) 75 | rspec-expectations (3.8.4) 76 | diff-lcs (>= 1.2.0, < 2.0) 77 | rspec-support (~> 3.8.0) 78 | rspec-mocks (3.8.1) 79 | diff-lcs (>= 1.2.0, < 2.0) 80 | rspec-support (~> 3.8.0) 81 | rspec-rails (3.8.2) 82 | actionpack (>= 3.0) 83 | activesupport (>= 3.0) 84 | railties (>= 3.0) 85 | rspec-core (~> 3.8.0) 86 | rspec-expectations (~> 3.8.0) 87 | rspec-mocks (~> 3.8.0) 88 | rspec-support (~> 3.8.0) 89 | rspec-support (3.8.2) 90 | sqlite3 (1.4.1) 91 | test-unit (3.3.3) 92 | power_assert 93 | thor (0.20.3) 94 | thread_safe (0.3.6) 95 | tzinfo (1.2.5) 96 | thread_safe (~> 0.1) 97 | zeitwerk (2.1.10) 98 | 99 | PLATFORMS 100 | ruby 101 | 102 | DEPENDENCIES 103 | actionpack 104 | activerecord 105 | bundler (~> 2.1) 106 | railties 107 | rake (~> 13.0) 108 | rspec (~> 3.0) 109 | rspec-rails 110 | sqlite3 111 | test-unit 112 | yeet_dba! 113 | 114 | BUNDLED WITH 115 | 2.1.4 116 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Kevin Coleman 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Foreign Key by Ary Prasetyo from the Noun Project](./yeet_dba.png) 3 | 4 | # yeet_dba - find missing foreign key constraints 5 | [![Gem Version](https://badge.fury.io/rb/yeet_dba.svg)](https://badge.fury.io/rb/yeet_dba) [![Build Status](https://travis-ci.com/KevinColemanInc/yeet_dba.svg?branch=master)](https://travis-ci.com/KevinColemanInc/yeet_dba) 6 | 7 | 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. 8 | 9 | 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. 10 | 11 | ### But why should I use foreign keys? 12 | 13 | You can save yourself an N+1 call by checking if the id has a value instead of loading up the object. 14 | 15 | ```ruby 16 | user.company.id # bad - N+1 17 | user.company_id # good 18 | ``` 19 | 20 | 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. 21 | 22 | ### But what is the difference between yeet_db and [lol_dba](https://github.com/plentz/lol_dba)? 23 | 24 | lol_dba will only add indexes for RoR models. yeet_dba looks at every table (including join tables) to add foreign key constraints. 25 | 26 | ## Installation 27 | 28 | Add this line to your application's Gemfile: 29 | 30 | ```ruby 31 | gem 'yeet_dba' 32 | ``` 33 | 34 | And then execute: 35 | 36 | $ bundle 37 | 38 | ## Start here 39 | 40 | ### 1. Find invalid rows 41 | 42 | 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. 43 | 44 | This rake task will scan every column for orphaned rows. 45 | 46 | ``` 47 | $ RAILS_ENV=production rake yeet_dba:find_invalid_columns 48 | ``` 49 | 50 | Sample output: 51 | 52 | ``` 53 | ---RESULTS--- 54 | 55 | 🚨Houston, we have a problem 🚨. We found 1 invalid column. 56 | 57 | -> notifications.primary_image_id 58 | Invalid rows: 83 59 | Foreign table: active_storage_attachments 60 | 61 | This query should return no results: 62 | 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) 63 | 64 | ``` 65 | 66 | If you need to ignore certain tables from being checked simply add a `.yeet_dba.yml` in the Rails.root directory. 67 | 68 | ``` 69 | --- 70 | exclude_tables: 71 | - table_to_be_ignored 72 | ``` 73 | 74 | For a sample configuration file check `.yeet_dba.example.yml`. 75 | 76 | ### 2. Fix invalid rows 77 | 78 | You can either manually repair your data via rails console or direct SQL queries, or you can run a rake task to resolve failures. 79 | 80 | If your rails association requires a value (e.g. `belongs_to :user, required: true`), then we try to delete the row. 81 | 82 | If your rails association says a value is optional, then we try to nullify the value if the schema alls that column to be null. 83 | 84 | If your schema does not allow you to nullify a column, we print a warning. 85 | 86 | ``` 87 | $ RAILS_ENV=production rake yeet_dba:nullify_or_destroy_invalid_rows 88 | ``` 89 | 90 | ### 3a. Add foreign keys via migration 91 | 92 | Now that the database is in a valid state, we can add the foreign keys in a migration. 93 | 94 | ``` 95 | $ RAILS_ENV=production rails g yeet_dba:foreign_key_migration 96 | ``` 97 | 98 | 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.) 99 | 100 | `WARNING - cannot find an association for alternative_housings . supplier_id | suppliers` 101 | 102 | 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. 103 | 104 | `WARNING - cannot find a model for alternative_housings . supplier_id | suppliers` 105 | 106 | Finally, 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. 107 | 108 | `WARNING - orphaned rows alternative_housings . supplier_id | suppliers` 109 | 110 | ### 3b. Add missing foreign keys as a rake task 111 | 112 | 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 on failure. 113 | 114 | ``` 115 | $ RAILS_ENV=production rake yeet_dba:add_foreign_keys 116 | ``` 117 | 118 | Sample output 119 | 120 | ``` 121 | ERROR - users . profile_id failed to add key 122 | ``` 123 | 124 | This rake task is idempotent (safe to run as many times as you need). 125 | 126 | ## Compatibility 127 | 128 | - Rails 5.2 (but it may work with 5.0+) 129 | - Ruby 2.4+ 130 | 131 | ## Road map to v1 132 | 133 | - [x] rspec tests 134 | - [x] add rake task identify all dangling records 135 | - [x] add rake task to automatically nullify or destroy dangling records 136 | - [x] run adding foreign keys as rake task instead of generating a migration 137 | - [x] 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) 138 | 139 | ## Development 140 | 141 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 142 | 143 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 144 | 145 | ## Contributing 146 | 147 | Bug reports and pull requests are welcome on GitHub at https://github.com/kevincolemaninc/yeet_dba. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 148 | 149 | ## License 150 | 151 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 152 | 153 | ## Code of Conduct 154 | 155 | 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). 156 | 157 | ## Logo design attribute 158 | Foreign Key by Ary Prasetyo from the Noun Project 159 | 160 | ## Thanks 161 | 162 | [AvoVietnam - Chat with Vietnamese](https://www.avovietnam.com) 163 | 164 | ## Author 165 | 166 | Kevin Coleman, [https://kcoleman.me/](https://kcoleman.me) 167 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'yeet_dba' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /lib/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinColemanInc/yeet_dba/f8c5aec73f3a9eed28167322fbeceabc6eff2040/lib/.DS_Store -------------------------------------------------------------------------------- /lib/generators/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinColemanInc/yeet_dba/f8c5aec73f3a9eed28167322fbeceabc6eff2040/lib/generators/.DS_Store -------------------------------------------------------------------------------- /lib/generators/yeet_dba/foreign_key_migration_generator.rb: -------------------------------------------------------------------------------- 1 | # require 'generators/yeet_dba/generator_helpers' 2 | 3 | module YeetDba 4 | # Custom scaffolding generator 5 | class ForeignKeyMigrationGenerator < Rails::Generators::Base 6 | include Rails::Generators::Migration 7 | source_root File.expand_path('templates', __dir__) 8 | desc 'Generates migration for adding foreign key constraints.' 9 | 10 | def copy_migration_and_spec_files 11 | migration_template 'add_foreign_keys_yeet_dba.rb', 12 | migration_file, 13 | migration_version: migration_version 14 | end 15 | 16 | private 17 | 18 | def migration_file 19 | File.join('db/migrate', 'add_foreign_keys_yeet_dba.rb') 20 | end 21 | 22 | def migration_version 23 | "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" 24 | end 25 | 26 | def self.next_migration_number(_path) 27 | Time.now.utc.strftime('%Y%m%d%H%M%S%L') 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/generators/yeet_dba/templates/add_foreign_keys_yeet_dba.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddForeignKeysYeetDba < ActiveRecord::Migration<%= migration_version %> 4 | def change 5 | <% ::YeetDba::MissingForeignKeys.foreign_keys.each do |foreign_key| %> 6 | add_foreign_key :<%= foreign_key.table_a %>, 7 | :<%= foreign_key.table_b %>, 8 | column: :<%= foreign_key.column %> 9 | <% end %> 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/yeet_dba.rb: -------------------------------------------------------------------------------- 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' 9 | 10 | module YeetDba 11 | class Error < StandardError; end 12 | end 13 | -------------------------------------------------------------------------------- /lib/yeet_dba/Rakefile: -------------------------------------------------------------------------------- 1 | # lib/Rakefile 2 | require 'yeet_dba' 3 | 4 | path = File.expand_path(__dir__) 5 | Dir.glob("#{path}/tasks/**/*.rake").each { |f| import f } 6 | -------------------------------------------------------------------------------- /lib/yeet_dba/column.rb: -------------------------------------------------------------------------------- 1 | module YeetDba 2 | class Column 3 | attr_accessor :db_column, :table_name, :tables 4 | 5 | def initialize(db_column:, table_name:, tables:) 6 | @db_column = db_column 7 | @table_name = table_name 8 | @tables = tables 9 | end 10 | 11 | def is_association? 12 | db_column.name =~ /_id\z/ 13 | end 14 | 15 | def association_klass 16 | association&.klass 17 | end 18 | 19 | def association_table_name 20 | if association_klass&.ancestors&.include?(ActiveRecord::Base) 21 | association_klass&.table_name 22 | else 23 | tables.detect { |table| table == guessed_table_name } 24 | end 25 | end 26 | 27 | def association_name 28 | db_column.name.gsub(/_id\z/, '') 29 | end 30 | 31 | def model 32 | ActiveRecord::Base.descendants.detect { |c| c.table_name == table_name } 33 | end 34 | 35 | def association 36 | model && model.reflections[association_name] 37 | end 38 | 39 | def polymorphic_association? 40 | association && association.options[:polymorphic] 41 | end 42 | 43 | def foreign_key_exists? 44 | ActiveRecord::Migration.foreign_key_exists?(table_name, column: db_column.name) 45 | end 46 | 47 | def guessed_table_name 48 | @guessed_table_name ||= association_name.pluralize 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/yeet_dba/missing_foreign_keys.rb: -------------------------------------------------------------------------------- 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 - self.ignored_tables 25 | end 26 | 27 | def self.ignored_tables 28 | config['exclude_tables'] || [] 29 | end 30 | 31 | def self.config 32 | @config ||= begin 33 | config_file = Pathname.new(Rails.root).join('.yeet_dba.yml') 34 | 35 | if File.exist?(config_file) 36 | YAML.load(File.read(config_file)) 37 | else 38 | {} 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/yeet_dba/models/foreign_key.rb: -------------------------------------------------------------------------------- 1 | module YeetDba 2 | class ForeignKey 3 | attr_accessor :table_a, 4 | :table_b, 5 | :column 6 | 7 | def initialize(table_a:, table_b:, column:) 8 | @table_a = table_a 9 | @table_b = table_b 10 | @column = column 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/yeet_dba/models/invalid_column.rb: -------------------------------------------------------------------------------- 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 21 | -------------------------------------------------------------------------------- /lib/yeet_dba/railtie.rb: -------------------------------------------------------------------------------- 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 13 | -------------------------------------------------------------------------------- /lib/yeet_dba/table.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/yeet_dba/tasks/add_foreign_keys.rake: -------------------------------------------------------------------------------- 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 21 | -------------------------------------------------------------------------------- /lib/yeet_dba/tasks/bad_data/find_orphaned_rows.rake: -------------------------------------------------------------------------------- 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_name}.#{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 56 | -------------------------------------------------------------------------------- /lib/yeet_dba/verify_data.rb: -------------------------------------------------------------------------------- 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 | def orphaned_rows_count 14 | orphaned_rows.count 15 | end 16 | 17 | def query 18 | orphaned_rows.to_sql 19 | end 20 | 21 | def orphaned_rows 22 | association = column.association 23 | 24 | column_name = column.db_column.name 25 | table_name = column.table_name 26 | association_table = column.association_table_name 27 | model = column.model 28 | 29 | # Check to see there could be rows with bad data 30 | if model 31 | model.joins("left join #{association_table} as association_table on association_table.id = #{table_name}.#{column_name}") 32 | .where.not(column_name => nil) 33 | .where('association_table.id is null') 34 | else 35 | [] 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/yeet_dba/version.rb: -------------------------------------------------------------------------------- 1 | module YeetDba 2 | VERSION = '1.0.3'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/.yeet_dba.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_tables: 3 | - table_to_be_ignored 4 | -------------------------------------------------------------------------------- /spec/fixtures/app/models/company.rb: -------------------------------------------------------------------------------- 1 | class Company < ActiveRecord::Base 2 | has_and_belongs_to_many :users 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/app/models/profile.rb: -------------------------------------------------------------------------------- 1 | class Profile < ActiveRecord::Base 2 | has_one :profile, foreign_key: :owner_id 3 | has_and_belongs_to_many :companies 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_one :profile, foreign_key: :owner_id 3 | has_and_belongs_to_many :companies 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table 'users', force: true do |t| 3 | end 4 | 5 | create_table 'companies', force: true do |t| 6 | t.column 'owned_id', :integer 7 | t.column 'country_id', :integer 8 | end 9 | 10 | add_index :companies, :country_id 11 | 12 | create_table 'profiles', force: true do |t| 13 | t.column 'user_id', :integer 14 | end 15 | 16 | create_table 'companies_users', force: true do |t| 17 | t.column 'user_id', :integer 18 | t.column 'company_id', :integer 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rails/all' 3 | require 'yeet_dba' 4 | 5 | ENV['RAILS_ENV'] ||= 'test' 6 | 7 | ActiveRecord::Base.establish_connection( 8 | adapter: 'sqlite3', 9 | database: ':memory:' 10 | ) 11 | 12 | module Rails 13 | def self.root 14 | Pathname.new('spec/fixtures/') 15 | end 16 | end 17 | Dir.glob("#{Rails.root}/app/models/*.rb").sort.each { |file| require_dependency file } 18 | 19 | ActiveRecord::Schema.verbose = false 20 | load 'fixtures/schema.rb' 21 | 22 | root_dir = File.dirname(__FILE__) 23 | 24 | # add current dir to the load path 25 | $LOAD_PATH.unshift('.') 26 | 27 | RSpec.configure do |config| 28 | # Enable flags like --only-failures and --next-failure 29 | config.example_status_persistence_file_path = '.rspec_status' 30 | 31 | # Disable RSpec exposing methods globally on `Module` and `main` 32 | config.disable_monkey_patching! 33 | 34 | config.expect_with :rspec do |c| 35 | c.syntax = :expect 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/yeet_dba/column_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe YeetDba::Column do 2 | it '#is_association? - true' do 3 | column = YeetDba::Column.new(db_column: OpenStruct.new(name: 'user_id'), 4 | table_name: 'profiles', 5 | tables: 'user_id') 6 | expect(column.is_association?).to be_truthy 7 | end 8 | 9 | it '#is_association? - false' do 10 | column = YeetDba::Column.new(db_column: OpenStruct.new(name: 'kitten'), 11 | table_name: 'profiles', 12 | tables: 'user_id') 13 | expect(column.is_association?).to be_falsey 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/yeet_dba/missing_foreign_keys_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe YeetDba::MissingForeignKeys do 2 | before { YeetDba::MissingForeignKeys.instance_variable_set(:@config, nil) } 3 | 4 | it 'finds missing foreign keys' do 5 | missing_foreign_keys = [%w[user_id profiles users], 6 | %w[user_id companies_users users], 7 | %w[company_id companies_users companies]] 8 | expect(YeetDba::MissingForeignKeys.foreign_keys).not_to be_empty 9 | 10 | YeetDba::MissingForeignKeys.foreign_keys.each_with_index do |f_k, i| 11 | expect(f_k.column).to eq missing_foreign_keys[i][0] 12 | expect(f_k.table_a).to eq missing_foreign_keys[i][1] 13 | expect(f_k.table_b).to eq missing_foreign_keys[i][2] 14 | end 15 | end 16 | 17 | describe '.tables' do 18 | it 'removes ignored tables' do 19 | expect(YeetDba::MissingForeignKeys).to receive(:ignored_tables).and_return(['ar_internal_metadata']) 20 | expect(YeetDba::MissingForeignKeys.tables).not_to include('ar_internal_metadata') 21 | end 22 | end 23 | 24 | describe '.ignored_tables' do 25 | let(:configuration) do 26 | {'exclude_tables' => ['table_to_be_ignored'] } 27 | end 28 | 29 | let(:empty_configuration) do 30 | {'exclude_tables' => nil } 31 | end 32 | 33 | let(:no_configuration) do 34 | {} 35 | end 36 | 37 | it 'returns array of tables' do 38 | expect(YeetDba::MissingForeignKeys).to receive(:config).and_return(configuration) 39 | expect(YeetDba::MissingForeignKeys.ignored_tables).to eq(['table_to_be_ignored']) 40 | end 41 | 42 | it 'returns empty array for empty config' do 43 | expect(YeetDba::MissingForeignKeys).to receive(:config).and_return(empty_configuration) 44 | expect(YeetDba::MissingForeignKeys.ignored_tables).to eq([]) 45 | end 46 | 47 | it 'return empty array for no config' do 48 | expect(YeetDba::MissingForeignKeys).to receive(:config).and_return(no_configuration) 49 | expect(YeetDba::MissingForeignKeys.ignored_tables).to eq([]) 50 | end 51 | end 52 | 53 | describe '.config' do 54 | it 'loads .yeet_dba.yml' do 55 | expect(YeetDba::MissingForeignKeys.config).to eq( 56 | {'exclude_tables' => ['table_to_be_ignored'] } 57 | ) 58 | end 59 | 60 | it 'empty object when no file exists' do 61 | expect(File).to receive(:exist?).with(Pathname.new(Rails.root).join('.yeet_dba.yml')).and_return(false) 62 | expect(YeetDba::MissingForeignKeys.config).to eq({}) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/yeet_dba/models/foreign_key_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe YeetDba::ForeignKey do 2 | it 'can be created' do 3 | expect(YeetDba::ForeignKey.new(table_a: 'users', 4 | table_b: 'profiles', 5 | column: 'user_id')).to be_truthy 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/yeet_dba/models/invalid_column_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe YeetDba::InvalidColumn do 2 | it '#to_s' do 3 | verify_data = YeetDba::VerifyData.new(column: nil) 4 | allow(verify_data).to receive(:orphaned_rows_count) { 3 } 5 | 6 | table_name = 'users' 7 | association_table_name = 'profiles' 8 | column_name = 'user_id' 9 | column = OpenStruct.new(association_table_name: association_table_name, 10 | db_column: OpenStruct.new(name: column_name)) 11 | 12 | invalid_column = YeetDba::InvalidColumn.new(table_name: table_name, 13 | column: column, 14 | verify_data: verify_data) 15 | 16 | out = invalid_column.to_s 17 | expect(out).to include table_name 18 | expect(out).to include association_table_name 19 | expect(out).to include 3.to_s 20 | expect(out).to include column_name 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/yeet_dba/version_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe YeetDba do 2 | it 'has a version number' do 3 | expect(YeetDba::VERSION).not_to be nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /yeet_dba.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'yeet_dba/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'yeet_dba' 7 | spec.version = YeetDba::VERSION 8 | spec.platform = Gem::Platform::RUBY 9 | spec.authors = ['Kevin Coleman'] 10 | spec.email = ['kevin.coleman@sparkstart.io'] 11 | 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 | spec.homepage = 'http://rubygems.org/gems/yeet_dba' 15 | spec.license = 'MIT' 16 | 17 | # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' 18 | # to allow pushing to a single host or delete this section to allow pushing to any host. 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' 23 | else 24 | raise 'RubyGems 2.0 or newer is required to protect against ' \ 25 | 'public gem pushes.' 26 | end 27 | 28 | # Specify which files should be added to the gem when it is released. 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(__dir__)) do 31 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 32 | end 33 | spec.bindir = 'exe' 34 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 35 | spec.require_paths = ['lib'] 36 | 37 | spec.add_development_dependency 'bundler', '~> 2.1' 38 | spec.add_development_dependency 'rake', '~> 13.0' 39 | spec.add_development_dependency 'rspec', '~> 3.0' 40 | 41 | spec.required_ruby_version = '>= 2.4.0' 42 | spec.add_dependency 'actionpack', '>= 3.0', '< 8.0' 43 | spec.add_dependency 'activerecord', '>= 3.0', '< 8.0' 44 | spec.add_dependency 'railties', '>= 3.0', '< 8.0' 45 | end 46 | -------------------------------------------------------------------------------- /yeet_dba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinColemanInc/yeet_dba/f8c5aec73f3a9eed28167322fbeceabc6eff2040/yeet_dba.png --------------------------------------------------------------------------------