Skip to content

Commit a637826

Browse files
justin808claude
andcommitted
Add database check to bin/dev before starting server
When running bin/dev on a fresh checkout or after database cleanup, the script now checks if the database is set up before starting all services. This prevents confusing errors buried in Foreman/Overmind logs and provides clear guidance on how to fix the issue. The check handles three cases: - No database exists: suggests db:setup or db:create - Pending migrations: suggests db:migrate - Connection errors: shows troubleshooting steps If Rails/ActiveRecord isn't available, or if an unexpected error occurs, the check is skipped to allow apps without databases to continue. Closes #2099 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 16b3908 commit a637826

File tree

3 files changed

+282
-3
lines changed

3 files changed

+282
-3
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# frozen_string_literal: true
2+
3+
require "rainbow"
4+
5+
module ReactOnRails
6+
module Dev
7+
# DatabaseChecker validates that the database is set up before starting
8+
# the development server.
9+
#
10+
# This prevents confusing errors buried in combined Foreman/Overmind logs
11+
# and provides clear guidance on how to set up the database.
12+
#
13+
class DatabaseChecker
14+
class << self
15+
# Check if the database is set up and provide helpful output
16+
#
17+
# @return [Boolean] true if database is ready, false otherwise
18+
def check_database
19+
return true unless rails_available?
20+
21+
check_and_report_database
22+
end
23+
24+
private
25+
26+
def rails_available?
27+
defined?(Rails) && defined?(ActiveRecord::Base)
28+
end
29+
30+
def check_and_report_database
31+
print_check_header
32+
33+
if database_ready?
34+
print_database_ok
35+
true
36+
else
37+
print_database_failed
38+
false
39+
end
40+
end
41+
42+
def database_ready?
43+
# Try to establish connection and run a simple query
44+
ActiveRecord::Base.connection.execute("SELECT 1")
45+
true
46+
rescue ActiveRecord::NoDatabaseError
47+
# Database doesn't exist
48+
@error_type = :no_database
49+
false
50+
rescue ActiveRecord::PendingMigrationError
51+
# Database exists but migrations are pending
52+
@error_type = :pending_migrations
53+
false
54+
rescue ActiveRecord::ConnectionNotEstablished,
55+
ActiveRecord::StatementInvalid => e
56+
# Connection failed or other database error
57+
@error_type = :connection_error
58+
@error_message = e.message
59+
false
60+
rescue StandardError => e
61+
# Unexpected error - log but don't block startup
62+
# This allows apps without databases to still use bin/dev
63+
warn "Database check warning: #{e.message}" if ENV["DEBUG"]
64+
true
65+
end
66+
67+
def print_check_header
68+
puts ""
69+
puts Rainbow("🗄️ Checking database...").cyan.bold
70+
puts ""
71+
end
72+
73+
def print_database_ok
74+
puts " #{Rainbow('✓').green} Database is ready"
75+
puts ""
76+
end
77+
78+
# rubocop:disable Metrics/AbcSize
79+
def print_database_failed
80+
puts " #{Rainbow('✗').red} Database is not ready"
81+
puts ""
82+
puts Rainbow("❌ Database not set up!").red.bold
83+
puts ""
84+
85+
case @error_type
86+
when :no_database
87+
puts Rainbow("The database does not exist.").yellow
88+
puts ""
89+
puts Rainbow("Run one of these commands:").cyan.bold
90+
puts " #{Rainbow('bin/rails db:setup').green} # Create database, load schema, seed data"
91+
puts " #{Rainbow('bin/rails db:create').green} # Just create the database"
92+
when :pending_migrations
93+
puts Rainbow("The database exists but has pending migrations.").yellow
94+
puts ""
95+
puts Rainbow("Run this command:").cyan.bold
96+
puts " #{Rainbow('bin/rails db:migrate').green} # Run pending migrations"
97+
when :connection_error
98+
puts Rainbow("Could not connect to the database.").yellow
99+
if @error_message
100+
puts ""
101+
puts Rainbow("Error: #{@error_message}").red
102+
end
103+
puts ""
104+
puts Rainbow("Possible solutions:").cyan.bold
105+
puts " #{Rainbow('1.').yellow} Check if your database server is running"
106+
puts " #{Rainbow('2.').yellow} Verify database.yml configuration"
107+
puts " #{Rainbow('3.').yellow} Run #{Rainbow('bin/rails db:setup').green} to create the database"
108+
end
109+
110+
puts ""
111+
puts Rainbow("💡 Tip:").blue.bold
112+
puts " After fixing the database, run #{Rainbow('bin/dev').green} again"
113+
puts ""
114+
end
115+
# rubocop:enable Metrics/AbcSize
116+
end
117+
end
118+
end
119+
end

react_on_rails/lib/react_on_rails/dev/server_manager.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require "open3"
55
require "rainbow"
66
require_relative "../packer_utils"
7+
require_relative "database_checker"
78
require_relative "service_checker"
89

910
module ReactOnRails
@@ -411,8 +412,9 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil)
411412

412413
print_procfile_info(procfile, route: route)
413414

414-
# Check required services before starting
415+
# Check required services and database before starting
415416
exit 1 unless ServiceChecker.check_services
417+
exit 1 unless DatabaseChecker.check_database
416418

417419
print_server_info(
418420
"🏭 Starting production-like development server...",
@@ -536,8 +538,9 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil)
536538
def run_static_development(procfile, verbose: false, route: nil)
537539
print_procfile_info(procfile, route: route)
538540

539-
# Check required services before starting
541+
# Check required services and database before starting
540542
exit 1 unless ServiceChecker.check_services
543+
exit 1 unless DatabaseChecker.check_database
541544

542545
features = [
543546
"Using shakapacker --watch (no HMR)",
@@ -565,8 +568,9 @@ def run_static_development(procfile, verbose: false, route: nil)
565568
def run_development(procfile, verbose: false, route: nil)
566569
print_procfile_info(procfile, route: route)
567570

568-
# Check required services before starting
571+
# Check required services and database before starting
569572
exit 1 unless ServiceChecker.check_services
573+
exit 1 unless DatabaseChecker.check_database
570574

571575
PackGenerator.generate(verbose: verbose)
572576
ProcessManager.ensure_procfile(procfile)
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# frozen_string_literal: true
2+
3+
require "react_on_rails/dev/database_checker"
4+
require "stringio"
5+
6+
# Create a test helper class for mocking ActiveRecord connection
7+
class MockConnection
8+
def execute(query); end
9+
end
10+
11+
# Create a test helper class for mocking ActiveRecord::Base
12+
class MockActiveRecordBase
13+
def self.connection; end
14+
end
15+
16+
RSpec.describe ReactOnRails::Dev::DatabaseChecker do
17+
describe ".check_database" do
18+
context "when Rails/ActiveRecord is not available" do
19+
before do
20+
hide_const("Rails")
21+
hide_const("ActiveRecord::Base")
22+
end
23+
24+
it "returns true without checking database" do
25+
expect(described_class.check_database).to be true
26+
end
27+
end
28+
29+
context "when Rails is available" do
30+
let(:mock_connection) { instance_double(MockConnection) }
31+
32+
before do
33+
stub_const("Rails", class_double(Object))
34+
stub_const("ActiveRecord::Base", class_double(MockActiveRecordBase, connection: mock_connection))
35+
stub_const("ActiveRecord::NoDatabaseError", Class.new(StandardError))
36+
stub_const("ActiveRecord::PendingMigrationError", Class.new(StandardError))
37+
stub_const("ActiveRecord::ConnectionNotEstablished", Class.new(StandardError))
38+
stub_const("ActiveRecord::StatementInvalid", Class.new(StandardError))
39+
end
40+
41+
context "when database is ready" do
42+
before do
43+
allow(mock_connection).to receive(:execute).with("SELECT 1").and_return(true)
44+
end
45+
46+
it "returns true" do
47+
output = capture_stdout do
48+
expect(described_class.check_database).to be true
49+
end
50+
51+
expect(output).to include("Checking database")
52+
expect(output).to include("Database is ready")
53+
end
54+
end
55+
56+
context "when database does not exist" do
57+
before do
58+
allow(mock_connection).to receive(:execute).and_raise(ActiveRecord::NoDatabaseError)
59+
end
60+
61+
it "returns false and shows db:setup guidance" do
62+
output = capture_stdout do
63+
expect(described_class.check_database).to be false
64+
end
65+
66+
expect(output).to include("Database not set up")
67+
expect(output).to include("database does not exist")
68+
expect(output).to include("bin/rails db:setup")
69+
expect(output).to include("bin/rails db:create")
70+
end
71+
end
72+
73+
context "when migrations are pending" do
74+
before do
75+
allow(mock_connection).to receive(:execute).and_raise(ActiveRecord::PendingMigrationError)
76+
end
77+
78+
it "returns false and shows db:migrate guidance" do
79+
output = capture_stdout do
80+
expect(described_class.check_database).to be false
81+
end
82+
83+
expect(output).to include("Database not set up")
84+
expect(output).to include("pending migrations")
85+
expect(output).to include("bin/rails db:migrate")
86+
end
87+
end
88+
89+
context "when connection cannot be established" do
90+
before do
91+
allow(mock_connection).to receive(:execute).and_raise(
92+
ActiveRecord::ConnectionNotEstablished.new("Connection refused")
93+
)
94+
end
95+
96+
it "returns false and shows connection troubleshooting" do
97+
output = capture_stdout do
98+
expect(described_class.check_database).to be false
99+
end
100+
101+
expect(output).to include("Database not set up")
102+
expect(output).to include("Could not connect")
103+
expect(output).to include("database server is running")
104+
expect(output).to include("database.yml")
105+
end
106+
end
107+
108+
context "when a statement error occurs" do
109+
before do
110+
allow(mock_connection).to receive(:execute).and_raise(
111+
ActiveRecord::StatementInvalid.new("Unknown error")
112+
)
113+
end
114+
115+
it "returns false and shows connection troubleshooting" do
116+
output = capture_stdout do
117+
expect(described_class.check_database).to be false
118+
end
119+
120+
expect(output).to include("Database not set up")
121+
expect(output).to include("Could not connect")
122+
end
123+
end
124+
125+
context "when an unexpected error occurs" do
126+
before do
127+
allow(mock_connection).to receive(:execute).and_raise(StandardError.new("Something unexpected"))
128+
end
129+
130+
it "returns true to allow apps without databases to continue" do
131+
output = capture_stdout do
132+
expect(described_class.check_database).to be true
133+
end
134+
135+
# Should show the check header but not fail
136+
expect(output).to include("Checking database")
137+
end
138+
139+
it "outputs a warning when DEBUG is enabled" do
140+
allow(ENV).to receive(:[]).with("DEBUG").and_return("true")
141+
expect { described_class.check_database }.to output(/Database check warning/).to_stderr
142+
end
143+
end
144+
end
145+
end
146+
147+
# Helper methods
148+
def capture_stdout
149+
old_stdout = $stdout
150+
$stdout = StringIO.new
151+
yield
152+
$stdout.string
153+
ensure
154+
$stdout = old_stdout
155+
end
156+
end

0 commit comments

Comments
 (0)