summaryrefslogtreecommitdiff
path: root/tests/postgres_tests/test_array.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/postgres_tests/test_array.py')
-rw-r--r--tests/postgres_tests/test_array.py389
1 files changed, 389 insertions, 0 deletions
diff --git a/tests/postgres_tests/test_array.py b/tests/postgres_tests/test_array.py
new file mode 100644
index 0000000000..35ea65480a
--- /dev/null
+++ b/tests/postgres_tests/test_array.py
@@ -0,0 +1,389 @@
+import unittest
+
+from django.contrib.postgres.fields import ArrayField
+from django.contrib.postgres.forms import SimpleArrayField, SplitArrayField
+from django.core import exceptions, serializers
+from django.db import models, IntegrityError, connection
+from django.db.migrations.writer import MigrationWriter
+from django import forms
+from django.test import TestCase
+from django.utils import timezone
+
+from .models import IntegerArrayModel, NullableIntegerArrayModel, CharArrayModel, DateTimeArrayModel, NestedIntegerArrayModel
+
+
+@unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL required')
+class TestSaveLoad(TestCase):
+
+ def test_integer(self):
+ instance = IntegerArrayModel(field=[1, 2, 3])
+ instance.save()
+ loaded = IntegerArrayModel.objects.get()
+ self.assertEqual(instance.field, loaded.field)
+
+ def test_char(self):
+ instance = CharArrayModel(field=['hello', 'goodbye'])
+ instance.save()
+ loaded = CharArrayModel.objects.get()
+ self.assertEqual(instance.field, loaded.field)
+
+ def test_dates(self):
+ instance = DateTimeArrayModel(field=[timezone.now()])
+ instance.save()
+ loaded = DateTimeArrayModel.objects.get()
+ self.assertEqual(instance.field, loaded.field)
+
+ def test_tuples(self):
+ instance = IntegerArrayModel(field=(1,))
+ instance.save()
+ loaded = IntegerArrayModel.objects.get()
+ self.assertSequenceEqual(instance.field, loaded.field)
+
+ def test_integers_passed_as_strings(self):
+ # This checks that get_prep_value is deferred properly
+ instance = IntegerArrayModel(field=['1'])
+ instance.save()
+ loaded = IntegerArrayModel.objects.get()
+ self.assertEqual(loaded.field, [1])
+
+ def test_null_handling(self):
+ instance = NullableIntegerArrayModel(field=None)
+ instance.save()
+ loaded = NullableIntegerArrayModel.objects.get()
+ self.assertEqual(instance.field, loaded.field)
+
+ instance = IntegerArrayModel(field=None)
+ with self.assertRaises(IntegrityError):
+ instance.save()
+
+ def test_nested(self):
+ instance = NestedIntegerArrayModel(field=[[1, 2], [3, 4]])
+ instance.save()
+ loaded = NestedIntegerArrayModel.objects.get()
+ self.assertEqual(instance.field, loaded.field)
+
+
+@unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL required')
+class TestQuerying(TestCase):
+
+ def setUp(self):
+ self.objs = [
+ NullableIntegerArrayModel.objects.create(field=[1]),
+ NullableIntegerArrayModel.objects.create(field=[2]),
+ NullableIntegerArrayModel.objects.create(field=[2, 3]),
+ NullableIntegerArrayModel.objects.create(field=[20, 30, 40]),
+ NullableIntegerArrayModel.objects.create(field=None),
+ ]
+
+ def test_exact(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__exact=[1]),
+ self.objs[:1]
+ )
+
+ def test_isnull(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__isnull=True),
+ self.objs[-1:]
+ )
+
+ def test_gt(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__gt=[0]),
+ self.objs[:4]
+ )
+
+ def test_lt(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__lt=[2]),
+ self.objs[:1]
+ )
+
+ def test_in(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__in=[[1], [2]]),
+ self.objs[:2]
+ )
+
+ def test_contained_by(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__contained_by=[1, 2]),
+ self.objs[:2]
+ )
+
+ def test_contains(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__contains=[2]),
+ self.objs[1:3]
+ )
+
+ def test_index(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__0=2),
+ self.objs[1:3]
+ )
+
+ def test_index_chained(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__0__lt=3),
+ self.objs[0:3]
+ )
+
+ def test_index_nested(self):
+ instance = NestedIntegerArrayModel.objects.create(field=[[1, 2], [3, 4]])
+ self.assertSequenceEqual(
+ NestedIntegerArrayModel.objects.filter(field__0__0=1),
+ [instance]
+ )
+
+ @unittest.expectedFailure
+ def test_index_used_on_nested_data(self):
+ instance = NestedIntegerArrayModel.objects.create(field=[[1, 2], [3, 4]])
+ self.assertSequenceEqual(
+ NestedIntegerArrayModel.objects.filter(field__0=[1, 2]),
+ [instance]
+ )
+
+ def test_overlap(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__overlap=[1, 2]),
+ self.objs[0:3]
+ )
+
+ def test_len(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__len__lte=2),
+ self.objs[0:3]
+ )
+
+ def test_slice(self):
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__0_1=[2]),
+ self.objs[1:3]
+ )
+
+ self.assertSequenceEqual(
+ NullableIntegerArrayModel.objects.filter(field__0_2=[2, 3]),
+ self.objs[2:3]
+ )
+
+ @unittest.expectedFailure
+ def test_slice_nested(self):
+ instance = NestedIntegerArrayModel.objects.create(field=[[1, 2], [3, 4]])
+ self.assertSequenceEqual(
+ NestedIntegerArrayModel.objects.filter(field__0__0_1=[1]),
+ [instance]
+ )
+
+
+class TestChecks(TestCase):
+
+ def test_field_checks(self):
+ field = ArrayField(models.CharField())
+ field.set_attributes_from_name('field')
+ errors = field.check()
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].id, 'postgres.E001')
+
+ def test_invalid_base_fields(self):
+ field = ArrayField(models.ManyToManyField('postgres_tests.IntegerArrayModel'))
+ field.set_attributes_from_name('field')
+ errors = field.check()
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].id, 'postgres.E002')
+
+
+class TestMigrations(TestCase):
+
+ def test_deconstruct(self):
+ field = ArrayField(models.IntegerField())
+ name, path, args, kwargs = field.deconstruct()
+ new = ArrayField(*args, **kwargs)
+ self.assertEqual(type(new.base_field), type(field.base_field))
+
+ def test_deconstruct_with_size(self):
+ field = ArrayField(models.IntegerField(), size=3)
+ name, path, args, kwargs = field.deconstruct()
+ new = ArrayField(*args, **kwargs)
+ self.assertEqual(new.size, field.size)
+
+ def test_deconstruct_args(self):
+ field = ArrayField(models.CharField(max_length=20))
+ name, path, args, kwargs = field.deconstruct()
+ new = ArrayField(*args, **kwargs)
+ self.assertEqual(new.base_field.max_length, field.base_field.max_length)
+
+ def test_makemigrations(self):
+ field = ArrayField(models.CharField(max_length=20))
+ statement, imports = MigrationWriter.serialize(field)
+ self.assertEqual(statement, 'django.contrib.postgres.fields.ArrayField(models.CharField(max_length=20), size=None)')
+
+
+@unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL required')
+class TestSerialization(TestCase):
+ test_data = '[{"fields": {"field": "[\\"1\\", \\"2\\"]"}, "model": "postgres_tests.integerarraymodel", "pk": null}]'
+
+ def test_dumping(self):
+ instance = IntegerArrayModel(field=[1, 2])
+ data = serializers.serialize('json', [instance])
+ self.assertEqual(data, self.test_data)
+
+ def test_loading(self):
+ instance = list(serializers.deserialize('json', self.test_data))[0].object
+ self.assertEqual(instance.field, [1, 2])
+
+
+class TestValidation(TestCase):
+
+ def test_unbounded(self):
+ field = ArrayField(models.IntegerField())
+ with self.assertRaises(exceptions.ValidationError) as cm:
+ field.clean([1, None], None)
+ self.assertEqual(cm.exception.code, 'item_invalid')
+ self.assertEqual(cm.exception.message % cm.exception.params, 'Item 1 in the array did not validate: This field cannot be null.')
+
+ def test_blank_true(self):
+ field = ArrayField(models.IntegerField(blank=True, null=True))
+ # This should not raise a validation error
+ field.clean([1, None], None)
+
+ def test_with_size(self):
+ field = ArrayField(models.IntegerField(), size=3)
+ field.clean([1, 2, 3], None)
+ with self.assertRaises(exceptions.ValidationError) as cm:
+ field.clean([1, 2, 3, 4], None)
+ self.assertEqual(cm.exception.messages[0], 'List contains 4 items, it should contain no more than 3.')
+
+ def test_nested_array_mismatch(self):
+ field = ArrayField(ArrayField(models.IntegerField()))
+ field.clean([[1, 2], [3, 4]], None)
+ with self.assertRaises(exceptions.ValidationError) as cm:
+ field.clean([[1, 2], [3, 4, 5]], None)
+ self.assertEqual(cm.exception.code, 'nested_array_mismatch')
+ self.assertEqual(cm.exception.messages[0], 'Nested arrays must have the same length.')
+
+
+class TestSimpleFormField(TestCase):
+
+ def test_valid(self):
+ field = SimpleArrayField(forms.CharField())
+ value = field.clean('a,b,c')
+ self.assertEqual(value, ['a', 'b', 'c'])
+
+ def test_to_python_fail(self):
+ field = SimpleArrayField(forms.IntegerField())
+ with self.assertRaises(exceptions.ValidationError) as cm:
+ field.clean('a,b,9')
+ self.assertEqual(cm.exception.messages[0], 'Item 0 in the array did not validate: Enter a whole number.')
+
+ def test_validate_fail(self):
+ field = SimpleArrayField(forms.CharField(required=True))
+ with self.assertRaises(exceptions.ValidationError) as cm:
+ field.clean('a,b,')
+ self.assertEqual(cm.exception.messages[0], 'Item 2 in the array did not validate: This field is required.')
+
+ def test_validators_fail(self):
+ field = SimpleArrayField(forms.RegexField('[a-e]{2}'))
+ with self.assertRaises(exceptions.ValidationError) as cm:
+ field.clean('a,bc,de')
+ self.assertEqual(cm.exception.messages[0], 'Item 0 in the array did not validate: Enter a valid value.')
+
+ def test_delimiter(self):
+ field = SimpleArrayField(forms.CharField(), delimiter='|')
+ value = field.clean('a|b|c')
+ self.assertEqual(value, ['a', 'b', 'c'])
+
+ def test_delimiter_with_nesting(self):
+ field = SimpleArrayField(SimpleArrayField(forms.CharField()), delimiter='|')
+ value = field.clean('a,b|c,d')
+ self.assertEqual(value, [['a', 'b'], ['c', 'd']])
+
+ def test_prepare_value(self):
+ field = SimpleArrayField(forms.CharField())
+ value = field.prepare_value(['a', 'b', 'c'])
+ self.assertEqual(value, 'a,b,c')
+
+ def test_max_length(self):
+ field = SimpleArrayField(forms.CharField(), max_length=2)
+ with self.assertRaises(exceptions.ValidationError) as cm:
+ field.clean('a,b,c')
+ self.assertEqual(cm.exception.messages[0], 'List contains 3 items, it should contain no more than 2.')
+
+ def test_min_length(self):
+ field = SimpleArrayField(forms.CharField(), min_length=4)
+ with self.assertRaises(exceptions.ValidationError) as cm:
+ field.clean('a,b,c')
+ self.assertEqual(cm.exception.messages[0], 'List contains 3 items, it should contain no fewer than 4.')
+
+ def test_required(self):
+ field = SimpleArrayField(forms.CharField(), required=True)
+ with self.assertRaises(exceptions.ValidationError) as cm:
+ field.clean('')
+ self.assertEqual(cm.exception.messages[0], 'This field is required.')
+
+ def test_model_field_formfield(self):
+ model_field = ArrayField(models.CharField(max_length=27))
+ form_field = model_field.formfield()
+ self.assertIsInstance(form_field, SimpleArrayField)
+ self.assertIsInstance(form_field.base_field, forms.CharField)
+ self.assertEqual(form_field.base_field.max_length, 27)
+
+ def test_model_field_formfield_size(self):
+ model_field = ArrayField(models.CharField(max_length=27), size=4)
+ form_field = model_field.formfield()
+ self.assertIsInstance(form_field, SimpleArrayField)
+ self.assertEqual(form_field.max_length, 4)
+
+
+class TestSplitFormField(TestCase):
+
+ def test_valid(self):
+ class SplitForm(forms.Form):
+ array = SplitArrayField(forms.CharField(), size=3)
+
+ data = {'array_0': 'a', 'array_1': 'b', 'array_2': 'c'}
+ form = SplitForm(data)
+ self.assertTrue(form.is_valid())
+ self.assertEqual(form.cleaned_data, {'array': ['a', 'b', 'c']})
+
+ def test_required(self):
+ class SplitForm(forms.Form):
+ array = SplitArrayField(forms.CharField(), required=True, size=3)
+
+ data = {'array_0': '', 'array_1': '', 'array_2': ''}
+ form = SplitForm(data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.errors, {'array': ['This field is required.']})
+
+ def test_remove_trailing_nulls(self):
+ class SplitForm(forms.Form):
+ array = SplitArrayField(forms.CharField(required=False), size=5, remove_trailing_nulls=True)
+
+ data = {'array_0': 'a', 'array_1': '', 'array_2': 'b', 'array_3': '', 'array_4': ''}
+ form = SplitForm(data)
+ self.assertTrue(form.is_valid(), form.errors)
+ self.assertEqual(form.cleaned_data, {'array': ['a', '', 'b']})
+
+ def test_required_field(self):
+ class SplitForm(forms.Form):
+ array = SplitArrayField(forms.CharField(), size=3)
+
+ data = {'array_0': 'a', 'array_1': 'b', 'array_2': ''}
+ form = SplitForm(data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.errors, {'array': ['Item 2 in the array did not validate: This field is required.']})
+
+ def test_rendering(self):
+ class SplitForm(forms.Form):
+ array = SplitArrayField(forms.CharField(), size=3)
+
+ self.assertHTMLEqual(str(SplitForm()), '''
+ <tr>
+ <th><label for="id_array_0">Array:</label></th>
+ <td>
+ <input id="id_array_0" name="array_0" type="text" />
+ <input id="id_array_1" name="array_1" type="text" />
+ <input id="id_array_2" name="array_2" type="text" />
+ </td>
+ </tr>
+ ''')