diff --git a/django/db/backends/oracle/client.py b/django/db/backends/oracle/client.py index 102e77fd159..4c5070a2078 100644 --- a/django/db/backends/oracle/client.py +++ b/django/db/backends/oracle/client.py @@ -1,3 +1,4 @@ +import shutil import subprocess from django.db.backends.base.client import BaseDatabaseClient @@ -5,8 +6,12 @@ from django.db.backends.base.client import BaseDatabaseClient class DatabaseClient(BaseDatabaseClient): executable_name = 'sqlplus' + wrapper_name = 'rlwrap' def runshell(self): conn_string = self.connection._connect_string() args = [self.executable_name, "-L", conn_string] + wrapper_path = shutil.which(self.wrapper_name) + if wrapper_path: + args = [wrapper_path, *args] subprocess.check_call(args) diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index 8e03f54c3ea..5d76289da69 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -185,6 +185,9 @@ Management Commands * :djadmin:`inspectdb` now introspects :class:`~django.db.models.DurationField` for Oracle and PostgreSQL. +* On Oracle, :djadmin:`dbshell` is wrapped with ``rlwrap``, if available. + ``rlwrap`` provides a command history and editing of keyboard input. + Migrations ~~~~~~~~~~ diff --git a/tests/dbshell/test_oracle.py b/tests/dbshell/test_oracle.py new file mode 100644 index 00000000000..d236a932ab7 --- /dev/null +++ b/tests/dbshell/test_oracle.py @@ -0,0 +1,33 @@ +from unittest import mock, skipUnless + +from django.db import connection +from django.db.backends.oracle.client import DatabaseClient +from django.test import SimpleTestCase + + +@skipUnless(connection.vendor == 'oracle', 'Oracle tests') +class OracleDbshellTests(SimpleTestCase): + def _run_dbshell(self, rlwrap=False): + """Run runshell command and capture its arguments.""" + def _mock_subprocess_call(*args): + self.subprocess_args = tuple(*args) + return 0 + + client = DatabaseClient(connection) + self.subprocess_args = None + with mock.patch('subprocess.call', new=_mock_subprocess_call): + with mock.patch('shutil.which', return_value='/usr/bin/rlwrap' if rlwrap else None): + client.runshell() + return self.subprocess_args + + def test_without_rlwrap(self): + self.assertEqual( + self._run_dbshell(rlwrap=False), + ('sqlplus', '-L', connection._connect_string()), + ) + + def test_with_rlwrap(self): + self.assertEqual( + self._run_dbshell(rlwrap=True), + ('/usr/bin/rlwrap', 'sqlplus', '-L', connection._connect_string()), + )