Django 4.2, is a 2nd autofield-like field on a model possible?

  • we use UUIDField for all primary keys on our models
  • we have a requirement to have an autoincrementing field as well

One cannot simply add an AutoField to the model, as it is required to be primary key, and we already have a primary key.

Have added a PositiveBigIntegerField for this purpose, and added a manual migration step to add a sequence/identity and apply it to the field. However, when an instance of the model is saved, it complains about a NULL value.

Assuming this is the correct approach, what is actually needed is for the compiled INSERT statement to either

  • not include the sequenced column in the insert (but still return the generated value via RETURNING) or
  • leverage the SQL DEFAULT keyword.

The latter feels like the correct approach, but I’m at a loss for how to achieve this. The objective is to transparently create a database-saved instance of this model with the ORM python instance of it containing the generated value (like you would see with an AutoField as PK).

See the ticket #8576 (Multiple AutoFields in a model) – Django and the thread at Rationale for unique AutoField's

@jaddison you might also be interested in this accepted feature request to include a non-primary key serial field to contrib.postgres.

With the recent extended support for db_returning, generated, and db_default in main I suspect this one might be easier to implement now if you want to have a shot at it. Happy to support you through code reviews.

To add support for it to 4.2 you’ll likely have to tweak a few things, I suggest you have a look at how db_default was implemented in 5.0. It includes the introduction of a DatabaseDefault expression which you might be able to draw inspiration from by having the field pre_save return an instance of. From there setting db_returning on your field subclass should do the trick :crossed_fingers:.

Hope it helps!

1 Like

@charettes thank you - that was the breadcrumb I needed. The following rough implementation works:

class Default(Expression):
    def as_sql(self, compiler, connection):
        return "DEFAULT", []


class AutoPositiveBigIntegerField(models.PositiveBigIntegerField):
    @property
    def db_returning(self):
        return True

    def get_default(self):
        return Default()


class Invoice(models.Model):
    ...
    number = AutoPositiveBigIntegerField(unique=True)

The above, coupled with the following manual migration on the number field:

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ("billing", "0003_my_migration"),
    ]

    operations = [
        migrations.RunSQL(
            "ALTER TABLE invoice ALTER COLUMN number ADD GENERATED BY DEFAULT AS IDENTITY;",
            reverse_sql="ALTER TABLE invoice ALTER COLUMN number DROP IDENTITY IF EXISTS;"
        ),
    ]

A coworker (thanks Mark!) pointedout that I could go one step further and remove the need for the custom migration by overriding db_type_suffix:

class Default(Expression):
    def as_sql(self, compiler, connection):
        return "DEFAULT", []


class AutoPositiveBigIntegerField(models.PositiveBigIntegerField):
    @property
    def db_returning(self):
        return True

    def get_default(self):
        return Default()

    def db_type_suffix(self, connection):
        return "GENERATED BY DEFAULT AS IDENTITY"


class Invoice(models.Model):
    ...
    number = AutoPositiveBigIntegerField(unique=True)

@jaddison there is now some work on this: Fixed #27452 -- Added serial fields to contrib.postgres. by csirmazbendeguz · Pull Request #18123 · django/django · GitHub

It would be really great to have any input you might have on the review.