Skip to content

Commit 4bcb8e4

Browse files
Move dbconsole logic to Active Record connection adapter.
Instead of hosting the connection logic in the command object, the database adapter should be responsible for connecting to a console session. This patch moves #find_cmd_and_exec to the adapter and exposes a new API to lookup the adapter class without instantiating it. Co-authored-by: Paarth Madan <[email protected]>
1 parent c267be4 commit 4bcb8e4

File tree

12 files changed

+393
-259
lines changed

12 files changed

+393
-259
lines changed

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,39 @@ def self.quoted_table_names # :nodoc:
8989
@quoted_table_names ||= {}
9090
end
9191

92+
def self.find_cmd_and_exec(commands, *args) # :doc:
93+
commands = Array(commands)
94+
95+
dirs_on_path = ENV["PATH"].to_s.split(File::PATH_SEPARATOR)
96+
unless (ext = RbConfig::CONFIG["EXEEXT"]).empty?
97+
commands = commands.map { |cmd| "#{cmd}#{ext}" }
98+
end
99+
100+
full_path_command = nil
101+
found = commands.detect do |cmd|
102+
dirs_on_path.detect do |path|
103+
full_path_command = File.join(path, cmd)
104+
begin
105+
stat = File.stat(full_path_command)
106+
rescue SystemCallError
107+
else
108+
stat.file? && stat.executable?
109+
end
110+
end
111+
end
112+
113+
if found
114+
exec full_path_command, *args
115+
else
116+
abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.")
117+
end
118+
end
119+
120+
# Opens a database console session.
121+
def self.dbconsole(config, options = {})
122+
raise NotImplementedError
123+
end
124+
92125
def initialize(config_or_deprecated_connection, deprecated_logger = nil, deprecated_connection_options = nil, deprecated_config = nil) # :nodoc:
93126
super()
94127

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,36 @@ def dealloc(stmt)
5151
end
5252
end
5353

54+
class << self
55+
def dbconsole(config, options = {})
56+
mysql_config = config.configuration_hash
57+
58+
args = {
59+
host: "--host",
60+
port: "--port",
61+
socket: "--socket",
62+
username: "--user",
63+
encoding: "--default-character-set",
64+
sslca: "--ssl-ca",
65+
sslcert: "--ssl-cert",
66+
sslcapath: "--ssl-capath",
67+
sslcipher: "--ssl-cipher",
68+
sslkey: "--ssl-key",
69+
ssl_mode: "--ssl-mode"
70+
}.filter_map { |opt, arg| "#{arg}=#{mysql_config[opt]}" if mysql_config[opt] }
71+
72+
if mysql_config[:password] && options[:include_password]
73+
args << "--password=#{mysql_config[:password]}"
74+
elsif mysql_config[:password] && !mysql_config[:password].to_s.empty?
75+
args << "-p"
76+
end
77+
78+
args << config.database
79+
80+
find_cmd_and_exec(["mysql", "mysql5"], *args)
81+
end
82+
end
83+
5484
def get_database_version # :nodoc:
5585
full_version_string = get_full_version
5686
version_string = version_string(full_version_string)

activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88

99
module ActiveRecord
1010
module ConnectionHandling # :nodoc:
11+
def mysql2_connection_class
12+
ConnectionAdapters::Mysql2Adapter
13+
end
14+
1115
# Establishes a connection to the database that's used by all Active Record objects.
1216
def mysql2_connection(config)
13-
ConnectionAdapters::Mysql2Adapter.new(config)
17+
mysql2_connection_class.new(config)
1418
end
1519
end
1620

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@
2121

2222
module ActiveRecord
2323
module ConnectionHandling # :nodoc:
24+
def postgresql_connection_class
25+
ConnectionAdapters::PostgreSQLAdapter
26+
end
27+
2428
# Establishes a connection to the database that's used by all Active Record objects
2529
def postgresql_connection(config)
26-
ConnectionAdapters::PostgreSQLAdapter.new(config)
30+
postgresql_connection_class.new(config)
2731
end
2832
end
2933

@@ -72,6 +76,25 @@ def new_client(conn_params)
7276
raise ActiveRecord::ConnectionNotEstablished, error.message
7377
end
7478
end
79+
80+
def dbconsole(config, options = {})
81+
pg_config = config.configuration_hash
82+
83+
ENV["PGUSER"] = pg_config[:username] if pg_config[:username]
84+
ENV["PGHOST"] = pg_config[:host] if pg_config[:host]
85+
ENV["PGPORT"] = pg_config[:port].to_s if pg_config[:port]
86+
ENV["PGPASSWORD"] = pg_config[:password].to_s if pg_config[:password] && options[:include_password]
87+
ENV["PGSSLMODE"] = pg_config[:sslmode].to_s if pg_config[:sslmode]
88+
ENV["PGSSLCERT"] = pg_config[:sslcert].to_s if pg_config[:sslcert]
89+
ENV["PGSSLKEY"] = pg_config[:sslkey].to_s if pg_config[:sslkey]
90+
ENV["PGSSLROOTCERT"] = pg_config[:sslrootcert].to_s if pg_config[:sslrootcert]
91+
if pg_config[:variables]
92+
ENV["PGOPTIONS"] = pg_config[:variables].filter_map do |name, value|
93+
"-c #{name}=#{value.to_s.gsub(/[ \\]/, '\\\\\0')}" unless value == ":default" || value == :default
94+
end.join(" ")
95+
end
96+
find_cmd_and_exec("psql", config.database)
97+
end
7598
end
7699

77100
##

activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515

1616
module ActiveRecord
1717
module ConnectionHandling # :nodoc:
18+
def sqlite3_connection_class
19+
ConnectionAdapters::SQLite3Adapter
20+
end
21+
1822
def sqlite3_connection(config)
19-
ConnectionAdapters::SQLite3Adapter.new(config)
23+
sqlite3_connection_class.new(config)
2024
end
2125
end
2226

@@ -40,6 +44,16 @@ def new_client(config)
4044
raise
4145
end
4246
end
47+
48+
def dbconsole(config, options = {})
49+
args = []
50+
51+
args << "-#{options[:mode]}" if options[:mode]
52+
args << "-header" if options[:header]
53+
args << File.expand_path(config.database, Rails.respond_to?(:root) ? Rails.root : nil)
54+
55+
find_cmd_and_exec("sqlite3", *args)
56+
end
4357
end
4458

4559
include SQLite3::Quoting

activerecord/lib/active_record/database_configurations/database_config.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ def adapter_method
1717
"#{adapter}_connection"
1818
end
1919

20+
def adapter_class_method
21+
"#{adapter}_connection_class"
22+
end
23+
2024
def host
2125
raise NotImplementedError
2226
end
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# frozen_string_literal: true
2+
3+
require "cases/helper"
4+
require "active_support/testing/method_call_assertions"
5+
6+
module ActiveRecord
7+
module ConnectionAdapters
8+
class Mysql2DbConsoleTest < ActiveRecord::Mysql2TestCase
9+
include ActiveSupport::Testing::MethodCallAssertions
10+
11+
def test_mysql
12+
config = make_db_config(adapter: "mysql2", database: "db")
13+
14+
assert_find_cmd_and_exec_called_with([%w[mysql mysql5], "db"]) do
15+
Mysql2Adapter.dbconsole(config)
16+
end
17+
end
18+
19+
def test_mysql_full
20+
config = make_db_config(
21+
adapter: "mysql2",
22+
database: "db",
23+
host: "localhost",
24+
port: 1234,
25+
socket: "socket",
26+
username: "user",
27+
password: "qwerty",
28+
encoding: "UTF-8",
29+
sslca: "/path/to/ca-cert.pem",
30+
sslcert: "/path/to/client-cert.pem",
31+
sslcapath: "/path/to/cacerts",
32+
sslcipher: "DHE-RSA-AES256-SHA",
33+
sslkey: "/path/to/client-key.pem",
34+
ssl_mode: "VERIFY_IDENTITY"
35+
)
36+
37+
args = [
38+
%w[mysql mysql5],
39+
"--host=localhost",
40+
"--port=1234",
41+
"--socket=socket",
42+
"--user=user",
43+
"--default-character-set=UTF-8",
44+
"--ssl-ca=/path/to/ca-cert.pem",
45+
"--ssl-cert=/path/to/client-cert.pem",
46+
"--ssl-capath=/path/to/cacerts",
47+
"--ssl-cipher=DHE-RSA-AES256-SHA",
48+
"--ssl-key=/path/to/client-key.pem",
49+
"--ssl-mode=VERIFY_IDENTITY",
50+
"-p", "db"
51+
]
52+
53+
assert_find_cmd_and_exec_called_with(args) do
54+
Mysql2Adapter.dbconsole(config)
55+
end
56+
end
57+
58+
def test_mysql_include_password
59+
config = make_db_config(adapter: "mysql2", database: "db", username: "user", password: "qwerty")
60+
61+
assert_find_cmd_and_exec_called_with([%w[mysql mysql5], "--user=user", "--password=qwerty", "db"]) do
62+
Mysql2Adapter.dbconsole(config, include_password: true)
63+
end
64+
end
65+
66+
private
67+
def make_db_config(config)
68+
ActiveRecord::DatabaseConfigurations::HashConfig.new("test", "primary", config)
69+
end
70+
71+
def assert_find_cmd_and_exec_called_with(args, &block)
72+
assert_called_with(Mysql2Adapter, :find_cmd_and_exec, args, &block)
73+
end
74+
end
75+
end
76+
end
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# frozen_string_literal: true
2+
3+
require "cases/helper"
4+
require "active_support/testing/method_call_assertions"
5+
6+
module ActiveRecord
7+
module ConnectionAdapters
8+
class PostgresqlDbConsoleTest < ActiveRecord::PostgreSQLTestCase
9+
include ActiveSupport::Testing::MethodCallAssertions
10+
11+
ENV_VARS = %w(PGUSER PGHOST PGPORT PGPASSWORD PGSSLMODE PGSSLCERT PGSSLKEY PGSSLROOTCERT PGOPTIONS)
12+
13+
def run(*)
14+
preserve_pg_env do
15+
super
16+
end
17+
end
18+
19+
def test_postgresql
20+
config = make_db_config(adapter: "postgresql", database: "db")
21+
22+
assert_find_cmd_and_exec_called_with(["psql", "db"]) do
23+
PostgreSQLAdapter.dbconsole(config)
24+
end
25+
end
26+
27+
def test_postgresql_full
28+
config = make_db_config(
29+
adapter: "postgresql",
30+
database: "db",
31+
username: "user",
32+
password: "q1w2e3",
33+
host: "host",
34+
port: 5432,
35+
)
36+
37+
assert_find_cmd_and_exec_called_with(["psql", "db"]) do
38+
PostgreSQLAdapter.dbconsole(config)
39+
end
40+
41+
assert_equal "user", ENV["PGUSER"]
42+
assert_equal "host", ENV["PGHOST"]
43+
assert_equal "5432", ENV["PGPORT"]
44+
assert_not_equal "q1w2e3", ENV["PGPASSWORD"]
45+
end
46+
47+
def test_postgresql_with_ssl
48+
config = make_db_config(adapter: "postgresql", database: "db", sslmode: "verify-full", sslcert: "client.crt", sslkey: "client.key", sslrootcert: "root.crt")
49+
50+
assert_find_cmd_and_exec_called_with(["psql", "db"]) do
51+
PostgreSQLAdapter.dbconsole(config)
52+
end
53+
54+
assert_equal "verify-full", ENV["PGSSLMODE"]
55+
assert_equal "client.crt", ENV["PGSSLCERT"]
56+
assert_equal "client.key", ENV["PGSSLKEY"]
57+
assert_equal "root.crt", ENV["PGSSLROOTCERT"]
58+
end
59+
60+
def test_postgresql_include_password
61+
config = make_db_config(adapter: "postgresql", database: "db", username: "user", password: "q1w2e3")
62+
63+
assert_find_cmd_and_exec_called_with(["psql", "db"]) do
64+
PostgreSQLAdapter.dbconsole(config, include_password: true)
65+
end
66+
67+
assert_equal "user", ENV["PGUSER"]
68+
assert_equal "q1w2e3", ENV["PGPASSWORD"]
69+
end
70+
71+
def test_postgresql_include_variables
72+
config = make_db_config(adapter: "postgresql", database: "db", variables: { search_path: "my_schema, default, \\my_schema", statement_timeout: 5000, lock_timeout: ":default" })
73+
74+
assert_find_cmd_and_exec_called_with(["psql", "db"]) do
75+
PostgreSQLAdapter.dbconsole(config)
76+
end
77+
78+
assert_equal "-c search_path=my_schema,\\ default,\\ \\\\my_schema -c statement_timeout=5000", ENV["PGOPTIONS"]
79+
end
80+
81+
private
82+
def preserve_pg_env
83+
old_values = ENV_VARS.map { |var| ENV[var] }
84+
yield
85+
ensure
86+
ENV_VARS.zip(old_values).each { |var, value| ENV[var] = value }
87+
end
88+
89+
def make_db_config(config)
90+
ActiveRecord::DatabaseConfigurations::HashConfig.new("test", "primary", config)
91+
end
92+
93+
def assert_find_cmd_and_exec_called_with(args, &block)
94+
assert_called_with(PostgreSQLAdapter, :find_cmd_and_exec, args, &block)
95+
end
96+
end
97+
end
98+
end

0 commit comments

Comments
 (0)