diff --git a/django/contrib/postgres/apps.py b/django/contrib/postgres/apps.py index a8ee3fbf4b..63fd9b274e 100644 --- a/django/contrib/postgres/apps.py +++ b/django/contrib/postgres/apps.py @@ -65,6 +65,11 @@ class PostgresConfig(AppConfig): 3910: "django.contrib.postgres.fields.DateTimeRangeField", 3912: "django.contrib.postgres.fields.DateRangeField", 3926: "django.contrib.postgres.fields.BigIntegerRangeField", + # PostgreSQL OIDs may vary depending on the + # installation, especially for datatypes from + # extensions, e.g. "hstore". In such cases, the + # type_display attribute (psycopg 3.2+) should be used. + "hstore": "django.contrib.postgres.fields.HStoreField", } ) if conn.connection is not None: diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index 17ac417fff..e2728afea5 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -8,6 +8,7 @@ import asyncio import threading import warnings from contextlib import contextmanager +from functools import lru_cache from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -29,6 +30,7 @@ except ImportError: raise ImproperlyConfigured("Error loading psycopg2 or psycopg module") +@lru_cache def psycopg_version(): version = Database.__version__.split(" ", 1)[0] return get_version_tuple(version) diff --git a/django/db/backends/postgresql/introspection.py b/django/db/backends/postgresql/introspection.py index 30edaf10da..791d729ea4 100644 --- a/django/db/backends/postgresql/introspection.py +++ b/django/db/backends/postgresql/introspection.py @@ -3,6 +3,7 @@ from collections import namedtuple from django.db.backends.base.introspection import BaseDatabaseIntrospection from django.db.backends.base.introspection import FieldInfo as BaseFieldInfo from django.db.backends.base.introspection import TableInfo as BaseTableInfo +from django.db.backends.postgresql.base import psycopg_version from django.db.models import DB_CASCADE, DB_SET_DEFAULT, DB_SET_NULL, DO_NOTHING, Index FieldInfo = namedtuple("FieldInfo", [*BaseFieldInfo._fields, "is_autofield", "comment"]) @@ -120,10 +121,19 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): cursor.execute( "SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name) ) + + # PostgreSQL OIDs may vary depending on the installation, especially + # for datatypes from extensions, e.g. "hstore". In such cases, the + # type_display attribute (psycopg 3.2+) should be used. + type_display_available = psycopg_version() >= (3, 2) return [ FieldInfo( line.name, - line.type_code, + ( + line.type_display + if type_display_available and line.type_display == "hstore" + else line.type_code + ), # display_size is always None on psycopg2. line.internal_size if line.display_size is None else line.display_size, line.internal_size, diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt index e0edf6876a..8c5594d5b6 100644 --- a/docs/releases/6.1.txt +++ b/docs/releases/6.1.txt @@ -126,7 +126,9 @@ Minor features :mod:`django.contrib.postgres` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* :djadmin:`inspectdb` now introspects + :class:`~django.contrib.postgres.fields.HStoreField` when ``psycopg`` 3.2+ is + installed and ``django.contrib.postgres`` is in :setting:`INSTALLED_APPS`. :mod:`django.contrib.redirects` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/backends/postgresql/tests.py b/tests/backends/postgresql/tests.py index f7e7d1e68c..e03c37cb57 100644 --- a/tests/backends/postgresql/tests.py +++ b/tests/backends/postgresql/tests.py @@ -517,8 +517,11 @@ class Tests(TestCase): def test_correct_extraction_psycopg_version(self): from django.db.backends.postgresql.base import Database, psycopg_version + psycopg_version.cache_clear() with mock.patch.object(Database, "__version__", "4.2.1 (dt dec pq3 ext lo64)"): + self.addCleanup(psycopg_version.cache_clear) self.assertEqual(psycopg_version(), (4, 2, 1)) + psycopg_version.cache_clear() with mock.patch.object( Database, "__version__", "4.2b0.dev1 (dt dec pq3 ext lo64)" ): diff --git a/tests/postgres_tests/test_introspection.py b/tests/postgres_tests/test_introspection.py index 73c426d1ba..1ae940fdb9 100644 --- a/tests/postgres_tests/test_introspection.py +++ b/tests/postgres_tests/test_introspection.py @@ -33,3 +33,16 @@ class InspectDBTests(PostgreSQLTestCase): "null=True)", ], ) + + def test_hstore_field(self): + from django.db.backends.postgresql.base import psycopg_version + + if psycopg_version() < (3, 2): + self.skipTest("psycopg 3.2+ is required.") + self.assertFieldsInModel( + "postgres_tests_hstoremodel", + [ + "field = django.contrib.postgres.fields.HStoreField(blank=True, " + "null=True)", + ], + )