Skip to content

Commit d7154cb

Browse files
committed
add Foreign Key constraints, enforce foreign key coverage via a test
1 parent ec42a53 commit d7154cb

9 files changed

+101
-31
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddForeignKeyBadgesUsers < ActiveRecord::Migration
2+
def change
3+
add_foreign_key :badges, :users
4+
end
5+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddForeignKeyCommentsUsers < ActiveRecord::Migration
2+
def change
3+
add_foreign_key :comments, :users
4+
end
5+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddForeignKeyCommentsProtips < ActiveRecord::Migration
2+
def change
3+
add_foreign_key :comments, :protips
4+
end
5+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddForeignKeyLikesUsers < ActiveRecord::Migration
2+
def change
3+
add_foreign_key :likes, :users
4+
end
5+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddForeignKeyProtipsUsers < ActiveRecord::Migration
2+
def change
3+
add_foreign_key :protips, :users
4+
end
5+
end

db/schema.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
#
1212
# It's strongly recommended that you check this file into your version control system.
1313

14-
ActiveRecord::Schema.define(version: 20160219190140) do
14+
ActiveRecord::Schema.define(version: 20160220185329) do
1515

1616
# These are extensions that must be enabled in order to support this database
1717
enable_extension "plpgsql"
@@ -111,4 +111,9 @@
111111
add_index "users", ["skills"], name: "index_users_on_skills", using: :gin
112112
add_index "users", ["username"], name: "index_users_on_username", unique: true, using: :btree
113113

114+
add_foreign_key "badges", "users"
115+
add_foreign_key "comments", "protips"
116+
add_foreign_key "comments", "users"
117+
add_foreign_key "likes", "users"
118+
add_foreign_key "protips", "users"
114119
end

spec/helpers/badges_helper_spec.rb

Lines changed: 0 additions & 15 deletions
This file was deleted.

spec/helpers/comments_helper_spec.rb

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
require 'rails_helper'
2+
3+
describe 'Foreign Key coverage' do
4+
# https://viget.com/extend/identifying-foreign-key-dependencies-from-activerecordbase-classes
5+
Association = Struct.new(:association) do
6+
delegate :foreign_key, to: :association
7+
8+
def klass
9+
association.klass unless polymorphic?
10+
end
11+
12+
def name
13+
association.options[:class_name] || association.name
14+
end
15+
16+
def polymorphic?
17+
!!association.options[:polymorphic]
18+
end
19+
20+
def polymorphic_dependencies
21+
return [] unless polymorphic?
22+
@polymorphic_dependencies ||= ActiveRecord::Base.subclasses.select { |model| polymorphic_match? model }
23+
end
24+
25+
def polymorphic_match?(model)
26+
model.reflect_on_all_associations(:has_many).any? do |has_many_association|
27+
has_many_association.options[:as] == association.name
28+
end
29+
end
30+
31+
def dependencies(include_polymorphic=true)
32+
polymorphic? ? (include_polymorphic ? polymorphic_dependencies : []) : Array(klass)
33+
end
34+
35+
def polymorphic_type
36+
association.foreign_type if polymorphic?
37+
end
38+
end
39+
40+
it 'is complete' do
41+
# to ignore an entire table, add the table name symbol as the key with value :all
42+
# to ignore specific columns, add the table name symbol as the key with an array of column name symbols as the value
43+
ignore = {
44+
users_roles: %i( left_side_id ),
45+
version_associations: :all # PaperTrail's models declare some relations to missing tables, so this is necessary even though we have no version_associations table.
46+
}
47+
48+
abc = ActiveRecord::Base.connection
49+
50+
Rails.application.eager_load!
51+
52+
# enumerate active record models
53+
ActiveRecord::Base.subclasses.each do |model|
54+
ignore_columns = ignore.fetch(model.table_name.to_sym, [])
55+
next if ignore_columns == :all
56+
57+
existing_foreign_keys = abc.foreign_keys(model.table_name).each_with_object([]) do |foreign_key, keys|
58+
keys.push "#{ foreign_key.from_table }.#{ foreign_key.options[:column] } => #{ foreign_key.to_table }.#{ foreign_key.options[:primary_key] }"
59+
end
60+
61+
model.reflect_on_all_associations(:belongs_to).each do |association|
62+
Association.new(association).dependencies(false).each do |dependency|
63+
next if ignore_columns.include?(association.foreign_key.to_sym)
64+
key = "#{ model.table_name }.#{ association.foreign_key } => #{ dependency.table_name }.#{ dependency.primary_key }"
65+
expect(existing_foreign_keys).to include(key), "Missing foreign key #{ key }"
66+
end
67+
end
68+
end
69+
end
70+
end

0 commit comments

Comments
 (0)