diff options
Diffstat (limited to 'tests/backends/postgresql')
| -rw-r--r-- | tests/backends/postgresql/__init__.py | 0 | ||||
| -rw-r--r-- | tests/backends/postgresql/test_creation.py | 95 | ||||
| -rw-r--r-- | tests/backends/postgresql/test_server_side_cursors.py | 82 | ||||
| -rw-r--r-- | tests/backends/postgresql/tests.py | 147 |
4 files changed, 324 insertions, 0 deletions
diff --git a/tests/backends/postgresql/__init__.py b/tests/backends/postgresql/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/backends/postgresql/__init__.py diff --git a/tests/backends/postgresql/test_creation.py b/tests/backends/postgresql/test_creation.py new file mode 100644 index 0000000000..9554e97a54 --- /dev/null +++ b/tests/backends/postgresql/test_creation.py @@ -0,0 +1,95 @@ +import unittest +from contextlib import contextmanager +from io import StringIO +from unittest import mock + +from django.db import connection +from django.db.backends.base.creation import BaseDatabaseCreation +from django.db.utils import DatabaseError +from django.test import SimpleTestCase + +try: + import psycopg2 # NOQA +except ImportError: + pass +else: + from psycopg2 import errorcodes + from django.db.backends.postgresql.creation import DatabaseCreation + + +@unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL tests') +class DatabaseCreationTests(SimpleTestCase): + + @contextmanager + def changed_test_settings(self, **kwargs): + settings = connection.settings_dict['TEST'] + saved_values = {} + for name in kwargs: + if name in settings: + saved_values[name] = settings[name] + + for name, value in kwargs.items(): + settings[name] = value + try: + yield + finally: + for name, value in kwargs.items(): + if name in saved_values: + settings[name] = saved_values[name] + else: + del settings[name] + + def check_sql_table_creation_suffix(self, settings, expected): + with self.changed_test_settings(**settings): + creation = DatabaseCreation(connection) + suffix = creation.sql_table_creation_suffix() + self.assertEqual(suffix, expected) + + def test_sql_table_creation_suffix_with_none_settings(self): + settings = {'CHARSET': None, 'TEMPLATE': None} + self.check_sql_table_creation_suffix(settings, "") + + def test_sql_table_creation_suffix_with_encoding(self): + settings = {'CHARSET': 'UTF8'} + self.check_sql_table_creation_suffix(settings, "WITH ENCODING 'UTF8'") + + def test_sql_table_creation_suffix_with_template(self): + settings = {'TEMPLATE': 'template0'} + self.check_sql_table_creation_suffix(settings, 'WITH TEMPLATE "template0"') + + def test_sql_table_creation_suffix_with_encoding_and_template(self): + settings = {'CHARSET': 'UTF8', 'TEMPLATE': 'template0'} + self.check_sql_table_creation_suffix(settings, '''WITH ENCODING 'UTF8' TEMPLATE "template0"''') + + def _execute_raise_database_already_exists(self, cursor, parameters, keepdb=False): + error = DatabaseError('database %s already exists' % parameters['dbname']) + error.pgcode = errorcodes.DUPLICATE_DATABASE + raise DatabaseError() from error + + def _execute_raise_permission_denied(self, cursor, parameters, keepdb=False): + error = DatabaseError('permission denied to create database') + error.pgcode = errorcodes.INSUFFICIENT_PRIVILEGE + raise DatabaseError() from error + + def patch_test_db_creation(self, execute_create_test_db): + return mock.patch.object(BaseDatabaseCreation, '_execute_create_test_db', execute_create_test_db) + + @mock.patch('sys.stdout', new_callable=StringIO) + @mock.patch('sys.stderr', new_callable=StringIO) + def test_create_test_db(self, *mocked_objects): + creation = DatabaseCreation(connection) + # Simulate test database creation raising "database already exists" + with self.patch_test_db_creation(self._execute_raise_database_already_exists): + with mock.patch('builtins.input', return_value='no'): + with self.assertRaises(SystemExit): + # SystemExit is raised if the user answers "no" to the + # prompt asking if it's okay to delete the test database. + creation._create_test_db(verbosity=0, autoclobber=False, keepdb=False) + # "Database already exists" error is ignored when keepdb is on + creation._create_test_db(verbosity=0, autoclobber=False, keepdb=True) + # Simulate test database creation raising unexpected error + with self.patch_test_db_creation(self._execute_raise_permission_denied): + with self.assertRaises(SystemExit): + creation._create_test_db(verbosity=0, autoclobber=False, keepdb=False) + with self.assertRaises(SystemExit): + creation._create_test_db(verbosity=0, autoclobber=False, keepdb=True) diff --git a/tests/backends/postgresql/test_server_side_cursors.py b/tests/backends/postgresql/test_server_side_cursors.py new file mode 100644 index 0000000000..8576686211 --- /dev/null +++ b/tests/backends/postgresql/test_server_side_cursors.py @@ -0,0 +1,82 @@ +import operator +import unittest +from collections import namedtuple +from contextlib import contextmanager + +from django.db import connection +from django.test import TestCase + +from ..models import Person + + +@unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL tests') +class ServerSideCursorsPostgres(TestCase): + cursor_fields = 'name, statement, is_holdable, is_binary, is_scrollable, creation_time' + PostgresCursor = namedtuple('PostgresCursor', cursor_fields) + + @classmethod + def setUpTestData(cls): + Person.objects.create(first_name='a', last_name='a') + Person.objects.create(first_name='b', last_name='b') + + def inspect_cursors(self): + with connection.cursor() as cursor: + cursor.execute('SELECT {fields} FROM pg_cursors;'.format(fields=self.cursor_fields)) + cursors = cursor.fetchall() + return [self.PostgresCursor._make(cursor) for cursor in cursors] + + @contextmanager + def override_db_setting(self, **kwargs): + for setting, value in kwargs.items(): + original_value = connection.settings_dict.get(setting) + if setting in connection.settings_dict: + self.addCleanup(operator.setitem, connection.settings_dict, setting, original_value) + else: + self.addCleanup(operator.delitem, connection.settings_dict, setting) + + connection.settings_dict[setting] = kwargs[setting] + yield + + def test_server_side_cursor(self): + persons = Person.objects.iterator() + next(persons) # Open a server-side cursor + cursors = self.inspect_cursors() + self.assertEqual(len(cursors), 1) + self.assertIn('_django_curs_', cursors[0].name) + self.assertFalse(cursors[0].is_scrollable) + self.assertFalse(cursors[0].is_holdable) + self.assertFalse(cursors[0].is_binary) + + def test_server_side_cursor_many_cursors(self): + persons = Person.objects.iterator() + persons2 = Person.objects.iterator() + next(persons) # Open a server-side cursor + next(persons2) # Open a second server-side cursor + cursors = self.inspect_cursors() + self.assertEqual(len(cursors), 2) + for cursor in cursors: + self.assertIn('_django_curs_', cursor.name) + self.assertFalse(cursor.is_scrollable) + self.assertFalse(cursor.is_holdable) + self.assertFalse(cursor.is_binary) + + def test_closed_server_side_cursor(self): + persons = Person.objects.iterator() + next(persons) # Open a server-side cursor + del persons + cursors = self.inspect_cursors() + self.assertEqual(len(cursors), 0) + + def test_server_side_cursors_setting(self): + with self.override_db_setting(DISABLE_SERVER_SIDE_CURSORS=False): + persons = Person.objects.iterator() + next(persons) # Open a server-side cursor + cursors = self.inspect_cursors() + self.assertEqual(len(cursors), 1) + del persons # Close server-side cursor + + with self.override_db_setting(DISABLE_SERVER_SIDE_CURSORS=True): + persons = Person.objects.iterator() + next(persons) # Should not open a server-side cursor + cursors = self.inspect_cursors() + self.assertEqual(len(cursors), 0) diff --git a/tests/backends/postgresql/tests.py b/tests/backends/postgresql/tests.py new file mode 100644 index 0000000000..140fbbc444 --- /dev/null +++ b/tests/backends/postgresql/tests.py @@ -0,0 +1,147 @@ +import unittest +import warnings +from unittest import mock + +from django.db import DatabaseError, connection +from django.test import TestCase + + +@unittest.skipUnless(connection.vendor == 'postgresql', 'PostgreSQL tests') +class Tests(TestCase): + + def test_nodb_connection(self): + """ + The _nodb_connection property fallbacks to the default connection + database when access to the 'postgres' database is not granted. + """ + def mocked_connect(self): + if self.settings_dict['NAME'] is None: + raise DatabaseError() + return '' + + nodb_conn = connection._nodb_connection + self.assertIsNone(nodb_conn.settings_dict['NAME']) + + # Now assume the 'postgres' db isn't available + with warnings.catch_warnings(record=True) as w: + with mock.patch('django.db.backends.base.base.BaseDatabaseWrapper.connect', + side_effect=mocked_connect, autospec=True): + warnings.simplefilter('always', RuntimeWarning) + nodb_conn = connection._nodb_connection + self.assertIsNotNone(nodb_conn.settings_dict['NAME']) + self.assertEqual(nodb_conn.settings_dict['NAME'], connection.settings_dict['NAME']) + # Check a RuntimeWarning has been emitted + self.assertEqual(len(w), 1) + self.assertEqual(w[0].message.__class__, RuntimeWarning) + + def test_connect_and_rollback(self): + """ + PostgreSQL shouldn't roll back SET TIME ZONE, even if the first + transaction is rolled back (#17062). + """ + new_connection = connection.copy() + try: + # Ensure the database default time zone is different than + # the time zone in new_connection.settings_dict. We can + # get the default time zone by reset & show. + cursor = new_connection.cursor() + cursor.execute("RESET TIMEZONE") + cursor.execute("SHOW TIMEZONE") + db_default_tz = cursor.fetchone()[0] + new_tz = 'Europe/Paris' if db_default_tz == 'UTC' else 'UTC' + new_connection.close() + + # Invalidate timezone name cache, because the setting_changed + # handler cannot know about new_connection. + del new_connection.timezone_name + + # Fetch a new connection with the new_tz as default + # time zone, run a query and rollback. + with self.settings(TIME_ZONE=new_tz): + new_connection.set_autocommit(False) + cursor = new_connection.cursor() + new_connection.rollback() + + # Now let's see if the rollback rolled back the SET TIME ZONE. + cursor.execute("SHOW TIMEZONE") + tz = cursor.fetchone()[0] + self.assertEqual(new_tz, tz) + + finally: + new_connection.close() + + def test_connect_non_autocommit(self): + """ + The connection wrapper shouldn't believe that autocommit is enabled + after setting the time zone when AUTOCOMMIT is False (#21452). + """ + new_connection = connection.copy() + new_connection.settings_dict['AUTOCOMMIT'] = False + + try: + # Open a database connection. + new_connection.cursor() + self.assertFalse(new_connection.get_autocommit()) + finally: + new_connection.close() + + def test_connect_isolation_level(self): + """ + The transaction level can be configured with + DATABASES ['OPTIONS']['isolation_level']. + """ + import psycopg2 + from psycopg2.extensions import ( + ISOLATION_LEVEL_READ_COMMITTED as read_committed, + ISOLATION_LEVEL_SERIALIZABLE as serializable, + ) + # Since this is a django.test.TestCase, a transaction is in progress + # and the isolation level isn't reported as 0. This test assumes that + # PostgreSQL is configured with the default isolation level. + + # Check the level on the psycopg2 connection, not the Django wrapper. + default_level = read_committed if psycopg2.__version__ < '2.7' else None + self.assertEqual(connection.connection.isolation_level, default_level) + + new_connection = connection.copy() + new_connection.settings_dict['OPTIONS']['isolation_level'] = serializable + try: + # Start a transaction so the isolation level isn't reported as 0. + new_connection.set_autocommit(False) + # Check the level on the psycopg2 connection, not the Django wrapper. + self.assertEqual(new_connection.connection.isolation_level, serializable) + finally: + new_connection.close() + + def _select(self, val): + with connection.cursor() as cursor: + cursor.execute('SELECT %s', (val,)) + return cursor.fetchone()[0] + + def test_select_ascii_array(self): + a = ['awef'] + b = self._select(a) + self.assertEqual(a[0], b[0]) + + def test_select_unicode_array(self): + a = ['ᄲawef'] + b = self._select(a) + self.assertEqual(a[0], b[0]) + + def test_lookup_cast(self): + from django.db.backends.postgresql.operations import DatabaseOperations + do = DatabaseOperations(connection=None) + lookups = ( + 'iexact', 'contains', 'icontains', 'startswith', 'istartswith', + 'endswith', 'iendswith', 'regex', 'iregex', + ) + for lookup in lookups: + with self.subTest(lookup=lookup): + self.assertIn('::text', do.lookup_cast(lookup)) + + def test_correct_extraction_psycopg2_version(self): + from django.db.backends.postgresql.base import psycopg2_version + with mock.patch('psycopg2.__version__', '4.2.1 (dt dec pq3 ext lo64)'): + self.assertEqual(psycopg2_version(), (4, 2, 1)) + with mock.patch('psycopg2.__version__', '4.2b0.dev1 (dt dec pq3 ext lo64)'): + self.assertEqual(psycopg2_version(), (4, 2)) |
