diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 9eac005dd19..7926c05c09c 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -152,10 +152,14 @@ class DatabaseWrapper(BaseDatabaseWrapper): def get_connection_params(self): settings_dict = self.settings_dict # None may be used to connect to the default 'postgres' db - if settings_dict['NAME'] == '': + if ( + settings_dict['NAME'] == '' and + not settings_dict.get('OPTIONS', {}).get('service') + ): raise ImproperlyConfigured( "settings.DATABASES is improperly configured. " - "Please supply the NAME value.") + "Please supply the NAME or OPTIONS['service'] value." + ) if len(settings_dict['NAME'] or '') > self.ops.max_name_length(): raise ImproperlyConfigured( "The database name '%s' (%d characters) is longer than " @@ -166,10 +170,19 @@ class DatabaseWrapper(BaseDatabaseWrapper): self.ops.max_name_length(), ) ) - conn_params = { - 'database': settings_dict['NAME'] or 'postgres', - **settings_dict['OPTIONS'], - } + conn_params = {} + if settings_dict['NAME']: + conn_params = { + 'database': settings_dict['NAME'], + **settings_dict['OPTIONS'], + } + elif settings_dict['NAME'] is None: + # Connect to the default 'postgres' db. + settings_dict.get('OPTIONS', {}).pop('service', None) + conn_params = {'database': 'postgres', **settings_dict['OPTIONS']} + else: + conn_params = {**settings_dict['OPTIONS']} + conn_params.pop('isolation_level', None) if settings_dict['USER']: conn_params['user'] = settings_dict['USER'] diff --git a/django/db/backends/postgresql/client.py b/django/db/backends/postgresql/client.py index 79654011632..2339880967b 100644 --- a/django/db/backends/postgresql/client.py +++ b/django/db/backends/postgresql/client.py @@ -16,6 +16,7 @@ class DatabaseClient(BaseDatabaseClient): dbname = settings_dict.get('NAME') or 'postgres' user = settings_dict.get('USER') passwd = settings_dict.get('PASSWORD') + service = options.get('service') sslmode = options.get('sslmode') sslrootcert = options.get('sslrootcert') sslcert = options.get('sslcert') @@ -33,6 +34,8 @@ class DatabaseClient(BaseDatabaseClient): env = {} if passwd: env['PGPASSWORD'] = str(passwd) + if service: + env['PGSERVICE'] = str(service) if sslmode: env['PGSSLMODE'] = str(sslmode) if sslrootcert: diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index a9edf43c542..b9a82140a09 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -108,11 +108,43 @@ required, though the latest release is recommended. .. _psycopg2: https://www.psycopg.org/ +.. _postgresql-connection-settings: + PostgreSQL connection settings ------------------------------- See :setting:`HOST` for details. +To connect using a service name from the `connection service file`_, you must +specify it in the :setting:`OPTIONS` part of your database configuration in +:setting:`DATABASES`: + +.. code-block:: python + :caption: settings.py + + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'OPTIONS': {'service': 'my_service'}, + } + } + +.. code-block:: text + :caption: .pg_service.conf + + [my_service] + host=localhost + user=USER + dbname=NAME + password=PASSWORD + port=5432 + +.. _connection service file: https://www.postgresql.org/docs/current/libpq-pgservice.html + +.. versionchanged:: 4.0 + + Support for connecting by a service name was added. + Optimizing PostgreSQL's configuration ------------------------------------- diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index d2684a9a2e6..1e408c97bda 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -64,7 +64,8 @@ Minor features :mod:`django.contrib.postgres` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* The PostgreSQL backend now supports connecting by a service name. See + :ref:`postgresql-connection-settings` for more details. :mod:`django.contrib.redirects` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/backends/postgresql/tests.py b/tests/backends/postgresql/tests.py index 8d0a801ea29..94ebfbdaf8d 100644 --- a/tests/backends/postgresql/tests.py +++ b/tests/backends/postgresql/tests.py @@ -68,6 +68,36 @@ class Tests(TestCase): with self.assertRaisesMessage(ImproperlyConfigured, msg): DatabaseWrapper(settings).get_connection_params() + def test_database_name_empty(self): + from django.db.backends.postgresql.base import DatabaseWrapper + settings = connection.settings_dict.copy() + settings['NAME'] = '' + msg = ( + "settings.DATABASES is improperly configured. Please supply the " + "NAME or OPTIONS['service'] value." + ) + with self.assertRaisesMessage(ImproperlyConfigured, msg): + DatabaseWrapper(settings).get_connection_params() + + def test_service_name(self): + from django.db.backends.postgresql.base import DatabaseWrapper + settings = connection.settings_dict.copy() + settings['OPTIONS'] = {'service': 'my_service'} + settings['NAME'] = '' + params = DatabaseWrapper(settings).get_connection_params() + self.assertEqual(params['service'], 'my_service') + self.assertNotIn('database', params) + + def test_service_name_default_db(self): + # None is used to connect to the default 'postgres' db. + from django.db.backends.postgresql.base import DatabaseWrapper + settings = connection.settings_dict.copy() + settings['NAME'] = None + settings['OPTIONS'] = {'service': 'django_test'} + params = DatabaseWrapper(settings).get_connection_params() + self.assertEqual(params['database'], 'postgres') + self.assertNotIn('service', params) + def test_connect_and_rollback(self): """ PostgreSQL shouldn't roll back SET TIME ZONE, even if the first diff --git a/tests/dbshell/test_postgresql.py b/tests/dbshell/test_postgresql.py index ccf49d7e50c..e9eb131db75 100644 --- a/tests/dbshell/test_postgresql.py +++ b/tests/dbshell/test_postgresql.py @@ -67,6 +67,12 @@ class PostgreSqlDbshellCommandTestCase(SimpleTestCase): ) ) + def test_service(self): + self.assertEqual( + self.settings_to_cmd_args_env({'OPTIONS': {'service': 'django_test'}}), + (['psql', 'postgres'], {'PGSERVICE': 'django_test'}), + ) + def test_column(self): self.assertEqual( self.settings_to_cmd_args_env({