Skip to content

Commit 43b72b6

Browse files
committedNov 15, 2018
Check foreign keys for has many associations
1 parent 7053d89 commit 43b72b6

File tree

10 files changed

+119
-0
lines changed

10 files changed

+119
-0
lines changed
 

‎lib/dbcop.rb

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
require 'dbcop/cop/belongs_to/foreign_key'
1313
require 'dbcop/cop/validation/presence'
1414
require 'dbcop/cop/validation/inclusion'
15+
require 'dbcop/cop/has_many/foreign_key'
1516

1617
# Base module for the gem
1718
module Dbcop

‎lib/dbcop/cop/accordance/primary_key.rb

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ def call
1515
private
1616

1717
def valid?
18+
return true unless connection.table_exists?(model.table_name)
19+
1820
@valid ||= !connection.primary_keys(model.table_name).map! do |pk|
1921
column_valid?(pk)
2022
end.include?(false)

‎lib/dbcop/cop/has_many/foreign_key.rb

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../cop'
4+
5+
module Dbcop
6+
module HasMany
7+
# Checks foreign key presence to the parent table of belongs_to association
8+
class ForeignKey < Cop
9+
def call
10+
results = associations.map do |association|
11+
valid?(association)
12+
end
13+
14+
results.none?(&:!)
15+
end
16+
17+
private
18+
19+
def valid?(association)
20+
success = foreign_key?(association)
21+
unless success
22+
from_table = association.class_name.constantize.table_name
23+
log("has_many #{association.name} but has no foreign key from #{from_table}.id")
24+
end
25+
progress(success, 'F')
26+
27+
success
28+
rescue NameError
29+
log("Error processing #{model.name}.#{association.name}")
30+
end
31+
32+
def associations
33+
model
34+
._reflections
35+
.values
36+
.select { |association| association.is_a?(ActiveRecord::Reflection::HasManyReflection) }
37+
.reject(&:polymorphic?)
38+
end
39+
40+
def foreign_key?(association)
41+
to_table = model.table_name
42+
connection.foreign_keys(association.class_name.constantize.table_name).any? do |foreign_key|
43+
foreign_key.to_table == to_table && foreign_key.options.fetch(:primary_key) == 'id'
44+
end
45+
end
46+
end
47+
end
48+
end

‎lib/dbcop/cop/validation/inclusion.rb

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ def validators
3535
end
3636

3737
def find_column(attribute)
38+
return nil unless connection.table_exists?(model.table_name)
39+
3840
connection
3941
.columns(model.table_name)
4042
.find { |col| col.name == attribute.to_s }

‎spec/internal/app/models/has_many.rb

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module HasMany
4+
def self.table_name_prefix
5+
'has_many_'
6+
end
7+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module HasMany
4+
class Address < ApplicationRecord
5+
has_many :phones, class_name: 'HasMany::Phone'
6+
end
7+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
module HasMany
4+
class Phone < ApplicationRecord
5+
end
6+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module HasMany
4+
class User < ApplicationRecord
5+
has_many :addresses, class_name: 'HasMany::Address'
6+
end
7+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
class CreateHasManyTables < ActiveRecord::Migration[5.2]
4+
def change
5+
create_table :has_many_users, &:timestamps
6+
7+
create_table :has_many_addresses do |t|
8+
t.references :user, foreign_key: { to_table: :has_many_users }
9+
t.text :data
10+
11+
t.timestamps
12+
end
13+
14+
create_table :has_many_phone do |t|
15+
t.references :address, foreign_key: false
16+
end
17+
end
18+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
RSpec.describe Dbcop::HasMany::ForeignKey do
6+
describe '#call' do
7+
context 'when there is fereign key to the parent table' do
8+
let(:result) do
9+
described_class.new(HasMany::User).call
10+
end
11+
12+
it { expect(result).to be_truthy }
13+
end
14+
15+
context 'when there is no foreign key to the parent table' do
16+
let(:result) { described_class.new(HasMany::Address).call }
17+
18+
it { expect(result).to be_falsey }
19+
end
20+
end
21+
end

0 commit comments

Comments
 (0)
Failed to load comments.