From a637826d802f78abbe1390920addf92ee16f1dbc Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 25 Nov 2025 21:12:55 -1000 Subject: [PATCH 1/2] Add database check to bin/dev before starting server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../react_on_rails/dev/database_checker.rb | 119 +++++++++++++ .../lib/react_on_rails/dev/server_manager.rb | 10 +- .../dev/database_checker_spec.rb | 156 ++++++++++++++++++ 3 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 react_on_rails/lib/react_on_rails/dev/database_checker.rb create mode 100644 react_on_rails/spec/react_on_rails/dev/database_checker_spec.rb diff --git a/react_on_rails/lib/react_on_rails/dev/database_checker.rb b/react_on_rails/lib/react_on_rails/dev/database_checker.rb new file mode 100644 index 0000000000..dd40b98038 --- /dev/null +++ b/react_on_rails/lib/react_on_rails/dev/database_checker.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require "rainbow" + +module ReactOnRails + module Dev + # DatabaseChecker validates that the database is set up before starting + # the development server. + # + # This prevents confusing errors buried in combined Foreman/Overmind logs + # and provides clear guidance on how to set up the database. + # + class DatabaseChecker + class << self + # Check if the database is set up and provide helpful output + # + # @return [Boolean] true if database is ready, false otherwise + def check_database + return true unless rails_available? + + check_and_report_database + end + + private + + def rails_available? + defined?(Rails) && defined?(ActiveRecord::Base) + end + + def check_and_report_database + print_check_header + + if database_ready? + print_database_ok + true + else + print_database_failed + false + end + end + + def database_ready? + # Try to establish connection and run a simple query + ActiveRecord::Base.connection.execute("SELECT 1") + true + rescue ActiveRecord::NoDatabaseError + # Database doesn't exist + @error_type = :no_database + false + rescue ActiveRecord::PendingMigrationError + # Database exists but migrations are pending + @error_type = :pending_migrations + false + rescue ActiveRecord::ConnectionNotEstablished, + ActiveRecord::StatementInvalid => e + # Connection failed or other database error + @error_type = :connection_error + @error_message = e.message + false + rescue StandardError => e + # Unexpected error - log but don't block startup + # This allows apps without databases to still use bin/dev + warn "Database check warning: #{e.message}" if ENV["DEBUG"] + true + end + + def print_check_header + puts "" + puts Rainbow("🗄️ Checking database...").cyan.bold + puts "" + end + + def print_database_ok + puts " #{Rainbow('✓').green} Database is ready" + puts "" + end + + # rubocop:disable Metrics/AbcSize + def print_database_failed + puts " #{Rainbow('✗').red} Database is not ready" + puts "" + puts Rainbow("❌ Database not set up!").red.bold + puts "" + + case @error_type + when :no_database + puts Rainbow("The database does not exist.").yellow + puts "" + puts Rainbow("Run one of these commands:").cyan.bold + puts " #{Rainbow('bin/rails db:setup').green} # Create database, load schema, seed data" + puts " #{Rainbow('bin/rails db:create').green} # Just create the database" + when :pending_migrations + puts Rainbow("The database exists but has pending migrations.").yellow + puts "" + puts Rainbow("Run this command:").cyan.bold + puts " #{Rainbow('bin/rails db:migrate').green} # Run pending migrations" + when :connection_error + puts Rainbow("Could not connect to the database.").yellow + if @error_message + puts "" + puts Rainbow("Error: #{@error_message}").red + end + puts "" + puts Rainbow("Possible solutions:").cyan.bold + puts " #{Rainbow('1.').yellow} Check if your database server is running" + puts " #{Rainbow('2.').yellow} Verify database.yml configuration" + puts " #{Rainbow('3.').yellow} Run #{Rainbow('bin/rails db:setup').green} to create the database" + end + + puts "" + puts Rainbow("💡 Tip:").blue.bold + puts " After fixing the database, run #{Rainbow('bin/dev').green} again" + puts "" + end + # rubocop:enable Metrics/AbcSize + end + end + end +end diff --git a/react_on_rails/lib/react_on_rails/dev/server_manager.rb b/react_on_rails/lib/react_on_rails/dev/server_manager.rb index 473abb8c13..26961317c2 100644 --- a/react_on_rails/lib/react_on_rails/dev/server_manager.rb +++ b/react_on_rails/lib/react_on_rails/dev/server_manager.rb @@ -4,6 +4,7 @@ require "open3" require "rainbow" require_relative "../packer_utils" +require_relative "database_checker" require_relative "service_checker" module ReactOnRails @@ -411,8 +412,9 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil) print_procfile_info(procfile, route: route) - # Check required services before starting + # Check required services and database before starting exit 1 unless ServiceChecker.check_services + exit 1 unless DatabaseChecker.check_database print_server_info( "🏭 Starting production-like development server...", @@ -536,8 +538,9 @@ def run_production_like(_verbose: false, route: nil, rails_env: nil) def run_static_development(procfile, verbose: false, route: nil) print_procfile_info(procfile, route: route) - # Check required services before starting + # Check required services and database before starting exit 1 unless ServiceChecker.check_services + exit 1 unless DatabaseChecker.check_database features = [ "Using shakapacker --watch (no HMR)", @@ -565,8 +568,9 @@ def run_static_development(procfile, verbose: false, route: nil) def run_development(procfile, verbose: false, route: nil) print_procfile_info(procfile, route: route) - # Check required services before starting + # Check required services and database before starting exit 1 unless ServiceChecker.check_services + exit 1 unless DatabaseChecker.check_database PackGenerator.generate(verbose: verbose) ProcessManager.ensure_procfile(procfile) diff --git a/react_on_rails/spec/react_on_rails/dev/database_checker_spec.rb b/react_on_rails/spec/react_on_rails/dev/database_checker_spec.rb new file mode 100644 index 0000000000..23a31296c3 --- /dev/null +++ b/react_on_rails/spec/react_on_rails/dev/database_checker_spec.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +require "react_on_rails/dev/database_checker" +require "stringio" + +# Create a test helper class for mocking ActiveRecord connection +class MockConnection + def execute(query); end +end + +# Create a test helper class for mocking ActiveRecord::Base +class MockActiveRecordBase + def self.connection; end +end + +RSpec.describe ReactOnRails::Dev::DatabaseChecker do + describe ".check_database" do + context "when Rails/ActiveRecord is not available" do + before do + hide_const("Rails") + hide_const("ActiveRecord::Base") + end + + it "returns true without checking database" do + expect(described_class.check_database).to be true + end + end + + context "when Rails is available" do + let(:mock_connection) { instance_double(MockConnection) } + + before do + stub_const("Rails", class_double(Object)) + stub_const("ActiveRecord::Base", class_double(MockActiveRecordBase, connection: mock_connection)) + stub_const("ActiveRecord::NoDatabaseError", Class.new(StandardError)) + stub_const("ActiveRecord::PendingMigrationError", Class.new(StandardError)) + stub_const("ActiveRecord::ConnectionNotEstablished", Class.new(StandardError)) + stub_const("ActiveRecord::StatementInvalid", Class.new(StandardError)) + end + + context "when database is ready" do + before do + allow(mock_connection).to receive(:execute).with("SELECT 1").and_return(true) + end + + it "returns true" do + output = capture_stdout do + expect(described_class.check_database).to be true + end + + expect(output).to include("Checking database") + expect(output).to include("Database is ready") + end + end + + context "when database does not exist" do + before do + allow(mock_connection).to receive(:execute).and_raise(ActiveRecord::NoDatabaseError) + end + + it "returns false and shows db:setup guidance" do + output = capture_stdout do + expect(described_class.check_database).to be false + end + + expect(output).to include("Database not set up") + expect(output).to include("database does not exist") + expect(output).to include("bin/rails db:setup") + expect(output).to include("bin/rails db:create") + end + end + + context "when migrations are pending" do + before do + allow(mock_connection).to receive(:execute).and_raise(ActiveRecord::PendingMigrationError) + end + + it "returns false and shows db:migrate guidance" do + output = capture_stdout do + expect(described_class.check_database).to be false + end + + expect(output).to include("Database not set up") + expect(output).to include("pending migrations") + expect(output).to include("bin/rails db:migrate") + end + end + + context "when connection cannot be established" do + before do + allow(mock_connection).to receive(:execute).and_raise( + ActiveRecord::ConnectionNotEstablished.new("Connection refused") + ) + end + + it "returns false and shows connection troubleshooting" do + output = capture_stdout do + expect(described_class.check_database).to be false + end + + expect(output).to include("Database not set up") + expect(output).to include("Could not connect") + expect(output).to include("database server is running") + expect(output).to include("database.yml") + end + end + + context "when a statement error occurs" do + before do + allow(mock_connection).to receive(:execute).and_raise( + ActiveRecord::StatementInvalid.new("Unknown error") + ) + end + + it "returns false and shows connection troubleshooting" do + output = capture_stdout do + expect(described_class.check_database).to be false + end + + expect(output).to include("Database not set up") + expect(output).to include("Could not connect") + end + end + + context "when an unexpected error occurs" do + before do + allow(mock_connection).to receive(:execute).and_raise(StandardError.new("Something unexpected")) + end + + it "returns true to allow apps without databases to continue" do + output = capture_stdout do + expect(described_class.check_database).to be true + end + + # Should show the check header but not fail + expect(output).to include("Checking database") + end + + it "outputs a warning when DEBUG is enabled" do + allow(ENV).to receive(:[]).with("DEBUG").and_return("true") + expect { described_class.check_database }.to output(/Database check warning/).to_stderr + end + end + end + end + + # Helper methods + def capture_stdout + old_stdout = $stdout + $stdout = StringIO.new + yield + $stdout.string + ensure + $stdout = old_stdout + end +end From 22c330806df6147db6e8bb2ce12148e2fa1d6735 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 25 Nov 2025 21:49:24 -1000 Subject: [PATCH 2/2] Address code review feedback for DatabaseChecker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor instance variables to result object pattern for thread safety - database_ready? now returns a Hash with :ready, :error_type, :error_message - print_database_failed now takes error_type and error_message as parameters - This makes the code thread-safe and improves maintainability - Add RBS type signatures (required by project standards) - Created sig/react_on_rails/dev/database_checker.rbs - Added to Steepfile (alphabetically ordered) - Validated with bundle exec rake rbs:validate - Add test case for DEBUG=false scenario - Verifies no warning is output when DEBUG is not enabled 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- react_on_rails/Steepfile | 1 + .../react_on_rails/dev/database_checker.rb | 27 +++++++++---------- .../react_on_rails/dev/database_checker.rbs | 16 +++++++++++ .../dev/database_checker_spec.rb | 5 ++++ 4 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 react_on_rails/sig/react_on_rails/dev/database_checker.rbs diff --git a/react_on_rails/Steepfile b/react_on_rails/Steepfile index 0d9d79e5f2..291fb94455 100644 --- a/react_on_rails/Steepfile +++ b/react_on_rails/Steepfile @@ -28,6 +28,7 @@ target :lib do check "lib/react_on_rails.rb" check "lib/react_on_rails/configuration.rb" check "lib/react_on_rails/controller.rb" + check "lib/react_on_rails/dev/database_checker.rb" check "lib/react_on_rails/dev/file_manager.rb" check "lib/react_on_rails/dev/pack_generator.rb" check "lib/react_on_rails/dev/process_manager.rb" diff --git a/react_on_rails/lib/react_on_rails/dev/database_checker.rb b/react_on_rails/lib/react_on_rails/dev/database_checker.rb index dd40b98038..c007477ee8 100644 --- a/react_on_rails/lib/react_on_rails/dev/database_checker.rb +++ b/react_on_rails/lib/react_on_rails/dev/database_checker.rb @@ -30,11 +30,12 @@ def rails_available? def check_and_report_database print_check_header - if database_ready? + result = database_ready? + if result[:ready] print_database_ok true else - print_database_failed + print_database_failed(result[:error_type], result[:error_message]) false end end @@ -42,26 +43,22 @@ def check_and_report_database def database_ready? # Try to establish connection and run a simple query ActiveRecord::Base.connection.execute("SELECT 1") - true + { ready: true } rescue ActiveRecord::NoDatabaseError # Database doesn't exist - @error_type = :no_database - false + { ready: false, error_type: :no_database } rescue ActiveRecord::PendingMigrationError # Database exists but migrations are pending - @error_type = :pending_migrations - false + { ready: false, error_type: :pending_migrations } rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::StatementInvalid => e # Connection failed or other database error - @error_type = :connection_error - @error_message = e.message - false + { ready: false, error_type: :connection_error, error_message: e.message } rescue StandardError => e # Unexpected error - log but don't block startup # This allows apps without databases to still use bin/dev warn "Database check warning: #{e.message}" if ENV["DEBUG"] - true + { ready: true } end def print_check_header @@ -76,13 +73,13 @@ def print_database_ok end # rubocop:disable Metrics/AbcSize - def print_database_failed + def print_database_failed(error_type, error_message) puts " #{Rainbow('✗').red} Database is not ready" puts "" puts Rainbow("❌ Database not set up!").red.bold puts "" - case @error_type + case error_type when :no_database puts Rainbow("The database does not exist.").yellow puts "" @@ -96,9 +93,9 @@ def print_database_failed puts " #{Rainbow('bin/rails db:migrate').green} # Run pending migrations" when :connection_error puts Rainbow("Could not connect to the database.").yellow - if @error_message + if error_message puts "" - puts Rainbow("Error: #{@error_message}").red + puts Rainbow("Error: #{error_message}").red end puts "" puts Rainbow("Possible solutions:").cyan.bold diff --git a/react_on_rails/sig/react_on_rails/dev/database_checker.rbs b/react_on_rails/sig/react_on_rails/dev/database_checker.rbs new file mode 100644 index 0000000000..e9053be70f --- /dev/null +++ b/react_on_rails/sig/react_on_rails/dev/database_checker.rbs @@ -0,0 +1,16 @@ +module ReactOnRails + module Dev + class DatabaseChecker + def self.check_database: () -> bool + + private + + def self.rails_available?: () -> bool + def self.check_and_report_database: () -> bool + def self.database_ready?: () -> Hash[Symbol, untyped] + def self.print_check_header: () -> void + def self.print_database_ok: () -> void + def self.print_database_failed: (Symbol?, String?) -> void + end + end +end diff --git a/react_on_rails/spec/react_on_rails/dev/database_checker_spec.rb b/react_on_rails/spec/react_on_rails/dev/database_checker_spec.rb index 23a31296c3..d93259712a 100644 --- a/react_on_rails/spec/react_on_rails/dev/database_checker_spec.rb +++ b/react_on_rails/spec/react_on_rails/dev/database_checker_spec.rb @@ -140,6 +140,11 @@ def self.connection; end allow(ENV).to receive(:[]).with("DEBUG").and_return("true") expect { described_class.check_database }.to output(/Database check warning/).to_stderr end + + it "does not output a warning when DEBUG is not enabled" do + allow(ENV).to receive(:[]).with("DEBUG").and_return(nil) + expect { described_class.check_database }.not_to output(/Database check warning/).to_stderr + end end end end