Skip to content

Commit 5b884d4

Browse files
committed
Fixed #29501 -- Allowed dbshell to pass options to underlying tool.
1 parent 8e8c3f9 commit 5b884d4

File tree

13 files changed

+117
-21
lines changed

13 files changed

+117
-21
lines changed

django/core/management/commands/dbshell.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import subprocess
2+
13
from django.core.management.base import BaseCommand, CommandError
24
from django.db import DEFAULT_DB_ALIAS, connections
35

@@ -15,11 +17,13 @@ def add_arguments(self, parser):
1517
'--database', default=DEFAULT_DB_ALIAS,
1618
help='Nominates a database onto which to open a shell. Defaults to the "default" database.',
1719
)
20+
parameters = parser.add_argument_group('parameters', prefix_chars='--')
21+
parameters.add_argument('parameters', nargs='*')
1822

1923
def handle(self, **options):
2024
connection = connections[options['database']]
2125
try:
22-
connection.client.runshell()
26+
connection.client.runshell(options['parameters'])
2327
except FileNotFoundError:
2428
# Note that we're assuming the FileNotFoundError relates to the
2529
# command missing. It could be raised for some other reason, in
@@ -29,3 +33,11 @@ def handle(self, **options):
2933
'You appear not to have the %r program installed or on your path.' %
3034
connection.client.executable_name
3135
)
36+
except subprocess.CalledProcessError as e:
37+
raise CommandError(
38+
'"%s" returned non-zero exit status %s.' % (
39+
' '.join(e.cmd),
40+
e.returncode,
41+
),
42+
returncode=e.returncode,
43+
)

django/db/backends/base/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ def __init__(self, connection):
88
# connection is an instance of BaseDatabaseWrapper.
99
self.connection = connection
1010

11-
def runshell(self):
11+
def runshell(self, parameters):
1212
raise NotImplementedError('subclasses of BaseDatabaseClient must provide a runshell() method')

django/db/backends/mysql/client.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class DatabaseClient(BaseDatabaseClient):
77
executable_name = 'mysql'
88

99
@classmethod
10-
def settings_to_cmd_args(cls, settings_dict):
10+
def settings_to_cmd_args(cls, settings_dict, parameters):
1111
args = [cls.executable_name]
1212
db = settings_dict['OPTIONS'].get('db', settings_dict['NAME'])
1313
user = settings_dict['OPTIONS'].get('user', settings_dict['USER'])
@@ -41,8 +41,9 @@ def settings_to_cmd_args(cls, settings_dict):
4141
args += ["--ssl-key=%s" % client_key]
4242
if db:
4343
args += [db]
44+
args.extend(parameters)
4445
return args
4546

46-
def runshell(self):
47-
args = DatabaseClient.settings_to_cmd_args(self.connection.settings_dict)
47+
def runshell(self, parameters):
48+
args = DatabaseClient.settings_to_cmd_args(self.connection.settings_dict, parameters)
4849
subprocess.run(args, check=True)

django/db/backends/mysql/creation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ def _clone_test_db(self, suffix, verbosity, keepdb=False):
5555
self._clone_db(source_database_name, target_database_name)
5656

5757
def _clone_db(self, source_database_name, target_database_name):
58-
dump_args = DatabaseClient.settings_to_cmd_args(self.connection.settings_dict)[1:]
58+
dump_args = DatabaseClient.settings_to_cmd_args(self.connection.settings_dict, [])[1:]
5959
dump_cmd = ['mysqldump', *dump_args[:-1], '--routines', '--events', source_database_name]
60-
load_cmd = DatabaseClient.settings_to_cmd_args(self.connection.settings_dict)
60+
load_cmd = DatabaseClient.settings_to_cmd_args(self.connection.settings_dict, [])
6161
load_cmd[-1] = target_database_name
6262

6363
with subprocess.Popen(dump_cmd, stdout=subprocess.PIPE) as dump_proc:

django/db/backends/oracle/client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ class DatabaseClient(BaseDatabaseClient):
88
executable_name = 'sqlplus'
99
wrapper_name = 'rlwrap'
1010

11-
def runshell(self):
11+
def runshell(self, parameters):
1212
conn_string = self.connection._connect_string()
1313
args = [self.executable_name, "-L", conn_string]
1414
wrapper_path = shutil.which(self.wrapper_name)
1515
if wrapper_path:
1616
args = [wrapper_path, *args]
17+
args.extend(parameters)
1718
subprocess.run(args, check=True)

django/db/backends/postgresql/client.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class DatabaseClient(BaseDatabaseClient):
99
executable_name = 'psql'
1010

1111
@classmethod
12-
def runshell_db(cls, conn_params):
12+
def runshell_db(cls, conn_params, parameters):
1313
args = [cls.executable_name]
1414

1515
host = conn_params.get('host', '')
@@ -29,6 +29,7 @@ def runshell_db(cls, conn_params):
2929
if port:
3030
args += ['-p', str(port)]
3131
args += [dbname]
32+
args.extend(parameters)
3233

3334
sigint_handler = signal.getsignal(signal.SIGINT)
3435
subprocess_env = os.environ.copy()
@@ -50,5 +51,5 @@ def runshell_db(cls, conn_params):
5051
# Restore the original SIGINT handler.
5152
signal.signal(signal.SIGINT, sigint_handler)
5253

53-
def runshell(self):
54-
DatabaseClient.runshell_db(self.connection.get_connection_params())
54+
def runshell(self, parameters):
55+
self.runshell_db(self.connection.get_connection_params(), parameters)

django/db/backends/sqlite3/client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
class DatabaseClient(BaseDatabaseClient):
77
executable_name = 'sqlite3'
88

9-
def runshell(self):
9+
def runshell(self, parameters):
1010
# TODO: Remove str() when dropping support for PY37.
1111
# args parameter accepts path-like objects on Windows since Python 3.8.
1212
args = [self.executable_name,
1313
str(self.connection.settings_dict['NAME'])]
14+
args.extend(parameters)
1415
subprocess.run(args, check=True)

docs/ref/django-admin.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,33 @@ program manually.
226226

227227
Specifies the database onto which to open a shell. Defaults to ``default``.
228228

229+
.. django-admin-option:: -- ARGUMENTS
230+
231+
.. versionadded:: 3.1
232+
233+
Any arguments following a ``--`` divider will be passed on to the underlying
234+
command-line client. For example, with PostgreSQL you can use the ``psql``
235+
command's ``-c`` flag to execute a raw SQL query directly:
236+
237+
.. console::
238+
239+
$ django-admin dbshell -- -c 'select current_user'
240+
current_user
241+
--------------
242+
postgres
243+
(1 row)
244+
245+
On MySQL/MariaDB, you can do this with the ``mysql`` command's ``-e`` flag:
246+
247+
.. console::
248+
249+
$ django-admin dbshell -- -e "select user()"
250+
+----------------------+
251+
| user() |
252+
+----------------------+
253+
| djangonaut@localhost |
254+
+----------------------+
255+
229256
.. note::
230257

231258
Be aware that not all options set in the :setting:`OPTIONS` part of your

docs/releases/3.1.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,9 @@ Management Commands
317317
:attr:`~django.core.management.CommandError` allows customizing the exit
318318
status for management commands.
319319

320+
* The new :option:`dbshell -- ARGUMENTS <dbshell -->` option allows passing
321+
extra arguments to the command-line client for the database.
322+
320323
Migrations
321324
~~~~~~~~~~
322325

@@ -505,6 +508,9 @@ backends.
505508
yields a cursor and automatically closes the cursor and connection upon
506509
exiting the ``with`` statement.
507510

511+
* ``DatabaseClient.runshell()`` now requires an additional ``parameters``
512+
argument as a list of extra arguments to pass on to the command-line client.
513+
508514
Dropped support for MariaDB 10.1
509515
--------------------------------
510516

tests/dbshell/test_mysql.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,23 @@ def test_ssl_certificate_is_added(self):
7676
},
7777
}))
7878

79-
def get_command_line_arguments(self, connection_settings):
80-
return DatabaseClient.settings_to_cmd_args(connection_settings)
79+
def test_parameters(self):
80+
self.assertEqual(
81+
['mysql', 'somedbname', '--help'],
82+
self.get_command_line_arguments(
83+
{
84+
'NAME': 'somedbname',
85+
'USER': None,
86+
'PASSWORD': None,
87+
'HOST': None,
88+
'PORT': None,
89+
'OPTIONS': {},
90+
},
91+
['--help'],
92+
),
93+
)
94+
95+
def get_command_line_arguments(self, connection_settings, parameters=None):
96+
if parameters is None:
97+
parameters = []
98+
return DatabaseClient.settings_to_cmd_args(connection_settings, parameters)

0 commit comments

Comments
 (0)