Skip to content

Commit db89f72

Browse files
committed
add rails_best_practices/always_add_db_index snippet
1 parent 890c715 commit db89f72

File tree

3 files changed

+207
-0
lines changed

3 files changed

+207
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# frozen_string_literal: true
2+
3+
Synvert::Rewriter.new 'rails_best_practices', 'always_add_db_index' do
4+
description <<~EOS
5+
EOS
6+
7+
call_helper 'rails/parse'
8+
rails_tables = load_data :rails_tables
9+
rails_models = load_data :rails_models
10+
11+
configure(parser: Synvert::PRISM_PARSER)
12+
13+
helper_method :foreign_key_column do |association|
14+
association[:foreign_key]&.to_s || association[:name].foreign_key
15+
end
16+
17+
helper_method :foreign_type_column do |association|
18+
association[:foreign_type]&.to_s || (association[:name].demodulize + '_type').underscore
19+
end
20+
21+
within_files 'db/schema.rb' do
22+
rails_tables.each do |table_name, table_info|
23+
belongs_to_associations = rails_models[:associations].select do |association|
24+
# find all belongs_to associations
25+
association[:class_name].tableize == table_name || association[:type] == 'belongs_to'
26+
end
27+
polymorphic_belongs_to_associations = belongs_to_associations.select do |association|
28+
next false unless association[:polymorphic]
29+
30+
# find all belongs_to associations without db index
31+
table_info[:columns].find { |column| column[:name] == foreign_key_column(association) } &&
32+
table_info[:columns].find { |column| column[:name] == foreign_type_column(association) } &&
33+
!table_info[:indices].find { |index| index[:columns].first == foreign_type_column(association) && index[:columns].second == foreign_key_column(association) }
34+
end
35+
general_belongs_to_associations = belongs_to_associations.select do |association|
36+
!association[:polymorphic] &&
37+
# find all belongs_to associations without db index
38+
table_info[:columns].find { |column| column[:name] == foreign_key_column(association) } &&
39+
!table_info[:indices].find { |index| index[:columns].first == foreign_key_column(association) }
40+
end
41+
42+
polymorphic_belongs_to_associations.each do |association|
43+
find_node ".call_node[receiver=nil][name=create_table][arguments!=nil][arguments.arguments.first='#{table_name}']
44+
.call_node[receiver=t][arguments!=nil][arguments.arguments.first='#{association[:name].foreign_key}']" do
45+
warn "always add db index #{table_name} => [\"#{foreign_type_column(association)}\", \"#{foreign_key_column(association)}\"]"
46+
end
47+
end
48+
49+
general_belongs_to_associations.each do |association|
50+
find_node ".call_node[receiver=nil][name=create_table][arguments!=nil][arguments.arguments.first='#{table_name}']
51+
.call_node[receiver=t][arguments!=nil][arguments.arguments.first='#{foreign_key_column(association)}']" do
52+
warn "always add db index #{table_name} => [\"#{foreign_key_column(association)}\"]"
53+
end
54+
end
55+
end
56+
end
57+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
RSpec.describe 'Rails always add db index' do
6+
before { load_helpers(%w[helpers/parse_rails]) }
7+
8+
context 'index exists' do
9+
let(:rewriter_name) { 'rails_best_practices/always_add_db_index' }
10+
let(:file_paths) { %w[db/schema.rb app/models/comment.rb] }
11+
let(:test_contents) { [<<~EOF.strip, <<~EOF.strip] }
12+
ActiveRecord::Schema[7.1].define(version: 2024_04_22_081242) do
13+
enable_extension "plpgsql"
14+
15+
create_table "comments", force: :cascade do |t|
16+
t.string "content", null: false
17+
t.bigint "post_id", null: false
18+
t.integer "user_id", null: false
19+
t.index ["post_id"], name: "index_comments_on_post_id"
20+
t.index ["user_id"], name: "index_comments_on_user_id"
21+
end
22+
end
23+
EOF
24+
class Comment < ApplicationRecord
25+
belongs_to :post
26+
belongs_to :user
27+
end
28+
EOF
29+
let(:warnings) { [] }
30+
31+
include_examples 'warnable'
32+
end
33+
34+
context 'index is missing' do
35+
let(:rewriter_name) { 'rails_best_practices/always_add_db_index' }
36+
let(:file_paths) { %w[db/schema.rb app/models/comment.rb] }
37+
let(:test_contents) { [<<~EOF.strip, <<~EOF.strip] }
38+
ActiveRecord::Schema[7.1].define(version: 2024_04_22_081242) do
39+
enable_extension "plpgsql"
40+
41+
create_table "comments", force: :cascade do |t|
42+
t.string "content", null: false
43+
t.bigint "post_id", null: false
44+
t.integer "user_id", null: false
45+
end
46+
end
47+
EOF
48+
class Comment < ApplicationRecord
49+
belongs_to :post
50+
belongs_to :user
51+
end
52+
EOF
53+
let(:warnings) { [
54+
'/db/schema.rb#6: always add db index comments => ["post_id"]',
55+
'/db/schema.rb#7: always add db index comments => ["user_id"]'
56+
] }
57+
58+
include_examples 'warnable'
59+
end
60+
61+
context 'polymorphic index exists' do
62+
let(:rewriter_name) { 'rails_best_practices/always_add_db_index' }
63+
let(:file_paths) { %w[db/schema.rb app/models/picture.rb] }
64+
let(:test_contents) { [<<~EOF.strip, <<~EOF.strip] }
65+
ActiveRecord::Schema[7.1].define(version: 2024_04_22_081242) do
66+
enable_extension "plpgsql"
67+
68+
create_table "pictures", force: :cascade do |t|
69+
t.string "name", null: false
70+
t.bigint "imageable_id", null: false
71+
t.string "imageable_type", null: false
72+
t.index ["imageable_type", "imageable_id"], name: "index_pictures_on_imageable_type_and_imageable_id"
73+
end
74+
end
75+
EOF
76+
class Picture < ApplicationRecord
77+
belongs_to :imageable, polymorphic: true
78+
end
79+
EOF
80+
let(:warnings) { [] }
81+
82+
include_examples 'warnable'
83+
end
84+
85+
context 'polymorphic index is missing' do
86+
let(:rewriter_name) { 'rails_best_practices/always_add_db_index' }
87+
let(:file_paths) { %w[db/schema.rb app/models/picture.rb] }
88+
let(:test_contents) { [<<~EOF.strip, <<~EOF.strip] }
89+
ActiveRecord::Schema[7.1].define(version: 2024_04_22_081242) do
90+
enable_extension "plpgsql"
91+
92+
create_table "pictures", force: :cascade do |t|
93+
t.string "name", null: false
94+
t.bigint "imageable_id", null: false
95+
t.string "imageable_type", null: false
96+
end
97+
end
98+
EOF
99+
class Picture < ApplicationRecord
100+
belongs_to :imageable, polymorphic: true
101+
end
102+
EOF
103+
let(:warnings) { [
104+
'/db/schema.rb#6: always add db index pictures => ["imageable_type", "imageable_id"]',
105+
] }
106+
107+
include_examples 'warnable'
108+
end
109+
110+
context 'foreign_key option index is missing' do
111+
let(:rewriter_name) { 'rails_best_practices/always_add_db_index' }
112+
let(:file_paths) { %w[db/schema.rb app/models/comment.rb] }
113+
let(:test_contents) { [<<~EOF.strip, <<~EOF.strip] }
114+
ActiveRecord::Schema[7.1].define(version: 2024_04_22_081242) do
115+
enable_extension "plpgsql"
116+
117+
create_table "comments", force: :cascade do |t|
118+
t.string "content", null: false
119+
t.integer "user_id", null: false
120+
end
121+
end
122+
EOF
123+
class Comment < ApplicationRecord
124+
belongs_to :commentor, foreign_key: :user_id
125+
end
126+
EOF
127+
let(:warnings) { [
128+
'/db/schema.rb#6: always add db index comments => ["user_id"]'
129+
] }
130+
131+
include_examples 'warnable'
132+
end
133+
end

spec/support/warnable.rb

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
shared_examples 'warnable' do
4+
# it needs to define rewriter_name, fake_file_path (optional), test_content, warnings
5+
let!(:rewriter) { load_snippet(rewriter_name) }
6+
7+
describe 'with fakefs', fakefs: true do
8+
it 'converts' do
9+
file_paths.each_with_index do |file_path, index|
10+
FileUtils.mkdir_p(File.dirname(file_path))
11+
File.write(file_path, test_contents[index])
12+
end
13+
rewriter.process
14+
expect(rewriter.warnings.map(&:message)).to eq(warnings)
15+
end
16+
end
17+
end

0 commit comments

Comments
 (0)