Skip to content

Commit 2755369

Browse files
committed
Reduce the number of queries by caching schema info
1 parent ace206c commit 2755369

16 files changed

+137
-66
lines changed

lib/active_record_doctor.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
require "active_record_doctor/errors"
2020
require "active_record_doctor/help"
2121
require "active_record_doctor/runner"
22+
require "active_record_doctor/caching_schema_inspector"
2223
require "active_record_doctor/version"
2324
require "active_record_doctor/config"
2425
require "active_record_doctor/config/loader"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveRecordDoctor
4+
class CachingSchemaInspector # :nodoc:
5+
def initialize(connection)
6+
@connection = connection
7+
@indexes = {}
8+
@foreign_keys = {}
9+
@check_constraints = {}
10+
end
11+
12+
def tables
13+
@tables ||=
14+
if ActiveRecord::VERSION::STRING >= "5.1"
15+
@connection.tables
16+
else
17+
@connection.data_sources
18+
end
19+
end
20+
21+
def primary_key(table_name)
22+
@connection.schema_cache.primary_keys(table_name)
23+
end
24+
25+
def columns(table_name)
26+
@connection.schema_cache.columns(table_name)
27+
end
28+
29+
def indexes(table_name)
30+
@indexes.fetch(table_name) do
31+
@indexes[table_name] = @connection.indexes(table_name)
32+
end
33+
end
34+
35+
def foreign_keys(table_name)
36+
@foreign_keys.fetch(table_name) do
37+
@foreign_keys[table_name] = @connection.foreign_keys(table_name)
38+
end
39+
end
40+
41+
def check_constraints(table_name)
42+
@check_constraints.fetch(table_name) do
43+
# ActiveRecord 6.1+
44+
if @connection.respond_to?(:supports_check_constraints?) && @connection.supports_check_constraints?
45+
@connection.check_constraints(table_name).select(&:validated?).map(&:expression)
46+
elsif postgresql?
47+
definitions =
48+
@connection.select_values(<<-SQL)
49+
SELECT pg_get_constraintdef(oid, true)
50+
FROM pg_constraint
51+
WHERE contype = 'c'
52+
AND convalidated
53+
AND conrelid = #{@connection.quote(table_name)}::regclass
54+
SQL
55+
56+
definitions.map { |definition| definition[/CHECK \((.+)\)/m, 1] }
57+
else
58+
# We don't support this Rails/database combination yet.
59+
[]
60+
end
61+
end
62+
end
63+
64+
def views
65+
@views ||=
66+
if @connection.respond_to?(:views)
67+
@connection.views
68+
elsif postgresql?
69+
@connection.select_values(<<-SQL)
70+
SELECT relname FROM pg_class WHERE relkind IN ('m', 'v')
71+
SQL
72+
elsif @connection.adapter_name == "Mysql2"
73+
@connection.select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'")
74+
else
75+
# We don't support this Rails/database combination yet.
76+
[]
77+
end
78+
end
79+
80+
private
81+
82+
def postgresql?
83+
["PostgreSQL", "PostGIS"].include?(@connection.adapter_name)
84+
end
85+
end
86+
end

lib/active_record_doctor/detectors/base.rb

Lines changed: 17 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ class Base
1313
class << self
1414
attr_reader :description
1515

16-
def run(config, io)
17-
new(config, io).run
16+
def run(config, schema_inspector, io)
17+
new(config, schema_inspector, io).run
1818
end
1919

2020
def underscored_name
@@ -38,9 +38,10 @@ def locals_and_globals
3838
end
3939
end
4040

41-
def initialize(config, io)
41+
def initialize(config, schema_inspector, io)
4242
@problems = []
4343
@config = config
44+
@schema_inspector = schema_inspector
4445
@io = io
4546
end
4647

@@ -92,7 +93,7 @@ def connection
9293
end
9394

9495
def indexes(table_name, except: [])
95-
connection.indexes(table_name).reject do |index|
96+
@schema_inspector.indexes(table_name).reject do |index|
9697
except.include?(index.name)
9798
end
9899
end
@@ -111,32 +112,22 @@ def tables(except: [])
111112
end
112113

113114
def primary_key(table_name)
114-
primary_key_name = connection.primary_key(table_name)
115+
primary_key_name = @schema_inspector.primary_key(table_name)
115116
return nil if primary_key_name.nil?
116117

117118
column(table_name, primary_key_name)
118119
end
119120

120121
def column(table_name, column_name)
121-
connection.columns(table_name).find { |column| column.name == column_name }
122+
columns(table_name).find { |column| column.name == column_name }
123+
end
124+
125+
def columns(table_name)
126+
@schema_inspector.columns(table_name)
122127
end
123128

124129
def views
125-
@views ||=
126-
if connection.respond_to?(:views)
127-
connection.views
128-
elsif postgresql?
129-
ActiveRecord::Base.connection.select_values(<<-SQL)
130-
SELECT relname FROM pg_class WHERE relkind IN ('m', 'v')
131-
SQL
132-
elsif connection.adapter_name == "Mysql2"
133-
ActiveRecord::Base.connection.select_values(<<-SQL)
134-
SHOW FULL TABLES WHERE table_type = 'VIEW'
135-
SQL
136-
else
137-
# We don't support this Rails/database combination yet.
138-
[]
139-
end
130+
@schema_inspector.views
140131
end
141132

142133
def not_null_check_constraint_exists?(table, column)
@@ -146,25 +137,12 @@ def not_null_check_constraint_exists?(table, column)
146137
end
147138
end
148139

140+
def foreign_keys(table_name)
141+
@schema_inspector.foreign_keys(table_name)
142+
end
143+
149144
def check_constraints(table_name)
150-
# ActiveRecord 6.1+
151-
if connection.respond_to?(:supports_check_constraints?) && connection.supports_check_constraints?
152-
connection.check_constraints(table_name).select(&:validated?).map(&:expression)
153-
elsif postgresql?
154-
definitions =
155-
connection.select_values(<<-SQL)
156-
SELECT pg_get_constraintdef(oid, true)
157-
FROM pg_constraint
158-
WHERE contype = 'c'
159-
AND convalidated
160-
AND conrelid = #{connection.quote(table_name)}::regclass
161-
SQL
162-
163-
definitions.map { |definition| definition[/CHECK \((.+)\)/m, 1] }
164-
else
165-
# We don't support this Rails/database combination yet.
166-
[]
167-
end
145+
@schema_inspector.check_constraints(table_name)
168146
end
169147

170148
def models(except: [])
@@ -176,10 +154,6 @@ def models(except: [])
176154
def underscored_name
177155
self.class.underscored_name
178156
end
179-
180-
def postgresql?
181-
["PostgreSQL", "PostGIS"].include?(connection.adapter_name)
182-
end
183157
end
184158
end
185159
end

lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def detect
2828
models(except: config(:ignore_models)).each do |model|
2929
next unless model.table_exists?
3030

31-
connection.columns(model.table_name).each do |column|
31+
columns(model.table_name).each do |column|
3232
next if config(:ignore_attributes).include?("#{model.name}.#{column.name}")
3333
next unless column.type == :boolean
3434
next unless has_presence_validator?(model, column)

lib/active_record_doctor/detectors/incorrect_dependent_option.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def dependent_models(model)
129129
end
130130

131131
def foreign_key(from_table, to_table)
132-
connection.foreign_keys(from_table).find do |foreign_key|
132+
foreign_keys(from_table).find do |foreign_key|
133133
foreign_key.to_table == to_table
134134
end
135135
end

lib/active_record_doctor/detectors/incorrect_length_validation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def detect
3434
models(except: config(:ignore_models)).each do |model|
3535
next unless model.table_exists?
3636

37-
connection.columns(model.table_name).each do |column|
37+
columns(model.table_name).each do |column|
3838
next if config(:ignore_attributes).include?("#{model.name}.#{column.name}")
3939
next if ![:string, :text].include?(column.type)
4040

lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def message(table:, column:)
2626

2727
def detect
2828
tables(except: config(:ignore_tables)).each do |table|
29-
connection.foreign_keys(table).each do |foreign_key|
29+
foreign_keys(table).each do |foreign_key|
3030
from_column = column(table, foreign_key.column)
3131

3232
next if config(:ignore_columns).include?("#{table}.#{from_column.name}")

lib/active_record_doctor/detectors/missing_foreign_keys.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def message(table:, column:)
2424

2525
def detect
2626
tables(except: config(:ignore_tables)).each do |table|
27-
connection.columns(table).each do |column|
27+
columns(table).each do |column|
2828
next if config(:ignore_columns).include?("#{table}.#{column.name}")
2929

3030
# We need to skip polymorphic associations as they can reference
@@ -44,14 +44,14 @@ def named_like_foreign_key?(column)
4444
end
4545

4646
def foreign_key?(table, column)
47-
connection.foreign_keys(table).any? do |foreign_key|
47+
foreign_keys(table).any? do |foreign_key|
4848
foreign_key.options[:column] == column.name
4949
end
5050
end
5151

5252
def polymorphic_foreign_key?(table, column)
5353
type_column_name = column.name.sub(/_id\Z/, "_type")
54-
connection.columns(table).any? do |another_column|
54+
columns(table).any? do |another_column|
5555
another_column.name == type_column_name
5656
end
5757
end

lib/active_record_doctor/detectors/missing_non_null_constraint.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def detect
3333
model.abstract_class? || sti_base_model?(model)
3434
end
3535

36-
connection.columns(table).each do |column|
36+
columns(table).each do |column|
3737
next if config(:ignore_columns).include?("#{table}.#{column.name}")
3838
next if !column.null
3939
next if !concrete_models.all? { |model| non_null_needed?(model, column) }

lib/active_record_doctor/detectors/missing_presence_validation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def detect
2626
models(except: config(:ignore_models)).each do |model|
2727
next unless model.table_exists?
2828

29-
connection.columns(model.table_name).each do |column|
29+
columns(model.table_name).each do |column|
3030
next unless validator_needed?(model, column)
3131
next if validator_present?(model, column)
3232
next if config(:ignore_attributes).include?("#{model}.#{column.name}")

0 commit comments

Comments
 (0)