From 3a97912f3ded1b32e6f79f90890aa94f8c6782c0 Mon Sep 17 00:00:00 2001 From: Bertrand Bordage Date: Mon, 28 Dec 2015 03:14:57 +0100 Subject: [PATCH] Handles and tests JSONField. --- cachalot/tests/migrations/0001_initial.py | 15 ++- cachalot/tests/models.py | 6 ++ cachalot/tests/postgres.py | 106 +++++++++++++++++++++- cachalot/utils.py | 4 +- 4 files changed, 123 insertions(+), 8 deletions(-) diff --git a/cachalot/tests/migrations/0001_initial.py b/cachalot/tests/migrations/0001_initial.py index 502fb5e..fc55ba5 100644 --- a/cachalot/tests/migrations/0001_initial.py +++ b/cachalot/tests/migrations/0001_initial.py @@ -56,16 +56,16 @@ if django_version[:2] >= (1, 8): from django.contrib.postgres.fields import ( ArrayField, HStoreField, IntegerRangeField, FloatRangeField, DateRangeField, DateTimeRangeField) + from django.contrib.postgres.operations import ( + HStoreExtension, UnaccentExtension) Migration.operations.extend(( migrations.AddField('Test', 'duration', models.DurationField(null=True, blank=True)), migrations.AddField('Test', 'uuid', models.UUIDField(null=True, blank=True)), - migrations.RunSQL('CREATE EXTENSION hstore;', - hints={'extension': 'hstore'}), - migrations.RunSQL('CREATE EXTENSION unaccent;', - hints={'extension': 'unaccent'}), + HStoreExtension(), + UnaccentExtension(), migrations.CreateModel( name='PostgresModel', fields=[ @@ -82,3 +82,10 @@ if django_version[:2] >= (1, 8): ], ), )) + +if django_version[:2] >= (1, 9): + from django.contrib.postgres.fields import JSONField + Migration.operations.append( + migrations.AddField('PostgresModel', 'json', + JSONField(null=True, blank=True)) + ) diff --git a/cachalot/tests/models.py b/cachalot/tests/models.py index 5b07bf7..a73b043 100644 --- a/cachalot/tests/models.py +++ b/cachalot/tests/models.py @@ -10,6 +10,7 @@ from django.db.models import ( FloatField, DecimalField) DJANGO_GTE_1_8 = django_version[:2] >= (1, 8) +DJANGO_GTE_1_9 = django_version[:2] >= (1, 9) if DJANGO_GTE_1_8: from django.db.models import DurationField, UUIDField @@ -50,6 +51,8 @@ if DJANGO_GTE_1_8: from django.contrib.postgres.fields import ( ArrayField, HStoreField, IntegerRangeField, FloatRangeField, DateRangeField, DateTimeRangeField) + if DJANGO_GTE_1_9: + from django.contrib.postgres.fields import JSONField class PostgresModel(Model): @@ -58,6 +61,9 @@ if DJANGO_GTE_1_8: hstore = HStoreField(null=True, blank=True) + if DJANGO_GTE_1_9: + json = JSONField(null=True, blank=True) + int_range = IntegerRangeField(null=True, blank=True) float_range = FloatRangeField(null=True, blank=True) date_range = DateRangeField(null=True, blank=True) diff --git a/cachalot/tests/postgres.py b/cachalot/tests/postgres.py index 24ffa35..1b124d3 100644 --- a/cachalot/tests/postgres.py +++ b/cachalot/tests/postgres.py @@ -15,6 +15,7 @@ from psycopg2.extras import NumericRange, DateRange, DateTimeTZRange from pytz import timezone DJANGO_GTE_1_8 = django_version[:2] >= (1, 8) +DJANGO_GTE_1_9 = django_version[:2] >= (1, 9) if DJANGO_GTE_1_8: from .models import PostgresModel, Test @@ -28,19 +29,41 @@ if DJANGO_GTE_1_8: @override_settings(USE_TZ=True) class PostgresReadTest(TransactionTestCase): def setUp(self): - PostgresModel.objects.create( + self.obj1 = PostgresModel( int_array=[1, 2, 3], hstore={'a': 'b', 'c': None}, int_range=[1900, 2000], float_range=[-1e3, 9.87654321], date_range=['1678-03-04', '1741-07-28'], datetime_range=[datetime(1989, 1, 30, 12, 20, tzinfo=timezone('Europe/Paris')), None]) - PostgresModel.objects.create( + if DJANGO_GTE_1_9: + self.obj1.json = {'a': 1, 'b': 2} + self.obj1.save() + + self.obj2 = PostgresModel( int_array=[4, None, 6], hstore={'a': '1', 'b': '2'}, int_range=[1989, None], float_range=[0.0, None], date_range=['1989-01-30', None], datetime_range=[None, None]) + if DJANGO_GTE_1_9: + self.obj2.json = [ + 'something', + { + 'a': 1, + 'b': None, + 'c': 123.456, + 'd': True, + 'e': { + 'another': 'dict', + 'and yet': { + 'another': 'one', + 'with a list': [], + }, + }, + }, + ] + self.obj2.save() def test_unaccent(self): Test.objects.create(name='Clémentine') @@ -179,6 +202,85 @@ class PostgresReadTest(TransactionTestCase): self.assertListEqual(data16, data15) self.assertListEqual(data16, [{'a': '1', 'b': '2'}]) + @skipUnless(DJANGO_GTE_1_9, + 'JSON field is only available in Django >= 1.9') + def test_json(self): + qs = PostgresModel.objects.all() + with self.assertNumQueries(1): + data1 = [o.json for o in qs.all()] + with self.assertNumQueries(0): + data2 = [o.json for o in qs.all()] + self.assertListEqual(data2, data1) + self.assertListEqual(data2, [self.obj1.json, self.obj2.json]) + + # Tests an index. + qs = PostgresModel.objects.filter(json__0='something') + with self.assertNumQueries(1): + data3 = [o.json for o in qs.all()] + with self.assertNumQueries(0): + data4 = [o.json for o in qs.all()] + self.assertListEqual(data4, data3) + self.assertListEqual(data4, [self.obj2.json]) + + qs = PostgresModel.objects.filter(json__0__nonexistent_key='something') + with self.assertNumQueries(1): + data5 = [o.json for o in qs.all()] + with self.assertNumQueries(0): + data6 = [o.json for o in qs.all()] + self.assertListEqual(data6, data5) + self.assertListEqual(data6, []) + + # Tests a path with spaces. + qs = PostgresModel.objects.filter( + **{'json__1__e__and yet__another': 'one'}) + with self.assertNumQueries(1): + data7 = [o.json for o in qs.all()] + with self.assertNumQueries(0): + data8 = [o.json for o in qs.all()] + self.assertListEqual(data8, data7) + self.assertListEqual(data8, [self.obj2.json]) + + qs = PostgresModel.objects.filter(json__contains=['something']) + with self.assertNumQueries(1): + data9 = [o.json for o in qs.all()] + with self.assertNumQueries(0): + data10 = [o.json for o in qs.all()] + self.assertListEqual(data10, data9) + self.assertListEqual(data10, [self.obj2.json]) + + qs = PostgresModel.objects.filter( + json__contained_by={'a': 1, 'b': 2, 'any': 'thing'}) + with self.assertNumQueries(1): + data11 = [o.json for o in qs.all()] + with self.assertNumQueries(0): + data12 = [o.json for o in qs.all()] + self.assertListEqual(data12, data11) + self.assertListEqual(data12, [self.obj1.json]) + + qs = PostgresModel.objects.filter(json__has_key='a') + with self.assertNumQueries(1): + data13 = [o.json for o in qs.all()] + with self.assertNumQueries(0): + data14 = [o.json for o in qs.all()] + self.assertListEqual(data14, data13) + self.assertListEqual(data14, [self.obj1.json]) + + qs = PostgresModel.objects.filter(json__has_any_keys=['a', 'b', 'c']) + with self.assertNumQueries(1): + data15 = [o.json for o in qs.all()] + with self.assertNumQueries(0): + data16 = [o.json for o in qs.all()] + self.assertListEqual(data16, data15) + self.assertListEqual(data16, [self.obj1.json]) + + qs = PostgresModel.objects.filter(json__has_keys=['a', 'b']) + with self.assertNumQueries(1): + data17 = [o.json for o in qs.all()] + with self.assertNumQueries(0): + data18 = [o.json for o in qs.all()] + self.assertListEqual(data18, data17) + self.assertListEqual(data18, [self.obj1.json]) + def test_int_range(self): qs = PostgresModel.objects.all() with self.assertNumQueries(1): diff --git a/cachalot/utils.py b/cachalot/utils.py index 0f74370..a5906c6 100644 --- a/cachalot/utils.py +++ b/cachalot/utils.py @@ -33,12 +33,12 @@ CACHABLE_PARAM_TYPES = { try: from psycopg2.extras import ( - NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet) + NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet, Json) except ImportError: pass else: CACHABLE_PARAM_TYPES.update(( - NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet)) + NumericRange, DateRange, DateTimeRange, DateTimeTZRange, Inet, Json)) def check_parameter_types(params):