Skip to content

Fixed #35957 -- Allowed auto fields to be used in composite primary keys. #19585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

shangxiao
Copy link
Contributor

@shangxiao shangxiao commented Jun 23, 2025

Trac ticket number

ticket-35957

Branch description

Allow auto fields to be used as part of a composite PK:

  • It appears all that was necessary was to remove the check and to remove setting the primary_key from the deconstruction...
  • SQLite does not allow this, ref https://www.sqlite.org/autoinc.html - whilst not explicitly stated it only talks about using AUTOINCREMENT after PRIMARY KEY on the column declaration

Checklist

  • This PR targets the main branch.
  • The commit message is written in past tense, mentions the ticket number, and ends with a period.
  • I have checked the "Has patch" ticket flag in the Trac system.
  • I have added or updated relevant tests.
  • I have added or updated relevant docs, including release notes if applicable.
  • I have attached screenshots in both light and dark modes for any UI changes.

@@ -2792,14 +2792,24 @@ def __init__(self, *args, **kwargs):
def check(self, **kwargs):
return [
*super().check(**kwargs),
*self._check_primary_key(),
*self._check_primary_key(kwargs.get("databases", ["default"])),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the tests I saw was passing databases kwarg into check() but in another test it was absent. Is this a correct assumption here that databases may be passed in?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should default to an empty list, see how CharField.check does it

def check(self, **kwargs):
databases = kwargs.get("databases") or []
return [
*super().check(**kwargs),
*self._check_db_collation(databases),
*self._check_max_length_attribute(**kwargs),
]

Comment on lines 2802 to 2805
all(
connections[db].features.supports_autofields_in_composite_pk
for db in databases
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simon had recently mentioned that only checking the default connection's features was harmful in a multi-engine setup. Is this what we want to do moving forward or just continue to use the default connection? If so we may need to abstract this as it will catch folks out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose what I should instead be doing here is iterating over connections in full 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also just wonder whether we may want to add something like this to Field:

    @cached_property
    def part_of_pk(self):
        return self.primary_key or self in getattr(self.model._meta.pk, "pk", [])

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what we want to do moving forward or just continue to use the default connection? If so we may need to abstract this as it will catch folks out.

We should most definitely not use the default connection. If not specified the check should not be performed.

Comment on lines +65 to +71
class AutoId(models.Model):
pk = models.CompositePrimaryKey("id", "name")
id = models.BigAutoField()
name = models.CharField(max_length=255)

class Meta:
required_db_features = ["supports_autofields_in_composite_pk"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does someone know how to skip checks when required_db_features is set? My tests were failing because it was still failing the check when run with SQLite.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refer to how Constraint.check is implemented

e.g.

if not (
connection.features.supports_table_check_constraints
or "supports_table_check_constraints" in model._meta.required_db_features
):

@@ -220,6 +220,12 @@ def test_model_forms(self):
):
self.assertIsNone(modelform_factory(Comment, fields=["pk"]))

def test_autoid_in_composite_pk(self):
obj = AutoId.objects.create(name="Automatically generated ID")
self.assertEqual(obj.id, 1)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the correct way to check an auto field's initial value?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert it's not None?

connections[db].features.supports_autofields_in_composite_pk
for db in databases
)
and self in getattr(self.model._meta.pk, "fields", [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
and self in getattr(self.model._meta.pk, "fields", [])
and self in self.model._meta.pk_fields

@@ -2792,14 +2792,24 @@ def __init__(self, *args, **kwargs):
def check(self, **kwargs):
return [
*super().check(**kwargs),
*self._check_primary_key(),
*self._check_primary_key(kwargs.get("databases", ["default"])),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should default to an empty list, see how CharField.check does it

def check(self, **kwargs):
databases = kwargs.get("databases") or []
return [
*super().check(**kwargs),
*self._check_db_collation(databases),
*self._check_max_length_attribute(**kwargs),
]

Comment on lines 2802 to 2805
all(
connections[db].features.supports_autofields_in_composite_pk
for db in databases
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this what we want to do moving forward or just continue to use the default connection? If so we may need to abstract this as it will catch folks out.

We should most definitely not use the default connection. If not specified the check should not be performed.

Comment on lines +65 to +71
class AutoId(models.Model):
pk = models.CompositePrimaryKey("id", "name")
id = models.BigAutoField()
name = models.CharField(max_length=255)

class Meta:
required_db_features = ["supports_autofields_in_composite_pk"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refer to how Constraint.check is implemented

e.g.

if not (
connection.features.supports_table_check_constraints
or "supports_table_check_constraints" in model._meta.required_db_features
):

@@ -220,6 +220,12 @@ def test_model_forms(self):
):
self.assertIsNone(modelform_factory(Comment, fields=["pk"]))

def test_autoid_in_composite_pk(self):
obj = AutoId.objects.create(name="Automatically generated ID")
self.assertEqual(obj.id, 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert it's not None?

@shangxiao
Copy link
Contributor Author

Thanks Simon, have applied your suggestions 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants