From 9c68331fb51bbb653b1277c1830d30e6dc921d13 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Sun, 4 Oct 2026 18:09:36 +1100 Subject: [PATCH 1/4] Support PostgreSQL and MySQL JDBC URLs On JRuby, databases must be reached via JDBC URLs such as "jdbc:postgresql://localhost/app?user=postgres&password=secret". Parse the URL nested after "jdbc:", and read the user and password from query params when they are not in the userinfo (the PostgreSQL JDBC driver does not accept them there). Use JDBC URLs for the PostgreSQL and MySQL specs when running on JRuby. On JRuby, PostgreSQL also needs the hanami fix that checks for the jdbc-postgres gem (not jdbc-postgresql). --- .../cli/commands/app/db/utils/database.rb | 29 +++++++++++++------ lib/hanami/cli/commands/app/db/utils/mysql.rb | 4 +-- .../cli/commands/app/db/utils/postgres.rb | 10 ++++--- .../cli/commands/app/db/utils/sqlite.rb | 2 +- spec/support/helpers.rb | 17 +++++++++++ .../hanami/cli/commands/app/db/create_spec.rb | 8 ++--- .../hanami/cli/commands/app/db/drop_spec.rb | 6 ++-- .../cli/commands/app/db/migrate_spec.rb | 4 +-- .../cli/commands/app/db/prepare_spec.rb | 4 +-- .../cli/commands/app/db/rollback_spec.rb | 20 ++++++------- .../commands/app/db/structure/dump_spec.rb | 6 ++-- .../commands/app/db/structure/load_spec.rb | 6 ++-- 12 files changed, 73 insertions(+), 43 deletions(-) diff --git a/lib/hanami/cli/commands/app/db/utils/database.rb b/lib/hanami/cli/commands/app/db/utils/database.rb index 7360a72a..bd4ed336 100644 --- a/lib/hanami/cli/commands/app/db/utils/database.rb +++ b/lib/hanami/cli/commands/app/db/utils/database.rb @@ -38,6 +38,10 @@ def sql require_relative("postgres") Postgres }, + "mysql" => -> { + require_relative("mysql") + Mysql + }, "mysql2" => -> { require_relative("mysql") Mysql @@ -45,10 +49,7 @@ def sql ).freeze def self.database_class(database_url) - database_scheme = URI(database_url).scheme - if database_scheme == "jdbc" - database_scheme = URI(database_url.sub("jdbc:", "")).scheme - end + database_scheme = URI(database_url.delete_prefix("jdbc:")).scheme DATABASE_CLASS_RESOLVER[database_scheme].call end @@ -86,8 +87,20 @@ def database_url slice.container.providers[:db].source.database_urls.fetch(gateway_name) end + # JDBC URLs (required to connect via JRuby) nest the real URL after "jdbc:", e.g. + # "jdbc:postgresql://localhost/app", so parse that nested URL instead. def database_uri - @database_uri ||= URI(database_url) + @database_uri ||= URI(database_url.delete_prefix("jdbc:")) + end + + # JDBC drivers expect the user and password as query params, e.g. + # "jdbc:postgresql://localhost/app?user=postgres&password=secret". + def database_user + database_uri.user || database_query_params["user"] + end + + def database_password + database_uri.password || database_query_params["password"] end def gateway @@ -197,10 +210,8 @@ def schema_migrations_sql_dump private - def database_path - database_uri.path || - # For `jdbc:` URIs the path is exposed via the opaque component - database_uri.opaque.sub(%r{^\w+:/?}, "") + def database_query_params + @database_query_params ||= URI.decode_www_form(database_uri.query.to_s).to_h end def jruby? diff --git a/lib/hanami/cli/commands/app/db/utils/mysql.rb b/lib/hanami/cli/commands/app/db/utils/mysql.rb index f14d943e..b15ddac2 100644 --- a/lib/hanami/cli/commands/app/db/utils/mysql.rb +++ b/lib/hanami/cli/commands/app/db/utils/mysql.rb @@ -71,13 +71,13 @@ def cli_options [].tap { |opts| opts << "--host=#{Shellwords.escape(database_uri.host)}" if database_uri.host opts << "--port=#{Shellwords.escape(database_uri.port)}" if database_uri.port - opts << "--user=#{Shellwords.escape(database_uri.user)}" if database_uri.user + opts << "--user=#{Shellwords.escape(database_user)}" if database_user }.join(" ") end def cli_env_vars @cli_env_vars ||= {}.tap do |vars| - vars["MYSQL_PWD"] = database_uri.password.to_s if database_uri.password + vars["MYSQL_PWD"] = database_password.to_s if database_password end end end diff --git a/lib/hanami/cli/commands/app/db/utils/postgres.rb b/lib/hanami/cli/commands/app/db/utils/postgres.rb index f65a2068..9e18ecd1 100644 --- a/lib/hanami/cli/commands/app/db/utils/postgres.rb +++ b/lib/hanami/cli/commands/app/db/utils/postgres.rb @@ -87,10 +87,12 @@ def escaped_name end def cli_env_vars - @cli_env_vars ||= %i[host port user password].each_with_object({}) do |field, vars| - value = database_uri.public_send(field).to_s - vars["PG#{field}".upcase] = value unless value.empty? - end + @cli_env_vars ||= { + "PGHOST" => database_uri.host, + "PGPORT" => database_uri.port, + "PGUSER" => database_user, + "PGPASSWORD" => database_password + }.transform_values(&:to_s).reject { |_, value| value.empty? } end end end diff --git a/lib/hanami/cli/commands/app/db/utils/sqlite.rb b/lib/hanami/cli/commands/app/db/utils/sqlite.rb index 74e01f65..459828fc 100644 --- a/lib/hanami/cli/commands/app/db/utils/sqlite.rb +++ b/lib/hanami/cli/commands/app/db/utils/sqlite.rb @@ -72,7 +72,7 @@ def name @name ||= begin raw = - if database_uri.scheme == "jdbc" + if database_url.start_with?("jdbc:") # For JDBC SQLite URIs like "jdbc:sqlite:db/app.sqlite3", # we need to extract the path part after "jdbc:sqlite:" # The standard URI.parse doesn't handle JDBC URIs well, so we remove the prefix manually diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb index c35d6a2e..4aeb6d23 100644 --- a/spec/support/helpers.rb +++ b/spec/support/helpers.rb @@ -27,6 +27,23 @@ def sqlite_db_name(url, dir: nil) jruby? && dir ? File.join(dir, url) : url end + def postgres_url(db_suffix) + url = "#{POSTGRES_BASE_URL}#{db_suffix}" + jruby? ? jdbc_url(url, scheme: "postgresql") : url + end + + def mysql_url(db_suffix) + url = "#{MYSQL_BASE_URL}#{db_suffix}" + jruby? ? jdbc_url(url, scheme: "mysql") : url + end + + def jdbc_url(url, scheme:) + uri = URI(url) + # JDBC drivers expect the user and password as query params, not in the userinfo + query = URI.encode_www_form({user: uri.user, password: uri.password}.compact) + "jdbc:#{scheme}://#{uri.host}:#{uri.port}#{uri.path}?#{query}" + end + def jruby? RUBY_ENGINE == "jruby" end diff --git a/spec/unit/hanami/cli/commands/app/db/create_spec.rb b/spec/unit/hanami/cli/commands/app/db/create_spec.rb index 25526cb9..6cd465bd 100644 --- a/spec/unit/hanami/cli/commands/app/db/create_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/create_spec.rb @@ -76,7 +76,7 @@ def before_prepare describe "postgres", :postgres do before do - ENV["DATABASE_URL"] = "#{POSTGRES_BASE_URL}_app" + ENV["DATABASE_URL"] = postgres_url("_app") end it "creates the database" do @@ -101,7 +101,7 @@ def before_prepare describe "mysql", :mysql do before do - ENV["DATABASE_URL"] = "#{MYSQL_BASE_URL}_app" + ENV["DATABASE_URL"] = mysql_url("_app") end it "creates the database" do @@ -253,8 +253,8 @@ def before_prepare describe "postgres", :postgres do before do - ENV["DATABASE_URL"] = "#{POSTGRES_BASE_URL}_app" - ENV["MAIN__DATABASE_URL"] = "#{POSTGRES_BASE_URL}_main" + ENV["DATABASE_URL"] = postgres_url("_app") + ENV["MAIN__DATABASE_URL"] = postgres_url("_main") end it "creates each database" do diff --git a/spec/unit/hanami/cli/commands/app/db/drop_spec.rb b/spec/unit/hanami/cli/commands/app/db/drop_spec.rb index f6458854..c596e1be 100644 --- a/spec/unit/hanami/cli/commands/app/db/drop_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/drop_spec.rb @@ -306,8 +306,8 @@ def before_prepare describe "postgres", :postgres do before do - ENV["DATABASE_URL"] = "#{POSTGRES_BASE_URL}_app" - ENV["MAIN__DATABASE_URL"] = "#{POSTGRES_BASE_URL}_main" + ENV["DATABASE_URL"] = postgres_url("_app") + ENV["MAIN__DATABASE_URL"] = postgres_url("_main") end it "drops each database" do @@ -427,7 +427,7 @@ def before_prepare describe "mysql", :mysql do before do - ENV["DATABASE_URL"] = "#{MYSQL_BASE_URL}_app" + ENV["DATABASE_URL"] = mysql_url("_app") end it "drops the database" do diff --git a/spec/unit/hanami/cli/commands/app/db/migrate_spec.rb b/spec/unit/hanami/cli/commands/app/db/migrate_spec.rb index f7dd51ba..8026f138 100644 --- a/spec/unit/hanami/cli/commands/app/db/migrate_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/migrate_spec.rb @@ -246,8 +246,8 @@ def before_prepare describe "postgres", :postgres do before do - ENV["DATABASE_URL"] = "#{POSTGRES_BASE_URL}_app" - ENV["MAIN__DATABASE_URL"] = "#{POSTGRES_BASE_URL}_main" + ENV["DATABASE_URL"] = postgres_url("_app") + ENV["MAIN__DATABASE_URL"] = postgres_url("_main") db_create end diff --git a/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb b/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb index 6e631f14..6fe8db8f 100644 --- a/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/prepare_spec.rb @@ -191,8 +191,8 @@ class Comments < Hanami::DB::Relation describe "postgres", :postgres do before do - ENV["DATABASE_URL"] = "#{POSTGRES_BASE_URL}_app" - ENV["MAIN__DATABASE_URL"] = "#{POSTGRES_BASE_URL}_main" + ENV["DATABASE_URL"] = postgres_url("_app") + ENV["MAIN__DATABASE_URL"] = postgres_url("_main") end context "from scratch, with structure dump and seeds" do diff --git a/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb b/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb index 9b35908f..47f69ec0 100644 --- a/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/rollback_spec.rb @@ -351,8 +351,8 @@ class App < Hanami::App describe "postgres", :postgres do before do - ENV["DATABASE_URL"] = "#{POSTGRES_BASE_URL}_app" - ENV["MAIN__DATABASE_URL"] = "#{POSTGRES_BASE_URL}_main" + ENV["DATABASE_URL"] = postgres_url("_app") + ENV["MAIN__DATABASE_URL"] = postgres_url("_main") end context "single database" do @@ -394,9 +394,9 @@ def before_prepare_gateways end before do - ENV["DATABASE_URL"] = "#{POSTGRES_BASE_URL}_app" - ENV["DATABASE_URL__EXTRA"] = "#{POSTGRES_BASE_URL}_extra" - ENV["DATABASE_URL__SUPER"] = "#{POSTGRES_BASE_URL}_super" + ENV["DATABASE_URL"] = postgres_url("_app") + ENV["DATABASE_URL__EXTRA"] = postgres_url("_extra") + ENV["DATABASE_URL__SUPER"] = postgres_url("_super") with_directory(@dir = make_tmp_directory) do write "config/app.rb", <<~RUBY @@ -424,8 +424,8 @@ class App < Hanami::App describe "mysql", :mysql do before do - ENV["DATABASE_URL"] = "#{MYSQL_BASE_URL}_app" - ENV["MAIN__DATABASE_URL"] = "#{MYSQL_BASE_URL}_main" + ENV["DATABASE_URL"] = mysql_url("_app") + ENV["MAIN__DATABASE_URL"] = mysql_url("_main") end context "single database" do @@ -467,9 +467,9 @@ def before_prepare_gateways end before do - ENV["DATABASE_URL"] = "#{MYSQL_BASE_URL}_app" - ENV["DATABASE_URL__EXTRA"] = "#{MYSQL_BASE_URL}_extra" - ENV["DATABASE_URL__SUPER"] = "#{MYSQL_BASE_URL}_super" + ENV["DATABASE_URL"] = mysql_url("_app") + ENV["DATABASE_URL__EXTRA"] = mysql_url("_extra") + ENV["DATABASE_URL__SUPER"] = mysql_url("_super") with_directory(@dir = make_tmp_directory) do write "config/app.rb", <<~RUBY diff --git a/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb b/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb index d2de9b11..fa84ffaf 100644 --- a/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/structure/dump_spec.rb @@ -217,8 +217,8 @@ def before_prepare describe "postgres", :postgres do before do - ENV["DATABASE_URL"] = "#{POSTGRES_BASE_URL}_app" - ENV["MAIN__DATABASE_URL"] = "#{POSTGRES_BASE_URL}_main" + ENV["DATABASE_URL"] = postgres_url("_app") + ENV["MAIN__DATABASE_URL"] = postgres_url("_main") db_migrate end @@ -299,7 +299,7 @@ def before_prepare describe "mysql", :mysql do before do - ENV["DATABASE_URL"] = "#{MYSQL_BASE_URL}_app" + ENV["DATABASE_URL"] = mysql_url("_app") db_migrate end diff --git a/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb b/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb index a27dfdf5..a423e2a0 100644 --- a/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb +++ b/spec/unit/hanami/cli/commands/app/db/structure/load_spec.rb @@ -190,8 +190,8 @@ def before_prepare describe "postgres", :postgres do before do - ENV["DATABASE_URL"] = "#{POSTGRES_BASE_URL}_app" - ENV["MAIN__DATABASE_URL"] = "#{POSTGRES_BASE_URL}_main" + ENV["DATABASE_URL"] = postgres_url("_app") + ENV["MAIN__DATABASE_URL"] = postgres_url("_main") db_structure_dump end @@ -249,7 +249,7 @@ def before_prepare describe "mysql", :mysql do before do - ENV["DATABASE_URL"] = "#{MYSQL_BASE_URL}_app" + ENV["DATABASE_URL"] = mysql_url("_app") db_structure_dump end From 7771d2b1b27f2da0a4bf74cf5b0d1ea006ef4941 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Sun, 4 Oct 2026 20:26:23 +1100 Subject: [PATCH 2/4] Remove readline gem We added it so pry could load on Ruby 4.0, where readline is a bundled gem rather than a default gem. However, pry 0.16 now depends on reline primarily, we no longer need readline. Removing readline is helpful because it was interfering with the JRuby builds, where it installed by did not actually define the `Readline` class, which left pry with no input. --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index 7d05977d..2143d36f 100644 --- a/Gemfile +++ b/Gemfile @@ -49,7 +49,6 @@ gem "hanami-devtools", github: "hanami/devtools", branch: "main" group :test do gem "pry" - gem "readline" gem "rspec", "~> 3.9" gem "ostruct", require: false end From c6fabdb7605f75cbd31da4c79f97e67b5b85eec9 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Sun, 4 Oct 2026 18:09:36 +1100 Subject: [PATCH 3/4] Spawn server in server spec, since JRuby has no fork --- .../hanami/cli/commands/app/server_spec.rb | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/spec/integration/hanami/cli/commands/app/server_spec.rb b/spec/integration/hanami/cli/commands/app/server_spec.rb index d2b3ce1d..03d97717 100644 --- a/spec/integration/hanami/cli/commands/app/server_spec.rb +++ b/spec/integration/hanami/cli/commands/app/server_spec.rb @@ -5,32 +5,30 @@ require "puma" RSpec.describe Hanami::CLI::Commands::App::Server do - subject { described_class.new } - it "starts rack server in the given environment" do host = ENV.fetch("HANAMI_CLI_TEST_HOST", "0.0.0.0") port = ENV.fetch("HANAMI_CLI_TEST_PORT", "2300") + app_root = File.join(__dir__, "../../../../../fixtures/test") begin - pid = fork do - $stdout.reopen "/dev/null", "a" - $stderr.reopen "/dev/null", "a" - subject.call( - config: File.join(File.dirname(__FILE__), "../../../../../fixtures/test/config.ru"), - host: host, - port: port, - env: "staging" - ) - end + # Spawn a new process rather than forking, since fork is not available on JRuby + pid = Process.spawn( + "bin/hanami", "server", "--host=#{host}", "--port=#{port}", "--env=staging", + chdir: app_root, out: File::NULL, err: File::NULL + ) response = open_uri("http://#{host}:#{port}/") expect(response).to eq("Hello, world! (staging)") ensure - Process.kill(:KILL, pid) + if pid + Process.kill(:KILL, pid) + Process.wait(pid) + end end end - def open_uri(uri, attempts = 5) + # Allow time for the new process to boot (including on JRuby) + def open_uri(uri, attempts = 30) URI.open(uri).read # rubocop:disable Security/Open rescue Errno::ECONNREFUSED raise if attempts.zero? From 02c0a5e279c99e63989551b1be59fb1d71eb92a5 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Sun, 4 Oct 2026 17:43:41 +1100 Subject: [PATCH 4/4] Expect JRuby to pass in CI --- .github/workflows/ci.yml | 7 +------ repo-sync.yml | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d21e370..f166385c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,7 @@ jobs: - "4.0" - "3.4" - "3.3" + - "jruby" rack: - "~> 2.0" - "~> 3.0" @@ -34,12 +35,6 @@ jobs: - ruby: "4.0" rack: "~> 2.0" coverage: "true" - - ruby: "jruby" - rack: "~> 2.0" - optional: true - - ruby: "jruby" - rack: "~> 3.0" - optional: true env: COVERAGE: ${{ matrix.coverage }} RACK_MATRIX_VALUE: ${{ matrix.rack || '' }} diff --git a/repo-sync.yml b/repo-sync.yml index c4c4a859..a5e462b5 100644 --- a/repo-sync.yml +++ b/repo-sync.yml @@ -6,7 +6,6 @@ github_org: hanami ci: matrix: rack: ["~> 2.0", "~> 3.0"] - jruby: false gemspec: authors: ["Hanakai team"] email: ["info@hanakai.org"]