Skip to content

Commit 2a3558b

Browse files
authored
Add support for checking for pending migrations and running them (#503)
Add the ability to check for pending migrations and run them through the Rails runner client. For checking if something is pending, we use the Rails API directly, which raises if there are any pending migrations. For running them, we can't do it in process. The code that runs migrations is not designed to be invoked repeatedly and uses `load`, which means that we could end up with a memory bloat. I used `Open3.capture2` to run in a child process and grab the results.
1 parent eb0c3a9 commit 2a3558b

File tree

3 files changed

+70
-0
lines changed

3 files changed

+70
-0
lines changed

lib/ruby_lsp/ruby_lsp_rails/runner_client.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,29 @@ def delegate_notification(server_addon_name:, request_name:, **params)
187187
)
188188
end
189189

190+
sig { returns(T.nilable(String)) }
191+
def pending_migrations_message
192+
response = make_request("pending_migrations_message")
193+
response[:pending_migrations_message] if response
194+
rescue IncompleteMessageError
195+
log_message(
196+
"Ruby LSP Rails failed when checking for pending migrations",
197+
type: RubyLsp::Constant::MessageType::ERROR,
198+
)
199+
nil
200+
end
201+
202+
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
203+
def run_migrations
204+
make_request("run_migrations")
205+
rescue IncompleteMessageError
206+
log_message(
207+
"Ruby LSP Rails failed to run migrations",
208+
type: RubyLsp::Constant::MessageType::ERROR,
209+
)
210+
nil
211+
end
212+
190213
# Delegates a request to a server add-on
191214
sig do
192215
params(

lib/ruby_lsp/ruby_lsp_rails/server.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# frozen_string_literal: true
33

44
require "json"
5+
require "open3"
56

67
# NOTE: We should avoid printing to stderr since it causes problems. We never read the standard error pipe from the
78
# client, so it will become full and eventually hang or crash. Instead, return a response with an `error` key.
@@ -109,6 +110,10 @@ def execute(request, params)
109110
send_message(resolve_database_info_from_model(params.fetch(:name)))
110111
when "association_target_location"
111112
send_message(resolve_association_target(params))
113+
when "pending_migrations_message"
114+
send_message({ result: { pending_migrations_message: pending_migrations_message } })
115+
when "run_migrations"
116+
send_message({ result: run_migrations })
112117
when "reload"
113118
::Rails.application.reloader.reload!
114119
when "route_location"
@@ -252,6 +257,26 @@ def active_record_model?(const)
252257
!const.abstract_class?
253258
)
254259
end
260+
261+
def pending_migrations_message
262+
return unless defined?(ActiveRecord)
263+
264+
ActiveRecord::Migration.check_all_pending!
265+
nil
266+
rescue ActiveRecord::PendingMigrationError => e
267+
e.message
268+
end
269+
270+
def run_migrations
271+
# Running migrations invokes `load` which will repeatedly load the same files. It's not designed to be invoked
272+
# multiple times within the same process. To avoid any memory bloat, we run migrations in a separate process
273+
stdout, status = Open3.capture2(
274+
{ "VERBOSE" => "true" },
275+
"bundle exec rails db:migrate",
276+
)
277+
278+
{ message: stdout, status: status.exitstatus }
279+
end
255280
end
256281
end
257282
end

test/ruby_lsp_rails/server_test.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,28 @@ def resolve_route_info(requirements)
176176
assert_equal("Hello\n", stderr)
177177
end
178178

179+
test "checking for pending migrations" do
180+
capture_subprocess_io do
181+
system("bundle exec rails g migration CreateStudents name:string")
182+
end
183+
184+
@server.execute("pending_migrations_message", {})
185+
message = response.dig(:result, :pending_migrations_message)
186+
assert_match("You have 1 pending migration", message)
187+
assert_match(%r{db/migrate/[\d]+_create_students\.rb}, message)
188+
ensure
189+
FileUtils.rm_rf("db") if File.directory?("db")
190+
end
191+
192+
test "running migrations happens in a child process" do
193+
Open3.expects(:capture2)
194+
.with({ "VERBOSE" => "true" }, "bundle exec rails db:migrate")
195+
.returns(["Running migrations...", mock(exitstatus: 0)])
196+
197+
@server.execute("run_migrations", {})
198+
assert_equal({ message: "Running migrations...", status: 0 }, response[:result])
199+
end
200+
179201
private
180202

181203
def response

0 commit comments

Comments
 (0)