Browse Source

Fixed #31007 -- Allowed specifying type of auto-created primary keys.

This also changes the default type of auto-created primary keys
for new apps and projects to BigAutoField.
Tom Forbes 4 years ago
parent
commit
b5e12d490a

+ 10 - 0
django/apps/config.py

@@ -5,6 +5,7 @@ from importlib import import_module
 
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.deprecation import RemovedInDjango41Warning
+from django.utils.functional import cached_property
 from django.utils.module_loading import import_string, module_has_submodule
 
 APPS_MODULE_NAME = 'apps'
@@ -55,6 +56,15 @@ class AppConfig:
     def __repr__(self):
         return '<%s: %s>' % (self.__class__.__name__, self.label)
 
+    @cached_property
+    def default_auto_field(self):
+        from django.conf import settings
+        return settings.DEFAULT_AUTO_FIELD
+
+    @property
+    def _is_default_auto_field_overridden(self):
+        return self.__class__.default_auto_field is not AppConfig.default_auto_field
+
     def _path_from_module(self, module):
         """Attempt to determine app's filesystem path from its module."""
         # See #21874 for extended discussion of the behavior of this method in

+ 1 - 0
django/conf/app_template/apps.py-tpl

@@ -2,4 +2,5 @@ from django.apps import AppConfig
 
 
 class {{ camel_case_app_name }}Config(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
     name = '{{ app_name }}'

+ 3 - 0
django/conf/global_settings.py

@@ -414,6 +414,9 @@ THOUSAND_SEPARATOR = ','
 DEFAULT_TABLESPACE = ''
 DEFAULT_INDEX_TABLESPACE = ''
 
+# Default primary key field type.
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+
 # Default X-Frame-Options header value
 X_FRAME_OPTIONS = 'DENY'
 

+ 5 - 0
django/conf/project_template/project_name/settings.py-tpl

@@ -118,3 +118,8 @@ USE_TZ = True
 # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
 
 STATIC_URL = '/static/'
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

+ 1 - 0
django/contrib/admin/apps.py

@@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
 class SimpleAdminConfig(AppConfig):
     """Simple AppConfig which does not do automatic discovery."""
 
+    default_auto_field = 'django.db.models.AutoField'
     default_site = 'django.contrib.admin.sites.AdminSite'
     name = 'django.contrib.admin'
     verbose_name = _("Administration")

+ 1 - 0
django/contrib/auth/apps.py

@@ -11,6 +11,7 @@ from .signals import user_logged_in
 
 
 class AuthConfig(AppConfig):
+    default_auto_field = 'django.db.models.AutoField'
     name = 'django.contrib.auth'
     verbose_name = _("Authentication and Authorization")
 

+ 1 - 0
django/contrib/contenttypes/apps.py

@@ -12,6 +12,7 @@ from .management import (
 
 
 class ContentTypesConfig(AppConfig):
+    default_auto_field = 'django.db.models.AutoField'
     name = 'django.contrib.contenttypes'
     verbose_name = _("Content Types")
 

+ 1 - 0
django/contrib/flatpages/apps.py

@@ -3,5 +3,6 @@ from django.utils.translation import gettext_lazy as _
 
 
 class FlatPagesConfig(AppConfig):
+    default_auto_field = 'django.db.models.AutoField'
     name = 'django.contrib.flatpages'
     verbose_name = _("Flat Pages")

+ 1 - 0
django/contrib/gis/apps.py

@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
 
 
 class GISConfig(AppConfig):
+    default_auto_field = 'django.db.models.AutoField'
     name = 'django.contrib.gis'
     verbose_name = _("GIS")
 

+ 1 - 0
django/contrib/redirects/apps.py

@@ -3,5 +3,6 @@ from django.utils.translation import gettext_lazy as _
 
 
 class RedirectsConfig(AppConfig):
+    default_auto_field = 'django.db.models.AutoField'
     name = 'django.contrib.redirects'
     verbose_name = _("Redirects")

+ 1 - 0
django/contrib/sitemaps/apps.py

@@ -3,5 +3,6 @@ from django.utils.translation import gettext_lazy as _
 
 
 class SiteMapsConfig(AppConfig):
+    default_auto_field = 'django.db.models.AutoField'
     name = 'django.contrib.sitemaps'
     verbose_name = _("Site Maps")

+ 1 - 0
django/contrib/sites/apps.py

@@ -8,6 +8,7 @@ from .management import create_default_site
 
 
 class SitesConfig(AppConfig):
+    default_auto_field = 'django.db.models.AutoField'
     name = 'django.contrib.sites'
     verbose_name = _("Sites")
 

+ 25 - 0
django/db/models/base.py

@@ -1290,10 +1290,35 @@ class Model(metaclass=ModelBase):
                 *cls._check_indexes(databases),
                 *cls._check_ordering(),
                 *cls._check_constraints(databases),
+                *cls._check_default_pk(),
             ]
 
         return errors
 
+    @classmethod
+    def _check_default_pk(cls):
+        if (
+            cls._meta.pk.auto_created and
+            not settings.is_overridden('DEFAULT_AUTO_FIELD') and
+            not cls._meta.app_config._is_default_auto_field_overridden
+        ):
+            return [
+                checks.Warning(
+                    f"Auto-created primary key used when not defining a "
+                    f"primary key type, by default "
+                    f"'{settings.DEFAULT_AUTO_FIELD}'.",
+                    hint=(
+                        f"Configure the DEFAULT_AUTO_FIELD setting or the "
+                        f"{cls._meta.app_config.__class__.__qualname__}."
+                        f"default_auto_field attribute to point to a subclass "
+                        f"of AutoField, e.g. 'django.db.models.BigAutoField'."
+                    ),
+                    obj=cls,
+                    id='models.W042',
+                ),
+            ]
+        return []
+
     @classmethod
     def _check_swappable(cls):
         """Check if the swapped model exists."""

+ 35 - 2
django/db/models/options.py

@@ -5,12 +5,13 @@ from collections import defaultdict
 
 from django.apps import apps
 from django.conf import settings
-from django.core.exceptions import FieldDoesNotExist
+from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
 from django.db import connections
 from django.db.models import AutoField, Manager, OrderWrt, UniqueConstraint
 from django.db.models.query_utils import PathInfo
 from django.utils.datastructures import ImmutableList, OrderedSet
 from django.utils.functional import cached_property
+from django.utils.module_loading import import_string
 from django.utils.text import camel_case_to_spaces, format_lazy
 from django.utils.translation import override
 
@@ -217,6 +218,37 @@ class Options:
             new_objs.append(obj)
         return new_objs
 
+    def _get_default_pk_class(self):
+        pk_class_path = getattr(
+            self.app_config,
+            'default_auto_field',
+            settings.DEFAULT_AUTO_FIELD,
+        )
+        if self.app_config and self.app_config._is_default_auto_field_overridden:
+            app_config_class = type(self.app_config)
+            source = (
+                f'{app_config_class.__module__}.'
+                f'{app_config_class.__qualname__}.default_auto_field'
+            )
+        else:
+            source = 'DEFAULT_AUTO_FIELD'
+        if not pk_class_path:
+            raise ImproperlyConfigured(f'{source} must not be empty.')
+        try:
+            pk_class = import_string(pk_class_path)
+        except ImportError as e:
+            msg = (
+                f"{source} refers to the module '{pk_class_path}' that could "
+                f"not be imported."
+            )
+            raise ImproperlyConfigured(msg) from e
+        if not issubclass(pk_class, AutoField):
+            raise ValueError(
+                f"Primary key '{pk_class_path}' referred by {source} must "
+                f"subclass AutoField."
+            )
+        return pk_class
+
     def _prepare(self, model):
         if self.order_with_respect_to:
             # The app registry will not be ready at this point, so we cannot
@@ -250,7 +282,8 @@ class Options:
                 field.primary_key = True
                 self.setup_pk(field)
             else:
-                auto = AutoField(verbose_name='ID', primary_key=True, auto_created=True)
+                pk_class = self._get_default_pk_class()
+                auto = pk_class(verbose_name='ID', primary_key=True, auto_created=True)
                 model.add_to_class('id', auto)
 
     def add_manager(self, manager):

+ 11 - 0
docs/ref/applications.txt

@@ -90,6 +90,7 @@ would provide a proper name for the admin::
     from django.apps import AppConfig
 
     class RockNRollConfig(AppConfig):
+        default_auto_field = 'django.db.models.BigAutoField'
         name = 'rock_n_roll'
         verbose_name = "Rock ’n’ roll"
 
@@ -219,6 +220,16 @@ Configurable attributes
 
     By default, this attribute isn't set.
 
+.. attribute:: AppConfig.default_auto_field
+
+    .. versionadded:: 3.2
+
+    The implicit primary key type to add to models within this app. You can
+    use this to keep :class:`~django.db.models.AutoField` as the primary key
+    type for third party applications.
+
+    By default, this is the value of :setting:`DEFAULT_AUTO_FIELD`.
+
 Read-only attributes
 --------------------
 

+ 2 - 0
docs/ref/checks.txt

@@ -378,6 +378,8 @@ Models
 * **models.W040**: ``<database>`` does not support indexes with non-key
   columns.
 * **models.E041**: ``constraints`` refers to the joined field ``<field name>``.
+* **models.W042**: Auto-created primary key used when not defining a primary
+  key type, by default ``django.db.models.AutoField``.
 
 Security
 --------

+ 11 - 3
docs/ref/models/fields.txt

@@ -415,9 +415,12 @@ cross-site scripting attack.
 If ``True``, this field is the primary key for the model.
 
 If you don't specify ``primary_key=True`` for any field in your model, Django
-will automatically add an :class:`AutoField` to hold the primary key, so you
-don't need to set ``primary_key=True`` on any of your fields unless you want to
-override the default primary-key behavior. For more, see
+will automatically add a field to hold the primary key, so you don't need to
+set ``primary_key=True`` on any of your fields unless you want to override the
+default primary-key behavior. The type of auto-created primary key fields can
+be specified per app in :attr:`AppConfig.default_auto_field
+<django.apps.AppConfig.default_auto_field>` or globally in the
+:setting:`DEFAULT_AUTO_FIELD` setting. For more, see
 :ref:`automatic-primary-key-fields`.
 
 ``primary_key=True`` implies :attr:`null=False <Field.null>` and
@@ -428,6 +431,11 @@ The primary key field is read-only. If you change the value of the primary
 key on an existing object and then save it, a new object will be created
 alongside the old one.
 
+.. versionchanged:: 3.2
+
+    In older versions, auto-created primary key fields were always
+    :class:`AutoField`\s.
+
 ``unique``
 ----------
 

+ 11 - 0
docs/ref/settings.txt

@@ -1245,6 +1245,17 @@ format has higher precedence and will be applied instead.
 See also :setting:`NUMBER_GROUPING`, :setting:`THOUSAND_SEPARATOR` and
 :setting:`USE_THOUSAND_SEPARATOR`.
 
+.. setting:: DEFAULT_AUTO_FIELD
+
+``DEFAULT_AUTO_FIELD``
+----------------------
+
+.. versionadded:: 3.2
+
+Default: ``'``:class:`django.db.models.AutoField`\ ``'``
+
+Default primary key field type to use for models that don't have a field with
+:attr:`primary_key=True <django.db.models.Field.primary_key>`.
 
 .. setting:: DEFAULT_CHARSET
 

+ 42 - 0
docs/releases/3.2.txt

@@ -53,6 +53,48 @@ needed. As a consequence, it's deprecated.
 
 See :ref:`configuring-applications-ref` for full details.
 
+Customizing type of auto-created primary keys
+---------------------------------------------
+
+When defining a model, if no field in a model is defined with
+:attr:`primary_key=True <django.db.models.Field.primary_key>` an implicit
+primary key is added. The type of this implicit primary key can now be
+controlled via the :setting:`DEFAULT_AUTO_FIELD` setting and
+:attr:`AppConfig.default_auto_field <django.apps.AppConfig.default_auto_field>`
+attribute. No more needing to override primary keys in all models.
+
+Maintaining the historical behavior, the default value for
+:setting:`DEFAULT_AUTO_FIELD` is :class:`~django.db.models.AutoField`. Starting
+with 3.2 new projects are generated with :setting:`DEFAULT_AUTO_FIELD` set to
+:class:`~django.db.models.BigAutoField`. Also, new apps are generated with
+:attr:`AppConfig.default_auto_field <django.apps.AppConfig.default_auto_field>`
+set to :class:`~django.db.models.BigAutoField`. In a future Django release the
+default value of :setting:`DEFAULT_AUTO_FIELD` will be changed to
+:class:`~django.db.models.BigAutoField`.
+
+To avoid unwanted migrations in the future, either explicitly set
+:setting:`DEFAULT_AUTO_FIELD` to :class:`~django.db.models.AutoField`::
+
+    DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+
+or configure it on a per-app basis::
+
+    from django.apps import AppConfig
+
+    class MyAppConfig(AppConfig):
+        default_auto_field = 'django.db.models.AutoField'
+        name = 'my_app'
+
+or on a per-model basis::
+
+    from django.db import models
+
+    class MyModel(models.Model):
+        id = models.AutoField(primary_key=True)
+
+In anticipation of the changing default, a system check will provide a warning
+if you do not have an explicit setting for :setting:`DEFAULT_AUTO_FIELD`.
+
 ``pymemcache`` support
 ----------------------
 

+ 10 - 4
docs/topics/db/models.txt

@@ -259,11 +259,12 @@ details can be found in the :ref:`common model field option reference
 Automatic primary key fields
 ----------------------------
 
-By default, Django gives each model the following field::
+By default, Django gives each model an auto-incrementing primary key with the
+type specified per app in :attr:`AppConfig.default_auto_field
+<django.apps.AppConfig.default_auto_field>` or globally in the
+:setting:`DEFAULT_AUTO_FIELD` setting. For example::
 
-    id = models.AutoField(primary_key=True)
-
-This is an auto-incrementing primary key.
+    id = models.BigAutoField(primary_key=True)
 
 If you'd like to specify a custom primary key, specify
 :attr:`primary_key=True <Field.primary_key>` on one of your fields. If Django
@@ -273,6 +274,11 @@ sees you've explicitly set :attr:`Field.primary_key`, it won't add the automatic
 Each model requires exactly one field to have :attr:`primary_key=True
 <Field.primary_key>` (either explicitly declared or automatically added).
 
+.. versionchanged:: 3.2
+
+    In older versions, auto-created primary key fields were always
+    :class:`AutoField`\s.
+
 .. _verbose-field-names:
 
 Verbose field names

+ 15 - 0
tests/admin_scripts/tests.py

@@ -61,6 +61,7 @@ class AdminScriptTestCase(SimpleTestCase):
                 settings_file.write("%s\n" % extra)
             exports = [
                 'DATABASES',
+                'DEFAULT_AUTO_FIELD',
                 'ROOT_URLCONF',
                 'SECRET_KEY',
             ]
@@ -2188,6 +2189,20 @@ class StartApp(AdminScriptTestCase):
             "won't replace conflicting files."
         )
 
+    def test_template(self):
+        out, err = self.run_django_admin(['startapp', 'new_app'])
+        self.assertNoOutput(err)
+        app_path = os.path.join(self.test_dir, 'new_app')
+        self.assertIs(os.path.exists(app_path), True)
+        with open(os.path.join(app_path, 'apps.py')) as f:
+            content = f.read()
+            self.assertIn('class NewAppConfig(AppConfig)', content)
+            self.assertIn(
+                "default_auto_field = 'django.db.models.BigAutoField'",
+                content,
+            )
+            self.assertIn("name = 'new_app'", content)
+
 
 class DiffSettings(AdminScriptTestCase):
     """Tests for diffsettings management command."""

+ 5 - 0
tests/apps/apps.py

@@ -31,3 +31,8 @@ class PlainAppsConfig(AppConfig):
 class RelabeledAppsConfig(AppConfig):
     name = 'apps'
     label = 'relabeled'
+
+
+class ModelPKAppsConfig(AppConfig):
+    name = 'apps'
+    default_auto_field = 'django.db.models.BigAutoField'

+ 26 - 2
tests/apps/tests.py

@@ -102,8 +102,8 @@ class AppsTests(SimpleTestCase):
     def test_no_such_app_config_with_choices(self):
         msg = (
             "Module 'apps.apps' does not contain a 'NoSuchConfig' class. "
-            "Choices are: 'BadConfig', 'MyAdmin', 'MyAuth', 'NoSuchApp', "
-            "'PlainAppsConfig', 'RelabeledAppsConfig'."
+            "Choices are: 'BadConfig', 'ModelPKAppsConfig', 'MyAdmin', "
+            "'MyAuth', 'NoSuchApp', 'PlainAppsConfig', 'RelabeledAppsConfig'."
         )
         with self.assertRaisesMessage(ImportError, msg):
             with self.settings(INSTALLED_APPS=['apps.apps.NoSuchConfig']):
@@ -436,6 +436,30 @@ class AppConfigTests(SimpleTestCase):
         ac = AppConfig('label', Stub(__path__=['a']))
         self.assertEqual(repr(ac), '<AppConfig: label>')
 
+    @override_settings(
+        INSTALLED_APPS=['apps.apps.ModelPKAppsConfig'],
+        DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField',
+    )
+    def test_app_default_auto_field(self):
+        apps_config = apps.get_app_config('apps')
+        self.assertEqual(
+            apps_config.default_auto_field,
+            'django.db.models.BigAutoField',
+        )
+        self.assertIs(apps_config._is_default_auto_field_overridden, True)
+
+    @override_settings(
+        INSTALLED_APPS=['apps.apps.PlainAppsConfig'],
+        DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField',
+    )
+    def test_default_auto_field_setting(self):
+        apps_config = apps.get_app_config('apps')
+        self.assertEqual(
+            apps_config.default_auto_field,
+            'django.db.models.SmallAutoField',
+        )
+        self.assertIs(apps_config._is_default_auto_field_overridden, False)
+
 
 class NamespacePackageAppTests(SimpleTestCase):
     # We need nsapp to be top-level so our multiple-paths tests can add another

+ 10 - 0
tests/check_framework/apps.py

@@ -0,0 +1,10 @@
+from django.apps import AppConfig
+
+
+class CheckDefaultPKConfig(AppConfig):
+    name = 'check_framework'
+
+
+class CheckPKConfig(AppConfig):
+    name = 'check_framework'
+    default_auto_field = 'django.db.models.BigAutoField'

+ 57 - 0
tests/check_framework/test_model_checks.py

@@ -1,3 +1,5 @@
+from unittest import mock
+
 from django.core import checks
 from django.core.checks import Error, Warning
 from django.db import models
@@ -358,3 +360,58 @@ class ConstraintNameTests(TestCase):
                 constraints = [constraint]
 
         self.assertEqual(checks.run_checks(app_configs=apps.get_app_configs()), [])
+
+
+def mocked_is_overridden(self, setting):
+    # Force treating DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' as a not
+    # overridden setting.
+    return (
+        setting != 'DEFAULT_AUTO_FIELD' or
+        self.DEFAULT_AUTO_FIELD != 'django.db.models.AutoField'
+    )
+
+
+@mock.patch('django.conf.UserSettingsHolder.is_overridden', mocked_is_overridden)
+@override_settings(DEFAULT_AUTO_FIELD='django.db.models.AutoField')
+@isolate_apps('check_framework.apps.CheckDefaultPKConfig', attr_name='apps')
+@override_system_checks([checks.model_checks.check_all_models])
+class ModelDefaultAutoFieldTests(SimpleTestCase):
+    def test_auto_created_pk(self):
+        class Model(models.Model):
+            pass
+
+        self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), [
+            Warning(
+                "Auto-created primary key used when not defining a primary "
+                "key type, by default 'django.db.models.AutoField'.",
+                hint=(
+                    "Configure the DEFAULT_AUTO_FIELD setting or the "
+                    "CheckDefaultPKConfig.default_auto_field attribute to "
+                    "point to a subclass of AutoField, e.g. "
+                    "'django.db.models.BigAutoField'."
+                ),
+                obj=Model,
+                id='models.W042',
+            ),
+        ])
+
+    @override_settings(DEFAULT_AUTO_FIELD='django.db.models.BigAutoField')
+    def test_default_auto_field_setting(self):
+        class Model(models.Model):
+            pass
+
+        self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), [])
+
+    def test_explicit_pk(self):
+        class Model(models.Model):
+            id = models.BigAutoField(primary_key=True)
+
+        self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), [])
+
+    @isolate_apps('check_framework.apps.CheckPKConfig', kwarg_name='apps')
+    def test_app_default_auto_field(self, apps):
+        class ModelWithPkViaAppConfig(models.Model):
+            class Meta:
+                app_label = 'check_framework.apps.CheckPKConfig'
+
+        self.assertEqual(checks.run_checks(app_configs=apps.get_app_configs()), [])

+ 25 - 0
tests/model_options/apps.py

@@ -0,0 +1,25 @@
+from django.apps import AppConfig
+
+
+class ModelDefaultPKConfig(AppConfig):
+    name = 'model_options'
+
+
+class ModelPKConfig(AppConfig):
+    name = 'model_options'
+    default_auto_field = 'django.db.models.SmallAutoField'
+
+
+class ModelPKNonAutoConfig(AppConfig):
+    name = 'model_options'
+    default_auto_field = 'django.db.models.TextField'
+
+
+class ModelPKNoneConfig(AppConfig):
+    name = 'model_options'
+    default_auto_field = None
+
+
+class ModelPKNonexistentConfig(AppConfig):
+    name = 'model_options'
+    default_auto_field = 'django.db.models.NonexistentAutoField'

+ 101 - 0
tests/model_options/test_default_pk.py

@@ -0,0 +1,101 @@
+from django.core.exceptions import ImproperlyConfigured
+from django.db import models
+from django.test import SimpleTestCase, override_settings
+from django.test.utils import isolate_apps
+
+
+@isolate_apps('model_options')
+class TestDefaultPK(SimpleTestCase):
+    @override_settings(DEFAULT_AUTO_FIELD='django.db.models.NonexistentAutoField')
+    def test_default_auto_field_setting_nonexistent(self):
+        msg = (
+            "DEFAULT_AUTO_FIELD refers to the module "
+            "'django.db.models.NonexistentAutoField' that could not be "
+            "imported."
+        )
+        with self.assertRaisesMessage(ImproperlyConfigured, msg):
+            class Model(models.Model):
+                pass
+
+    @isolate_apps('model_options.apps.ModelPKNonexistentConfig')
+    def test_app_default_auto_field_nonexistent(self):
+        msg = (
+            "model_options.apps.ModelPKNonexistentConfig.default_auto_field "
+            "refers to the module 'django.db.models.NonexistentAutoField' "
+            "that could not be imported."
+        )
+        with self.assertRaisesMessage(ImproperlyConfigured, msg):
+            class Model(models.Model):
+                pass
+
+    @override_settings(DEFAULT_AUTO_FIELD='django.db.models.TextField')
+    def test_default_auto_field_setting_non_auto(self):
+        msg = (
+            "Primary key 'django.db.models.TextField' referred by "
+            "DEFAULT_AUTO_FIELD must subclass AutoField."
+        )
+        with self.assertRaisesMessage(ValueError, msg):
+            class Model(models.Model):
+                pass
+
+    @isolate_apps('model_options.apps.ModelPKNonAutoConfig')
+    def test_app_default_auto_field_non_auto(self):
+        msg = (
+            "Primary key 'django.db.models.TextField' referred by "
+            "model_options.apps.ModelPKNonAutoConfig.default_auto_field must "
+            "subclass AutoField."
+        )
+        with self.assertRaisesMessage(ValueError, msg):
+            class Model(models.Model):
+                pass
+
+    @override_settings(DEFAULT_AUTO_FIELD=None)
+    def test_default_auto_field_setting_none(self):
+        msg = 'DEFAULT_AUTO_FIELD must not be empty.'
+        with self.assertRaisesMessage(ImproperlyConfigured, msg):
+            class Model(models.Model):
+                pass
+
+    @isolate_apps('model_options.apps.ModelPKNoneConfig')
+    def test_app_default_auto_field_none(self):
+        msg = (
+            'model_options.apps.ModelPKNoneConfig.default_auto_field must not '
+            'be empty.'
+        )
+        with self.assertRaisesMessage(ImproperlyConfigured, msg):
+            class Model(models.Model):
+                pass
+
+    @isolate_apps('model_options.apps.ModelDefaultPKConfig')
+    @override_settings(DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField')
+    def test_default_auto_field_setting(self):
+        class Model(models.Model):
+            pass
+
+        self.assertIsInstance(Model._meta.pk, models.SmallAutoField)
+
+    @isolate_apps('model_options.apps.ModelPKConfig')
+    @override_settings(DEFAULT_AUTO_FIELD='django.db.models.AutoField')
+    def test_app_default_auto_field(self):
+        class Model(models.Model):
+            pass
+
+        self.assertIsInstance(Model._meta.pk, models.SmallAutoField)
+
+    @isolate_apps('model_options.apps.ModelDefaultPKConfig')
+    @override_settings(DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField')
+    def test_m2m_default_auto_field_setting(self):
+        class M2MModel(models.Model):
+            m2m = models.ManyToManyField('self')
+
+        m2m_pk = M2MModel._meta.get_field('m2m').remote_field.through._meta.pk
+        self.assertIsInstance(m2m_pk, models.SmallAutoField)
+
+    @isolate_apps('model_options.apps.ModelPKConfig')
+    @override_settings(DEFAULT_AUTO_FIELD='django.db.models.AutoField')
+    def test_m2m_app_default_auto_field(self):
+        class M2MModel(models.Model):
+            m2m = models.ManyToManyField('self')
+
+        m2m_pk = M2MModel._meta.get_field('m2m').remote_field.through._meta.pk
+        self.assertIsInstance(m2m_pk, models.SmallAutoField)

+ 2 - 0
tests/test_sqlite.py

@@ -27,3 +27,5 @@ SECRET_KEY = "django_tests_secret_key"
 PASSWORD_HASHERS = [
     'django.contrib.auth.hashers.MD5PasswordHasher',
 ]
+
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'