Skip to content

Commit 0e94a7d

Browse files
authored
missing_foreign_keys: use belongs_to to identify foreign keys
1 parent eb16cde commit 0e94a7d

File tree

3 files changed

+54
-41
lines changed

3 files changed

+54
-41
lines changed

lib/active_record_doctor/config/default.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
detector :missing_foreign_keys,
3838
enabled: true,
39-
ignore_tables: [],
39+
ignore_models: [],
4040
ignore_columns: []
4141

4242
detector :missing_non_null_constraint,

lib/active_record_doctor/detectors/missing_foreign_keys.rb

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ module Detectors
77
class MissingForeignKeys < Base # :nodoc:
88
@description = "detect foreign-key-like columns lacking an actual foreign key constraint"
99
@config = {
10-
ignore_tables: {
11-
description: "tables whose columns should not be checked",
10+
ignore_models: {
11+
description: "models whose columns should not be checked",
1212
global: true
1313
},
1414
ignore_columns: {
@@ -23,42 +23,18 @@ def message(table:, column:)
2323
end
2424

2525
def detect
26-
each_table(except: config(:ignore_tables)) do |table|
27-
each_column(table, except: config(:ignore_columns)) do |column|
28-
# We need to skip polymorphic associations as they can reference
29-
# multiple tables but a foreign key constraint can reference
30-
# a single predefined table.
31-
next unless looks_like_foreign_key?(column)
32-
next if foreign_key?(table, column)
33-
next if polymorphic_foreign_key?(table, column)
34-
next if model_destroyed_async?(table, column)
26+
each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
27+
foreign_keys = connection.foreign_keys(model.table_name)
28+
foreign_key_columns = foreign_keys.map { |key| key.options[:column] }
3529

36-
problem!(table: table, column: column.name)
37-
end
38-
end
39-
end
40-
41-
def foreign_key?(table, column)
42-
connection.foreign_keys(table).any? do |foreign_key|
43-
foreign_key.options[:column] == column.name
44-
end
45-
end
30+
each_association(model, type: :belongs_to) do |association|
31+
next if ignored?(association.foreign_key, config(:ignore_columns))
32+
next if association.options[:polymorphic]
4633

47-
def polymorphic_foreign_key?(table, column)
48-
type_column_name = column.name.sub(/_id\Z/, "_type")
49-
connection.columns(table).any? do |another_column|
50-
another_column.name == type_column_name
51-
end
52-
end
34+
has_foreign_key = foreign_key_columns.include?(association.foreign_key)
35+
next if has_foreign_key
5336

54-
def model_destroyed_async?(table, column)
55-
# Check if there are any models having `has_many ..., dependent: :destroy_async`
56-
# referencing the specified table.
57-
models.any? do |model|
58-
model.reflect_on_all_associations(:has_many).any? do |reflection|
59-
reflection.options[:dependent] == :destroy_async &&
60-
reflection.foreign_key == column.name &&
61-
reflection.klass.table_name == table
37+
problem!(table: model.table_name, column: association.foreign_key)
6238
end
6339
end
6440
end

test/active_record_doctor/detectors/missing_foreign_keys_test.rb

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ def test_missing_foreign_key_is_reported
55
Context.create_table(:companies)
66
Context.create_table(:users) do |t|
77
t.references :company, foreign_key: false
8+
end.define_model do
9+
belongs_to :company
810
end
911

1012
assert_problems(<<~OUTPUT)
@@ -16,14 +18,29 @@ def test_present_foreign_key_is_not_reported
1618
Context.create_table(:companies)
1719
Context.create_table(:users) do |t|
1820
t.references :company, foreign_key: true
21+
end.define_model do
22+
belongs_to :company
1923
end
2024

2125
refute_problems
2226
end
2327

24-
def test_non_integer_missing_foreign_key_is_reported
28+
def test_missing_foreign_key_on_abstract_class_is_not_reported
29+
Context.create_table(:companies)
30+
Context.create_table(:users) do |t|
31+
t.references :company, foreign_key: false
32+
end.define_model do
33+
self.abstract_class = true
34+
belongs_to :company
35+
end
36+
37+
refute_problems
38+
end
39+
40+
def test_missing_foreign_key_without_belongs_to_is_not_reported
41+
Context.create_table(:companies)
2542
Context.create_table(:users) do |t|
26-
t.string :external_id
43+
t.references :company, foreign_key: false
2744
end
2845

2946
refute_problems
@@ -32,8 +49,13 @@ def test_non_integer_missing_foreign_key_is_reported
3249
def test_uuid_missing_foreign_key_is_reported
3350
require_uuid_column_type!
3451

52+
Context.create_table(:companies) do |t|
53+
t.uuid :id, default: "gen_random_uuid()"
54+
end
3555
Context.create_table(:users) do |t|
36-
t.uuid :company_id
56+
t.references :company, type: :uuid, foreign_key: false
57+
end.define_model do
58+
belongs_to :company
3759
end
3860

3961
assert_problems(<<~OUTPUT)
@@ -60,12 +82,14 @@ def test_config_ignore_models
6082
Context.create_table(:companies)
6183
Context.create_table(:users) do |t|
6284
t.references :company, foreign_key: false
85+
end.define_model do
86+
belongs_to :company
6387
end
6488

6589
config_file(<<-CONFIG)
6690
ActiveRecordDoctor.configure do |config|
6791
config.detector :missing_foreign_keys,
68-
ignore_tables: ["users"]
92+
ignore_models: [Context::User]
6993
end
7094
CONFIG
7195

@@ -76,11 +100,13 @@ def test_global_ignore_models
76100
Context.create_table(:companies)
77101
Context.create_table(:users) do |t|
78102
t.references :company, foreign_key: false
103+
end.define_model do
104+
belongs_to :company
79105
end
80106

81107
config_file(<<-CONFIG)
82108
ActiveRecordDoctor.configure do |config|
83-
config.global :ignore_tables, ["users"]
109+
config.global :ignore_models, [Context::User]
84110
end
85111
CONFIG
86112

@@ -102,4 +128,15 @@ def test_config_ignore_columns
102128

103129
refute_problems
104130
end
131+
132+
def test_polymorphic_association_is_not_reported
133+
Context.create_table(:companies)
134+
Context.create_table(:users) do |t|
135+
t.references :commentable, polymorphic: true, foreign_key: false
136+
end.define_model do
137+
belongs_to :commentable, polymorphic: true
138+
end
139+
140+
refute_problems
141+
end
105142
end

0 commit comments

Comments
 (0)