Skip to content
Browse files

Fixed creation of ArrayField(RangeField) instances

This patch integrates and updates the contributions from edrmp and
dvarrazzo from Django ticket #24726. The tests pass, but I'm not sure I
put the psycopg2 adapter bits in the right place. I guess this is more a
proof of concept.

I think there needs to be more discussion about how arrays of range
fields should work. Lookups don't work right now, and I'm not sure how
they would.

Also, dvarrazzo's suggestion to subclass NumericRange works, but
wouldn't that require range objects destined for the database to be
created with the adapted subclasses? I'm not sure if that's desired for
Django. I'm interested to hear what the core team thinks.

Anyway, I hope this patch is helpful!
  • Loading branch information...
adamzap committed Aug 5, 2016
1 parent e139ef5 commit 718bfaa18a12fb059d9e5cf8c4c716402ac8cb4d
@@ -1,5 +1,8 @@
import json

import psycopg2.extensions

from psycopg2._range import NumberRangeAdapter
from psycopg2.extras import DateRange, DateTimeTZRange, NumericRange, Range

from django.contrib.postgres import forms, lookups
@@ -14,6 +17,27 @@

class TypedNumericRange(NumericRange):
pg_type = None

class Int4NumericRange(TypedNumericRange):
pg_type = b'int4range'

class Int8NumericRange(TypedNumericRange):
pg_type = b'int8range'

class TypedNumericRangeAdapter(NumberRangeAdapter):
def getquoted(self):
return super(TypedNumericRangeAdapter, self).getquoted() + b'::' + self.adapted.pg_type

psycopg2.extensions.register_adapter(Int4NumericRange, TypedNumericRangeAdapter)
psycopg2.extensions.register_adapter(Int8NumericRange, TypedNumericRangeAdapter)

class RangeField(models.Field):
empty_strings_allowed = False

@@ -84,7 +108,7 @@ def formfield(self, **kwargs):

class IntegerRangeField(RangeField):
base_field = models.IntegerField
range_type = NumericRange
range_type = Int4NumericRange
form_field = forms.IntegerRangeField

def db_type(self, connection):
@@ -93,7 +117,7 @@ def db_type(self, connection):

class BigIntegerRangeField(RangeField):
base_field = models.BigIntegerField
range_type = NumericRange
range_type = Int8NumericRange
form_field = forms.IntegerRangeField

def db_type(self, connection):
@@ -215,6 +215,18 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('int_ranges', ArrayField(IntegerRangeField(), null=True, blank=True)),
('bigint_ranges', ArrayField(BigIntegerRangeField(), null=True, blank=True)),
'required_db_vendor': 'postgresql',

pg_94_operations = [
@@ -128,6 +128,11 @@ class RangeLookupsModel(PostgreSQLModel):
date = models.DateField(blank=True, null=True)

class IntegerRangesArrayModel(PostgreSQLModel):
int_ranges = ArrayField(IntegerRangeField(), blank=True, null=True)
bigint_ranges = ArrayField(BigIntegerRangeField(), blank=True, null=True)

# Only create this model for postgres >= 9.4
if connection.vendor == 'postgresql' and connection.pg_version >= 90400:
class JSONModel(models.Model):
@@ -14,11 +14,12 @@
from . import PostgreSQLTestCase
from .models import (
ArrayFieldSubclass, CharArrayModel, DateTimeArrayModel, IntegerArrayModel,
NestedIntegerArrayModel, NullableIntegerArrayModel, OtherTypesArrayModel,
PostgreSQLModel, Tag,
IntegerRangesArrayModel, NestedIntegerArrayModel, NullableIntegerArrayModel,
OtherTypesArrayModel, PostgreSQLModel, Tag,

from psycopg2.extras import NumericRange
from django.contrib.postgres.fields import ArrayField
from django.contrib.postgres.forms import SimpleArrayField, SplitArrayField
except ImportError:
@@ -117,6 +118,35 @@ def test_model_set_on_base_field(self):
self.assertEqual(field.model, IntegerArrayModel)
self.assertEqual(field.base_field.model, IntegerArrayModel)

def test_integer_ranges_passed_as_tuples(self):
instance = IntegerRangesArrayModel(
int_ranges=[(10, 20), (30, 40)],
bigint_ranges=[(7000000000, 10000000000), (50000000000, 70000000000)]
loaded = IntegerRangesArrayModel.objects.get()

instance_int_range_objs = [NumericRange(*t) for t in instance.int_ranges]
instance_bigint_range_objs = [NumericRange(*t) for t in instance.bigint_ranges]

self.assertEqual(instance_int_range_objs, loaded.int_ranges)
self.assertEqual(instance_bigint_range_objs, loaded.bigint_ranges)

def test_integer_ranges_passed_as_numericrange_instances(self):
from django.contrib.postgres.fields import ranges

instance = IntegerRangesArrayModel(
int_ranges=[ranges.Int4NumericRange(10, 20), ranges.Int4NumericRange(30, 40)],
ranges.Int8NumericRange(7000000000, 10000000000),
ranges.Int8NumericRange(50000000000, 70000000000)
loaded = IntegerRangesArrayModel.objects.get()
self.assertEqual(instance.int_ranges, loaded.int_ranges)
self.assertEqual(instance.bigint_ranges, loaded.bigint_ranges)

class TestQuerying(PostgreSQLTestCase):

0 comments on commit 718bfaa

Please sign in to comment.
You can’t perform that action at this time.