pythondjangopostgresqlorm

Constraint to forbid NaN in postgres numeric columns using Django ORM


Postgresql allows NaN values in numeric columns according to its documentation here.

When defining Postgres tables using Django ORM, a DecimalField is translated to numeric column in Postgres. Even if you define the column as bellow:

from django.db import models

# You can insert NaN to this column without any issue
numeric_field = models.DecimalField(max_digits=32, decimal_places=8, blank=False, null=False)

Is there a way to use Python/Django syntax to forbid NaN values in this scenario? The Postgres native solution is to probably use some kind of constraint. But is that possible using Django syntax?

Edit: As willeM_ Van Onsem pointed out, Django does not allow NaN to be inserted to DecimalField natively. However, the DB is manipulated from other sources as well, hence, the need to have an extra constraint at the DB level (as opposed to Django's built-in application level constraint).


Solution

  • I don't have a PostgreSQL database to test against but you can try creating a database constraint using a lookup based on the IsNull looukup:

    from decimal import Decimal
    from django.db.models import (
        CheckConstraint,
        DecimalField,
        Field,
        Model,
        Q,
    )
    from django.db.models.lookups import (
        BuiltinLookup,
    )
    
    
    @Field.register_lookup
    class IsNaN(BuiltinLookup):
        lookup_name = "isnan"
        prepare_rhs = False
    
        def as_sql(self, compiler, connection):
            if not isinstance(self.rhs, bool):
                raise ValueError(
                    "The QuerySet value for an isnan lookup must be True or False."
                )
            sql, params = self.process_lhs(compiler, connection)
            if self.rhs:
                return "%s = 'NaN'" % sql, params
            else:
                return "%s <> 'NaN'" % sql, params
    
    
    class Item(Model):
        numeric_field = DecimalField(
            max_digits=32,
            decimal_places=8,
            blank=False,
            null=False,
        )
    
        class Meta:
            constraints = [
                CheckConstraint(
                    check=Q(numeric_field__isnan=False),
                    name="numeric_field_not_isnan",
                ),
            ]