From ec9004728ee136e3b7e2b7cd2610203e16b6ce9b Mon Sep 17 00:00:00 2001 From: Caio Ariede Date: Wed, 5 Aug 2015 11:08:56 -0300 Subject: Fixed #25175 -- Renamed the postgresql_psycopg2 database backend to postgresql. --- django/contrib/gis/db/backends/postgis/base.py | 2 +- django/contrib/gis/db/backends/postgis/features.py | 2 +- .../gis/db/backends/postgis/introspection.py | 3 +- .../contrib/gis/db/backends/postgis/operations.py | 3 +- django/contrib/gis/db/backends/postgis/schema.py | 2 +- django/db/backends/postgresql/__init__.py | 0 django/db/backends/postgresql/base.py | 264 +++++++++++++++++++++ django/db/backends/postgresql/client.py | 66 ++++++ django/db/backends/postgresql/creation.py | 13 + django/db/backends/postgresql/features.py | 30 +++ django/db/backends/postgresql/introspection.py | 229 ++++++++++++++++++ django/db/backends/postgresql/operations.py | 240 +++++++++++++++++++ django/db/backends/postgresql/schema.py | 91 +++++++ django/db/backends/postgresql/utils.py | 7 + django/db/backends/postgresql/version.py | 44 ++++ django/db/backends/postgresql_psycopg2/__init__.py | 0 django/db/backends/postgresql_psycopg2/base.py | 264 --------------------- django/db/backends/postgresql_psycopg2/client.py | 66 ------ django/db/backends/postgresql_psycopg2/creation.py | 13 - django/db/backends/postgresql_psycopg2/features.py | 30 --- .../backends/postgresql_psycopg2/introspection.py | 229 ------------------ .../db/backends/postgresql_psycopg2/operations.py | 240 ------------------- django/db/backends/postgresql_psycopg2/schema.py | 91 ------- django/db/backends/postgresql_psycopg2/utils.py | 7 - django/db/backends/postgresql_psycopg2/version.py | 44 ---- django/db/utils.py | 12 +- docs/intro/tutorial02.txt | 2 +- docs/ref/models/querysets.txt | 2 +- docs/ref/settings.txt | 16 +- docs/ref/signals.txt | 2 +- docs/releases/1.9.txt | 7 + docs/topics/db/multi-db.txt | 2 +- tests/backends/test_utils.py | 2 +- tests/backends/tests.py | 8 +- tests/dbshell/test_postgresql_psycopg2.py | 2 +- 35 files changed, 1026 insertions(+), 1009 deletions(-) create mode 100644 django/db/backends/postgresql/__init__.py create mode 100644 django/db/backends/postgresql/base.py create mode 100644 django/db/backends/postgresql/client.py create mode 100644 django/db/backends/postgresql/creation.py create mode 100644 django/db/backends/postgresql/features.py create mode 100644 django/db/backends/postgresql/introspection.py create mode 100644 django/db/backends/postgresql/operations.py create mode 100644 django/db/backends/postgresql/schema.py create mode 100644 django/db/backends/postgresql/utils.py create mode 100644 django/db/backends/postgresql/version.py delete mode 100644 django/db/backends/postgresql_psycopg2/__init__.py delete mode 100644 django/db/backends/postgresql_psycopg2/base.py delete mode 100644 django/db/backends/postgresql_psycopg2/client.py delete mode 100644 django/db/backends/postgresql_psycopg2/creation.py delete mode 100644 django/db/backends/postgresql_psycopg2/features.py delete mode 100644 django/db/backends/postgresql_psycopg2/introspection.py delete mode 100644 django/db/backends/postgresql_psycopg2/operations.py delete mode 100644 django/db/backends/postgresql_psycopg2/schema.py delete mode 100644 django/db/backends/postgresql_psycopg2/utils.py delete mode 100644 django/db/backends/postgresql_psycopg2/version.py diff --git a/django/contrib/gis/db/backends/postgis/base.py b/django/contrib/gis/db/backends/postgis/base.py index fc8a16d97f..203e3ba075 100644 --- a/django/contrib/gis/db/backends/postgis/base.py +++ b/django/contrib/gis/db/backends/postgis/base.py @@ -1,5 +1,5 @@ from django.db.backends.base.base import NO_DB_ALIAS -from django.db.backends.postgresql_psycopg2.base import \ +from django.db.backends.postgresql.base import \ DatabaseWrapper as Psycopg2DatabaseWrapper from .features import DatabaseFeatures diff --git a/django/contrib/gis/db/backends/postgis/features.py b/django/contrib/gis/db/backends/postgis/features.py index 3fad9e70c0..ea1d450008 100644 --- a/django/contrib/gis/db/backends/postgis/features.py +++ b/django/contrib/gis/db/backends/postgis/features.py @@ -1,5 +1,5 @@ from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures -from django.db.backends.postgresql_psycopg2.features import \ +from django.db.backends.postgresql.features import \ DatabaseFeatures as Psycopg2DatabaseFeatures diff --git a/django/contrib/gis/db/backends/postgis/introspection.py b/django/contrib/gis/db/backends/postgis/introspection.py index 7f231a578a..71aa57f163 100644 --- a/django/contrib/gis/db/backends/postgis/introspection.py +++ b/django/contrib/gis/db/backends/postgis/introspection.py @@ -1,6 +1,5 @@ from django.contrib.gis.gdal import OGRGeomType -from django.db.backends.postgresql_psycopg2.introspection import \ - DatabaseIntrospection +from django.db.backends.postgresql.introspection import DatabaseIntrospection class GeoIntrospectionError(Exception): diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index 31406e6506..e5bb115b71 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -11,8 +11,7 @@ from django.contrib.gis.db.backends.utils import SpatialOperator from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Distance from django.core.exceptions import ImproperlyConfigured -from django.db.backends.postgresql_psycopg2.operations import \ - DatabaseOperations +from django.db.backends.postgresql.operations import DatabaseOperations from django.db.utils import ProgrammingError from django.utils.functional import cached_property diff --git a/django/contrib/gis/db/backends/postgis/schema.py b/django/contrib/gis/db/backends/postgis/schema.py index 8c2cb38608..8b4444b213 100644 --- a/django/contrib/gis/db/backends/postgis/schema.py +++ b/django/contrib/gis/db/backends/postgis/schema.py @@ -1,4 +1,4 @@ -from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor +from django.db.backends.postgresql.schema import DatabaseSchemaEditor class PostGISSchemaEditor(DatabaseSchemaEditor): diff --git a/django/db/backends/postgresql/__init__.py b/django/db/backends/postgresql/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py new file mode 100644 index 0000000000..af616e48c4 --- /dev/null +++ b/django/db/backends/postgresql/base.py @@ -0,0 +1,264 @@ +""" +PostgreSQL database backend for Django. + +Requires psycopg 2: http://initd.org/projects/psycopg2 +""" + +import warnings + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.db import DEFAULT_DB_ALIAS +from django.db.backends.base.base import BaseDatabaseWrapper +from django.db.backends.base.validation import BaseDatabaseValidation +from django.db.utils import DatabaseError as WrappedDatabaseError +from django.utils.encoding import force_str +from django.utils.functional import cached_property +from django.utils.safestring import SafeBytes, SafeText + +try: + import psycopg2 as Database + import psycopg2.extensions + import psycopg2.extras +except ImportError as e: + raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e) + + +def psycopg2_version(): + version = psycopg2.__version__.split(' ', 1)[0] + return tuple(int(v) for v in version.split('.') if v.isdigit()) + +PSYCOPG2_VERSION = psycopg2_version() + +if PSYCOPG2_VERSION < (2, 4, 5): + raise ImproperlyConfigured("psycopg2_version 2.4.5 or newer is required; you have %s" % psycopg2.__version__) + + +# Some of these import psycopg2, so import them after checking if it's installed. +from .client import DatabaseClient # isort:skip +from .creation import DatabaseCreation # isort:skip +from .features import DatabaseFeatures # isort:skip +from .introspection import DatabaseIntrospection # isort:skip +from .operations import DatabaseOperations # isort:skip +from .schema import DatabaseSchemaEditor # isort:skip +from .utils import utc_tzinfo_factory # isort:skip +from .version import get_version # isort:skip + +DatabaseError = Database.DatabaseError +IntegrityError = Database.IntegrityError + +psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) +psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY) +psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString) +psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString) +psycopg2.extras.register_uuid() + +# Register support for inet[] manually so we don't have to handle the Inet() +# object on load all the time. +INETARRAY_OID = 1041 +INETARRAY = psycopg2.extensions.new_array_type( + (INETARRAY_OID,), + 'INETARRAY', + psycopg2.extensions.UNICODE, +) +psycopg2.extensions.register_type(INETARRAY) + + +class DatabaseWrapper(BaseDatabaseWrapper): + vendor = 'postgresql' + # This dictionary maps Field objects to their associated PostgreSQL column + # types, as strings. Column-type strings can contain format strings; they'll + # be interpolated against the values of Field.__dict__ before being output. + # If a column type is set to None, it won't be included in the output. + data_types = { + 'AutoField': 'serial', + 'BinaryField': 'bytea', + 'BooleanField': 'boolean', + 'CharField': 'varchar(%(max_length)s)', + 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', + 'DateField': 'date', + 'DateTimeField': 'timestamp with time zone', + 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'DurationField': 'interval', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', + 'FloatField': 'double precision', + 'IntegerField': 'integer', + 'BigIntegerField': 'bigint', + 'IPAddressField': 'inet', + 'GenericIPAddressField': 'inet', + 'NullBooleanField': 'boolean', + 'OneToOneField': 'integer', + 'PositiveIntegerField': 'integer', + 'PositiveSmallIntegerField': 'smallint', + 'SlugField': 'varchar(%(max_length)s)', + 'SmallIntegerField': 'smallint', + 'TextField': 'text', + 'TimeField': 'time', + 'UUIDField': 'uuid', + } + data_type_check_constraints = { + 'PositiveIntegerField': '"%(column)s" >= 0', + 'PositiveSmallIntegerField': '"%(column)s" >= 0', + } + operators = { + 'exact': '= %s', + 'iexact': '= UPPER(%s)', + 'contains': 'LIKE %s', + 'icontains': 'LIKE UPPER(%s)', + 'regex': '~ %s', + 'iregex': '~* %s', + 'gt': '> %s', + 'gte': '>= %s', + 'lt': '< %s', + 'lte': '<= %s', + 'startswith': 'LIKE %s', + 'endswith': 'LIKE %s', + 'istartswith': 'LIKE UPPER(%s)', + 'iendswith': 'LIKE UPPER(%s)', + } + + # The patterns below are used to generate SQL pattern lookup clauses when + # the right-hand side of the lookup isn't a raw string (it might be an expression + # or the result of a bilateral transformation). + # In those cases, special characters for LIKE operators (e.g. \, *, _) should be + # escaped on database side. + # + # Note: we use str.format() here for readability as '%' is used as a wildcard for + # the LIKE operator. + pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\', '\\'), '%%', '\%%'), '_', '\_')" + pattern_ops = { + 'contains': "LIKE '%%' || {} || '%%'", + 'icontains': "LIKE '%%' || UPPER({}) || '%%'", + 'startswith': "LIKE {} || '%%'", + 'istartswith': "LIKE UPPER({}) || '%%'", + 'endswith': "LIKE '%%' || {}", + 'iendswith': "LIKE '%%' || UPPER({})", + } + + Database = Database + SchemaEditorClass = DatabaseSchemaEditor + + def __init__(self, *args, **kwargs): + super(DatabaseWrapper, self).__init__(*args, **kwargs) + + self.features = DatabaseFeatures(self) + self.ops = DatabaseOperations(self) + self.client = DatabaseClient(self) + self.creation = DatabaseCreation(self) + self.introspection = DatabaseIntrospection(self) + self.validation = BaseDatabaseValidation(self) + + def get_connection_params(self): + settings_dict = self.settings_dict + # None may be used to connect to the default 'postgres' db + if settings_dict['NAME'] == '': + raise ImproperlyConfigured( + "settings.DATABASES is improperly configured. " + "Please supply the NAME value.") + conn_params = { + 'database': settings_dict['NAME'] or 'postgres', + } + conn_params.update(settings_dict['OPTIONS']) + conn_params.pop('isolation_level', None) + if settings_dict['USER']: + conn_params['user'] = settings_dict['USER'] + if settings_dict['PASSWORD']: + conn_params['password'] = force_str(settings_dict['PASSWORD']) + if settings_dict['HOST']: + conn_params['host'] = settings_dict['HOST'] + if settings_dict['PORT']: + conn_params['port'] = settings_dict['PORT'] + return conn_params + + def get_new_connection(self, conn_params): + connection = Database.connect(**conn_params) + + # self.isolation_level must be set: + # - after connecting to the database in order to obtain the database's + # default when no value is explicitly specified in options. + # - before calling _set_autocommit() because if autocommit is on, that + # will set connection.isolation_level to ISOLATION_LEVEL_AUTOCOMMIT. + options = self.settings_dict['OPTIONS'] + try: + self.isolation_level = options['isolation_level'] + except KeyError: + self.isolation_level = connection.isolation_level + else: + # Set the isolation level to the value from OPTIONS. + if self.isolation_level != connection.isolation_level: + connection.set_session(isolation_level=self.isolation_level) + + return connection + + def init_connection_state(self): + self.connection.set_client_encoding('UTF8') + + conn_timezone_name = self.connection.get_parameter_status('TimeZone') + + if conn_timezone_name != self.timezone_name: + cursor = self.connection.cursor() + try: + cursor.execute(self.ops.set_time_zone_sql(), [self.timezone_name]) + finally: + cursor.close() + # Commit after setting the time zone (see #17062) + if not self.get_autocommit(): + self.connection.commit() + + def create_cursor(self): + cursor = self.connection.cursor() + cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None + return cursor + + def _set_autocommit(self, autocommit): + with self.wrap_database_errors: + self.connection.autocommit = autocommit + + def check_constraints(self, table_names=None): + """ + To check constraints, we set constraints to immediate. Then, when, we're done we must ensure they + are returned to deferred. + """ + self.cursor().execute('SET CONSTRAINTS ALL IMMEDIATE') + self.cursor().execute('SET CONSTRAINTS ALL DEFERRED') + + def is_usable(self): + try: + # Use a psycopg cursor directly, bypassing Django's utilities. + self.connection.cursor().execute("SELECT 1") + except Database.Error: + return False + else: + return True + + @cached_property + def _nodb_connection(self): + nodb_connection = super(DatabaseWrapper, self)._nodb_connection + try: + nodb_connection.ensure_connection() + except (DatabaseError, WrappedDatabaseError): + warnings.warn( + "Normally Django will use a connection to the 'postgres' database " + "to avoid running initialization queries against the production " + "database when it's not needed (for example, when running tests). " + "Django was unable to create a connection to the 'postgres' database " + "and will use the default database instead.", + RuntimeWarning + ) + settings_dict = self.settings_dict.copy() + settings_dict['NAME'] = settings.DATABASES[DEFAULT_DB_ALIAS]['NAME'] + nodb_connection = self.__class__( + self.settings_dict.copy(), + alias=self.alias, + allow_thread_sharing=False) + return nodb_connection + + @cached_property + def psycopg2_version(self): + return PSYCOPG2_VERSION + + @cached_property + def pg_version(self): + with self.temporary_connection(): + return get_version(self.connection) diff --git a/django/db/backends/postgresql/client.py b/django/db/backends/postgresql/client.py new file mode 100644 index 0000000000..5e3e288301 --- /dev/null +++ b/django/db/backends/postgresql/client.py @@ -0,0 +1,66 @@ +import os +import subprocess + +from django.core.files.temp import NamedTemporaryFile +from django.db.backends.base.client import BaseDatabaseClient +from django.utils.six import print_ + + +def _escape_pgpass(txt): + """ + Escape a fragment of a PostgreSQL .pgpass file. + """ + return txt.replace('\\', '\\\\').replace(':', '\\:') + + +class DatabaseClient(BaseDatabaseClient): + executable_name = 'psql' + + @classmethod + def runshell_db(cls, settings_dict): + args = [cls.executable_name] + + host = settings_dict.get('HOST', '') + port = settings_dict.get('PORT', '') + name = settings_dict.get('NAME', '') + user = settings_dict.get('USER', '') + passwd = settings_dict.get('PASSWORD', '') + + if user: + args += ['-U', user] + if host: + args += ['-h', host] + if port: + args += ['-p', str(port)] + args += [name] + + temp_pgpass = None + try: + if passwd: + # Create temporary .pgpass file. + temp_pgpass = NamedTemporaryFile(mode='w+') + try: + print_( + _escape_pgpass(host) or '*', + str(port) or '*', + _escape_pgpass(name) or '*', + _escape_pgpass(user) or '*', + _escape_pgpass(passwd), + file=temp_pgpass, + sep=':', + flush=True, + ) + os.environ['PGPASSFILE'] = temp_pgpass.name + except UnicodeEncodeError: + # If the current locale can't encode the data, we let + # the user input the password manually. + pass + subprocess.call(args) + finally: + if temp_pgpass: + temp_pgpass.close() + if 'PGPASSFILE' in os.environ: # unit tests need cleanup + del os.environ['PGPASSFILE'] + + def runshell(self): + DatabaseClient.runshell_db(self.connection.settings_dict) diff --git a/django/db/backends/postgresql/creation.py b/django/db/backends/postgresql/creation.py new file mode 100644 index 0000000000..13eea0d3b7 --- /dev/null +++ b/django/db/backends/postgresql/creation.py @@ -0,0 +1,13 @@ +from django.db.backends.base.creation import BaseDatabaseCreation + + +class DatabaseCreation(BaseDatabaseCreation): + + def sql_table_creation_suffix(self): + test_settings = self.connection.settings_dict['TEST'] + assert test_settings['COLLATION'] is None, ( + "PostgreSQL does not support collation setting at database creation time." + ) + if test_settings['CHARSET']: + return "WITH ENCODING '%s'" % test_settings['CHARSET'] + return '' diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py new file mode 100644 index 0000000000..a07700e601 --- /dev/null +++ b/django/db/backends/postgresql/features.py @@ -0,0 +1,30 @@ +from django.db.backends.base.features import BaseDatabaseFeatures +from django.db.utils import InterfaceError + + +class DatabaseFeatures(BaseDatabaseFeatures): + allows_group_by_selected_pks = True + can_return_id_from_insert = True + has_real_datatype = True + has_native_uuid_field = True + has_native_duration_field = True + driver_supports_timedelta_args = True + can_defer_constraint_checks = True + has_select_for_update = True + has_select_for_update_nowait = True + has_bulk_insert = True + uses_savepoints = True + can_release_savepoints = True + supports_tablespaces = True + supports_transactions = True + can_introspect_autofield = True + can_introspect_ip_address_field = True + can_introspect_small_integer_field = True + can_distinct_on_fields = True + can_rollback_ddl = True + supports_combined_alters = True + nulls_order_largest = True + closed_cursor_error_class = InterfaceError + has_case_insensitive_like = False + requires_sqlparse_for_splitting = False + greatest_least_ignores_nulls = True diff --git a/django/db/backends/postgresql/introspection.py b/django/db/backends/postgresql/introspection.py new file mode 100644 index 0000000000..9b3e9074b2 --- /dev/null +++ b/django/db/backends/postgresql/introspection.py @@ -0,0 +1,229 @@ +from __future__ import unicode_literals + +from collections import namedtuple + +from django.db.backends.base.introspection import ( + BaseDatabaseIntrospection, FieldInfo, TableInfo, +) +from django.utils.encoding import force_text + +FieldInfo = namedtuple('FieldInfo', FieldInfo._fields + ('default',)) + + +class DatabaseIntrospection(BaseDatabaseIntrospection): + # Maps type codes to Django Field types. + data_types_reverse = { + 16: 'BooleanField', + 17: 'BinaryField', + 20: 'BigIntegerField', + 21: 'SmallIntegerField', + 23: 'IntegerField', + 25: 'TextField', + 700: 'FloatField', + 701: 'FloatField', + 869: 'GenericIPAddressField', + 1042: 'CharField', # blank-padded + 1043: 'CharField', + 1082: 'DateField', + 1083: 'TimeField', + 1114: 'DateTimeField', + 1184: 'DateTimeField', + 1266: 'TimeField', + 1700: 'DecimalField', + } + + ignored_tables = [] + + _get_indexes_query = """ + SELECT attr.attname, idx.indkey, idx.indisunique, idx.indisprimary + FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, + pg_catalog.pg_index idx, pg_catalog.pg_attribute attr + WHERE c.oid = idx.indrelid + AND idx.indexrelid = c2.oid + AND attr.attrelid = c.oid + AND attr.attnum = idx.indkey[0] + AND c.relname = %s""" + + def get_field_type(self, data_type, description): + field_type = super(DatabaseIntrospection, self).get_field_type(data_type, description) + if field_type == 'IntegerField' and description.default and 'nextval' in description.default: + return 'AutoField' + return field_type + + def get_table_list(self, cursor): + """ + Returns a list of table and view names in the current database. + """ + cursor.execute(""" + SELECT c.relname, c.relkind + FROM pg_catalog.pg_class c + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('r', 'v') + AND n.nspname NOT IN ('pg_catalog', 'pg_toast') + AND pg_catalog.pg_table_is_visible(c.oid)""") + return [TableInfo(row[0], {'r': 't', 'v': 'v'}.get(row[1])) + for row in cursor.fetchall() + if row[0] not in self.ignored_tables] + + def get_table_description(self, cursor, table_name): + "Returns a description of the table, with the DB-API cursor.description interface." + # As cursor.description does not return reliably the nullable property, + # we have to query the information_schema (#7783) + cursor.execute(""" + SELECT column_name, is_nullable, column_default + FROM information_schema.columns + WHERE table_name = %s""", [table_name]) + field_map = {line[0]: line[1:] for line in cursor.fetchall()} + cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name)) + return [FieldInfo(*((force_text(line[0]),) + line[1:6] + + (field_map[force_text(line[0])][0] == 'YES', field_map[force_text(line[0])][1]))) + for line in cursor.description] + + def get_relations(self, cursor, table_name): + """ + Returns a dictionary of {field_name: (field_name_other_table, other_table)} + representing all relationships to the given table. + """ + cursor.execute(""" + SELECT c2.relname, a1.attname, a2.attname + FROM pg_constraint con + LEFT JOIN pg_class c1 ON con.conrelid = c1.oid + LEFT JOIN pg_class c2 ON con.confrelid = c2.oid + LEFT JOIN pg_attribute a1 ON c1.oid = a1.attrelid AND a1.attnum = con.conkey[1] + LEFT JOIN pg_attribute a2 ON c2.oid = a2.attrelid AND a2.attnum = con.confkey[1] + WHERE c1.relname = %s + AND con.contype = 'f'""", [table_name]) + relations = {} + for row in cursor.fetchall(): + relations[row[1]] = (row[2], row[0]) + return relations + + def get_key_columns(self, cursor, table_name): + key_columns = [] + cursor.execute(""" + SELECT kcu.column_name, ccu.table_name AS referenced_table, ccu.column_name AS referenced_column + FROM information_schema.constraint_column_usage ccu + LEFT JOIN information_schema.key_column_usage kcu + ON ccu.constraint_catalog = kcu.constraint_catalog + AND ccu.constraint_schema = kcu.constraint_schema + AND ccu.constraint_name = kcu.constraint_name + LEFT JOIN information_schema.table_constraints tc + ON ccu.constraint_catalog = tc.constraint_catalog + AND ccu.constraint_schema = tc.constraint_schema + AND ccu.constraint_name = tc.constraint_name + WHERE kcu.table_name = %s AND tc.constraint_type = 'FOREIGN KEY'""", [table_name]) + key_columns.extend(cursor.fetchall()) + return key_columns + + def get_indexes(self, cursor, table_name): + # This query retrieves each index on the given table, including the + # first associated field name + cursor.execute(self._get_indexes_query, [table_name]) + indexes = {} + for row in cursor.fetchall(): + # row[1] (idx.indkey) is stored in the DB as an array. It comes out as + # a string of space-separated integers. This designates the field + # indexes (1-based) of the fields that have indexes on the table. + # Here, we skip any indexes across multiple fields. + if ' ' in row[1]: + continue + if row[0] not in indexes: + indexes[row[0]] = {'primary_key': False, 'unique': False} + # It's possible to have the unique and PK constraints in separate indexes. + if row[3]: + indexes[row[0]]['primary_key'] = True + if row[2]: + indexes[row[0]]['unique'] = True + return indexes + + def get_constraints(self, cursor, table_name): + """ + Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns. + """ + constraints = {} + # Loop over the key table, collecting things as constraints + # This will get PKs, FKs, and uniques, but not CHECK + cursor.execute(""" + SELECT + kc.constraint_name, + kc.column_name, + c.constraint_type, + array(SELECT table_name::text || '.' || column_name::text + FROM information_schema.constraint_column_usage + WHERE constraint_name = kc.constraint_name) + FROM information_schema.key_column_usage AS kc + JOIN information_schema.table_constraints AS c ON + kc.table_schema = c.table_schema AND + kc.table_name = c.table_name AND + kc.constraint_name = c.constraint_name + WHERE + kc.table_schema = %s AND + kc.table_name = %s + ORDER BY kc.ordinal_position ASC + """, ["public", table_name]) + for constraint, column, kind, used_cols in cursor.fetchall(): + # If we're the first column, make the record + if constraint not in constraints: + constraints[constraint] = { + "columns": [], + "primary_key": kind.lower() == "primary key", + "unique": kind.lower() in ["primary key", "unique"], + "foreign_key": tuple(used_cols[0].split(".", 1)) if kind.lower() == "foreign key" else None, + "check": False, + "index": False, + } + # Record the details + constraints[constraint]['columns'].append(column) + # Now get CHECK constraint columns + cursor.execute(""" + SELECT kc.constraint_name, kc.column_name + FROM information_schema.constraint_column_usage AS kc + JOIN information_schema.table_constraints AS c ON + kc.table_schema = c.table_schema AND + kc.table_name = c.table_name AND + kc.constraint_name = c.constraint_name + WHERE + c.constraint_type = 'CHECK' AND + kc.table_schema = %s AND + kc.table_name = %s + """, ["public", table_name]) + for constraint, column in cursor.fetchall(): + # If we're the first column, make the record + if constraint not in constraints: + constraints[constraint] = { + "columns": [], + "primary_key": False, + "unique": False, + "foreign_key": None, + "check": True, + "index": False, + } + # Record the details + constraints[constraint]['columns'].append(column) + # Now get indexes + cursor.execute(""" + SELECT + c2.relname, + ARRAY( + SELECT (SELECT attname FROM pg_catalog.pg_attribute WHERE attnum = i AND attrelid = c.oid) + FROM unnest(idx.indkey) i + ), + idx.indisunique, + idx.indisprimary + FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, + pg_catalog.pg_index idx + WHERE c.oid = idx.indrelid + AND idx.indexrelid = c2.oid + AND c.relname = %s + """, [table_name]) + for index, columns, unique, primary in cursor.fetchall(): + if index not in constraints: + constraints[index] = { + "columns": list(columns), + "primary_key": primary, + "unique": unique, + "foreign_key": None, + "check": False, + "index": True, + } + return constraints diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py new file mode 100644 index 0000000000..866e2ca38b --- /dev/null +++ b/django/db/backends/postgresql/operations.py @@ -0,0 +1,240 @@ +from __future__ import unicode_literals + +from psycopg2.extras import Inet + +from django.conf import settings +from django.db.backends.base.operations import BaseDatabaseOperations + + +class DatabaseOperations(BaseDatabaseOperations): + def unification_cast_sql(self, output_field): + internal_type = output_field.get_internal_type() + if internal_type in ("GenericIPAddressField", "IPAddressField", "TimeField", "UUIDField"): + # PostgreSQL will resolve a union as type 'text' if input types are + # 'unknown'. + # http://www.postgresql.org/docs/9.4/static/typeconv-union-case.html + # These fields cannot be implicitly cast back in the default + # PostgreSQL configuration so we need to explicitly cast them. + # We must also remove components of the type within brackets: + # varchar(255) -> varchar. + return 'CAST(%%s AS %s)' % output_field.db_type(self.connection).split('(')[0] + return '%s' + + def date_extract_sql(self, lookup_type, field_name): + # http://www.postgresql.org/docs/current/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT + if lookup_type == 'week_day': + # For consistency across backends, we return Sunday=1, Saturday=7. + return "EXTRACT('dow' FROM %s) + 1" % field_name + else: + return "EXTRACT('%s' FROM %s)" % (lookup_type, field_name) + + def date_trunc_sql(self, lookup_type, field_name): + # http://www.postgresql.org/docs/current/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC + return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) + + def _convert_field_to_tz(self, field_name, tzname): + if settings.USE_TZ: + field_name = "%s AT TIME ZONE %%s" % field_name + params = [tzname] + else: + params = [] + return field_name, params + + def datetime_cast_date_sql(self, field_name, tzname): + field_name, params = self._convert_field_to_tz(field_name, tzname) + sql = '(%s)::date' % field_name + return sql, params + + def datetime_extract_sql(self, lookup_type, field_name, tzname): + field_name, params = self._convert_field_to_tz(field_name, tzname) + sql = self.date_extract_sql(lookup_type, field_name) + return sql, params + + def datetime_trunc_sql(self, lookup_type, field_name, tzname): + field_name, params = self._convert_field_to_tz(field_name, tzname) + # http://www.postgresql.org/docs/current/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC + sql = "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) + return sql, params + + def deferrable_sql(self): + return " DEFERRABLE INITIALLY DEFERRED" + + def lookup_cast(self, lookup_type, internal_type=None): + lookup = '%s' + + # Cast text lookups to text to allow things like filter(x__contains=4) + if lookup_type in ('iexact', 'contains', 'icontains', 'startswith', + 'istartswith', 'endswith', 'iendswith', 'regex', 'iregex'): + if internal_type in ('IPAddressField', 'GenericIPAddressField'): + lookup = "HOST(%s)" + else: + lookup = "%s::text" + + # Use UPPER(x) for case-insensitive lookups; it's faster. + if lookup_type in ('iexact', 'icontains', 'istartswith', 'iendswith'): + lookup = 'UPPER(%s)' % lookup + + return lookup + + def last_insert_id(self, cursor, table_name, pk_name): + # Use pg_get_serial_sequence to get the underlying sequence name + # from the table name and column name (available since PostgreSQL 8) + cursor.execute("SELECT CURRVAL(pg_get_serial_sequence('%s','%s'))" % ( + self.quote_name(table_name), pk_name)) + return cursor.fetchone()[0] + + def no_limit_value(self): + return None + + def prepare_sql_script(self, sql): + return [sql] + + def quote_name(self, name): + if name.startswith('"') and name.endswith('"'): + return name # Quoting once is enough. + return '"%s"' % name + + def set_time_zone_sql(self): + return "SET TIME ZONE %s" + + def sql_flush(self, style, tables, sequences, allow_cascade=False): + if tables: + # Perform a single SQL 'TRUNCATE x, y, z...;' statement. It allows + # us to truncate tables referenced by a foreign key in any other + # table. + tables_sql = ', '.join( + style.SQL_FIELD(self.quote_name(table)) for table in tables) + if allow_cascade: + sql = ['%s %s %s;' % ( + style.SQL_KEYWORD('TRUNCATE'), + tables_sql, + style.SQL_KEYWORD('CASCADE'), + )] + else: + sql = ['%s %s;' % ( + style.SQL_KEYWORD('TRUNCATE'), + tables_sql, + )] + sql.extend(self.sequence_reset_by_name_sql(style, sequences)) + return sql + else: + return [] + + def sequence_reset_by_name_sql(self, style, sequences): + # 'ALTER SEQUENCE sequence_name RESTART WITH 1;'... style SQL statements + # to reset sequence indices + sql = [] + for sequence_info in sequences: + table_name = sequence_info['table'] + column_name = sequence_info['column'] + if not (column_name and len(column_name) > 0): + # This will be the case if it's an m2m using an autogenerated + # intermediate table (see BaseDatabaseIntrospection.sequence_list) + column_name = 'id' + sql.append("%s setval(pg_get_serial_sequence('%s','%s'), 1, false);" % + (style.SQL_KEYWORD('SELECT'), + style.SQL_TABLE(self.quote_name(table_name)), + style.SQL_FIELD(column_name)) + ) + return sql + + def tablespace_sql(self, tablespace, inline=False): + if inline: + return "USING INDEX TABLESPACE %s" % self.quote_name(tablespace) + else: + return "TABLESPACE %s" % self.quote_name(tablespace) + + def sequence_reset_sql(self, style, model_list): + from django.db import models + output = [] + qn = self.quote_name + for model in model_list: + # Use `coalesce` to set the sequence for each model to the max pk value if there are records, + # or 1 if there are none. Set the `is_called` property (the third argument to `setval`) to true + # if there are records (as the max pk value is already in use), otherwise set it to false. + # Use pg_get_serial_sequence to get the underlying sequence name from the table name + # and column name (available since PostgreSQL 8) + + for f in model._meta.local_fields: + if isinstance(f, models.AutoField): + output.append( + "%s setval(pg_get_serial_sequence('%s','%s'), " + "coalesce(max(%s), 1), max(%s) %s null) %s %s;" % ( + style.SQL_KEYWORD('SELECT'), + style.SQL_TABLE(qn(model._meta.db_table)), + style.SQL_FIELD(f.column), + style.SQL_FIELD(qn(f.column)), + style.SQL_FIELD(qn(f.column)), + style.SQL_KEYWORD('IS NOT'), + style.SQL_KEYWORD('FROM'), + style.SQL_TABLE(qn(model._meta.db_table)), + ) + ) + break # Only one AutoField is allowed per model, so don't bother continuing. + for f in model._meta.many_to_many: + if not f.remote_field.through: + output.append( + "%s setval(pg_get_serial_sequence('%s','%s'), " + "coalesce(max(%s), 1), max(%s) %s null) %s %s;" % ( + style.SQL_KEYWORD('SELECT'), + style.SQL_TABLE(qn(f.m2m_db_table())), + style.SQL_FIELD('id'), + style.SQL_FIELD(qn('id')), + style.SQL_FIELD(qn('id')), + style.SQL_KEYWORD('IS NOT'), + style.SQL_KEYWORD('FROM'), + style.SQL_TABLE(qn(f.m2m_db_table())) + ) + ) + return output + + def prep_for_iexact_query(self, x): + return x + + def max_name_length(self): + """ + Returns the maximum length of an identifier. + + Note that the maximum length of an identifier is 63 by default, but can + be changed by recompiling PostgreSQL after editing the NAMEDATALEN + macro in src/include/pg_config_manual.h . + + This implementation simply returns 63, but can easily be overridden by a + custom database backend that inherits most of its behavior from this one. + """ + + return 63 + + def distinct_sql(self, fields): + if fields: + return 'DISTINCT ON (%s)' % ', '.join(fields) + else: + return 'DISTINCT' + + def last_executed_query(self, cursor, sql, params): + # http://initd.org/psycopg/docs/cursor.html#cursor.query + # The query attribute is a Psycopg extension to the DB API 2.0. + if cursor.query is not None: + return cursor.query.decode('utf-8') + return None + + def return_insert_id(self): + return "RETURNING %s", () + + def bulk_insert_sql(self, fields, num_values): + items_sql = "(%s)" % ", ".join(["%s"] * len(fields)) + return "VALUES " + ", ".join([items_sql] * num_values) + + def adapt_datefield_value(self, value): + return value + + def adapt_datetimefield_value(self, value): + return value + + def adapt_timefield_value(self, value): + return value + + def adapt_ipaddressfield_value(self, value): + if value: + return Inet(value) + return None diff --git a/django/db/backends/postgresql/schema.py b/django/db/backends/postgresql/schema.py new file mode 100644 index 0000000000..bc03a12e8d --- /dev/null +++ b/django/db/backends/postgresql/schema.py @@ -0,0 +1,91 @@ +import psycopg2 + +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): + + sql_alter_column_type = "ALTER COLUMN %(column)s TYPE %(type)s USING %(column)s::%(type)s" + + sql_create_sequence = "CREATE SEQUENCE %(sequence)s" + sql_delete_sequence = "DROP SEQUENCE IF EXISTS %(sequence)s CASCADE" + sql_set_sequence_max = "SELECT setval('%(sequence)s', MAX(%(column)s)) FROM %(table)s" + + sql_create_varchar_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s varchar_pattern_ops)%(extra)s" + sql_create_text_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s text_pattern_ops)%(extra)s" + + def quote_value(self, value): + return psycopg2.extensions.adapt(value) + + def _model_indexes_sql(self, model): + output = super(DatabaseSchemaEditor, self)._model_indexes_sql(model) + if not model._meta.managed or model._meta.proxy or model._meta.swapped: + return output + + for field in model._meta.local_fields: + db_type = field.db_type(connection=self.connection) + if db_type is not None and (field.db_index or field.unique): + # Fields with database column types of `varchar` and `text` need + # a second index that specifies their operator class, which is + # needed when performing correct LIKE queries outside the + # C locale. See #12234. + if db_type.startswith('varchar'): + output.append(self._create_index_sql( + model, [field], suffix='_like', sql=self.sql_create_varchar_index)) + elif db_type.startswith('text'): + output.append(self._create_index_sql( + model, [field], suffix='_like', sql=self.sql_create_text_index)) + return output + + def _alter_column_type_sql(self, table, old_field, new_field, new_type): + """ + Makes ALTER TYPE with SERIAL make sense. + """ + if new_type.lower() == "serial": + column = new_field.column + sequence_name = "%s_%s_seq" % (table, column) + return ( + ( + self.sql_alter_column_type % { + "column": self.quote_name(column), + "type": "integer", + }, + [], + ), + [ + ( + self.sql_delete_sequence % { + "sequence": self.quote_name(sequence_name), + }, + [], + ), + ( + self.sql_create_sequence % { + "sequence": self.quote_name(sequence_name), + }, + [], + ), + ( + self.sql_alter_column % { + "table": self.quote_name(table), + "changes": self.sql_alter_column_default % { + "column": self.quote_name(column), + "default": "nextval('%s')" % self.quote_name(sequence_name), + } + }, + [], + ), + ( + self.sql_set_sequence_max % { + "table": self.quote_name(table), + "column": self.quote_name(column), + "sequence": self.quote_name(sequence_name), + }, + [], + ), + ], + ) + else: + return super(DatabaseSchemaEditor, self)._alter_column_type_sql( + table, old_field, new_field, new_type + ) diff --git a/django/db/backends/postgresql/utils.py b/django/db/backends/postgresql/utils.py new file mode 100644 index 0000000000..2c03ab36cd --- /dev/null +++ b/django/db/backends/postgresql/utils.py @@ -0,0 +1,7 @@ +from django.utils.timezone import utc + + +def utc_tzinfo_factory(offset): + if offset != 0: + raise AssertionError("database connection isn't set to UTC") + return utc diff --git a/django/db/backends/postgresql/version.py b/django/db/backends/postgresql/version.py new file mode 100644 index 0000000000..d558fb2e51 --- /dev/null +++ b/django/db/backends/postgresql/version.py @@ -0,0 +1,44 @@ +""" +Extracts the version of the PostgreSQL server. +""" + +import re + +# This reg-exp is intentionally fairly flexible here. +# Needs to be able to handle stuff like: +# PostgreSQL #.#.# +# EnterpriseDB #.# +# PostgreSQL #.# beta# +# PostgreSQL #.#beta# +VERSION_RE = re.compile(r'\S+ (\d+)\.(\d+)\.?(\d+)?') + + +def _parse_version(text): + "Internal parsing method. Factored out for testing purposes." + major, major2, minor = VERSION_RE.search(text).groups() + try: + return int(major) * 10000 + int(major2) * 100 + int(minor) + except (ValueError, TypeError): + return int(major) * 10000 + int(major2) * 100 + + +def get_version(connection): + """ + Returns an integer representing the major, minor and revision number of the + server. Format is the one used for the return value of libpq + PQServerVersion()/``server_version`` connection attribute (available in + newer psycopg2 versions.) + + For example, 90304 for 9.3.4. The last two digits will be 00 in the case of + releases (e.g., 90400 for 'PostgreSQL 9.4') or in the case of beta and + prereleases (e.g. 90100 for 'PostgreSQL 9.1beta2'). + + PQServerVersion()/``server_version`` doesn't execute a query so try that + first, then fallback to a ``SELECT version()`` query. + """ + if hasattr(connection, 'server_version'): + return connection.server_version + else: + with connection.cursor() as cursor: + cursor.execute("SELECT version()") + return _parse_version(cursor.fetchone()[0]) diff --git a/django/db/backends/postgresql_psycopg2/__init__.py b/django/db/backends/postgresql_psycopg2/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py deleted file mode 100644 index af616e48c4..0000000000 --- a/django/db/backends/postgresql_psycopg2/base.py +++ /dev/null @@ -1,264 +0,0 @@ -""" -PostgreSQL database backend for Django. - -Requires psycopg 2: http://initd.org/projects/psycopg2 -""" - -import warnings - -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured -from django.db import DEFAULT_DB_ALIAS -from django.db.backends.base.base import BaseDatabaseWrapper -from django.db.backends.base.validation import BaseDatabaseValidation -from django.db.utils import DatabaseError as WrappedDatabaseError -from django.utils.encoding import force_str -from django.utils.functional import cached_property -from django.utils.safestring import SafeBytes, SafeText - -try: - import psycopg2 as Database - import psycopg2.extensions - import psycopg2.extras -except ImportError as e: - raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e) - - -def psycopg2_version(): - version = psycopg2.__version__.split(' ', 1)[0] - return tuple(int(v) for v in version.split('.') if v.isdigit()) - -PSYCOPG2_VERSION = psycopg2_version() - -if PSYCOPG2_VERSION < (2, 4, 5): - raise ImproperlyConfigured("psycopg2_version 2.4.5 or newer is required; you have %s" % psycopg2.__version__) - - -# Some of these import psycopg2, so import them after checking if it's installed. -from .client import DatabaseClient # isort:skip -from .creation import DatabaseCreation # isort:skip -from .features import DatabaseFeatures # isort:skip -from .introspection import DatabaseIntrospection # isort:skip -from .operations import DatabaseOperations # isort:skip -from .schema import DatabaseSchemaEditor # isort:skip -from .utils import utc_tzinfo_factory # isort:skip -from .version import get_version # isort:skip - -DatabaseError = Database.DatabaseError -IntegrityError = Database.IntegrityError - -psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) -psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY) -psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString) -psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString) -psycopg2.extras.register_uuid() - -# Register support for inet[] manually so we don't have to handle the Inet() -# object on load all the time. -INETARRAY_OID = 1041 -INETARRAY = psycopg2.extensions.new_array_type( - (INETARRAY_OID,), - 'INETARRAY', - psycopg2.extensions.UNICODE, -) -psycopg2.extensions.register_type(INETARRAY) - - -class DatabaseWrapper(BaseDatabaseWrapper): - vendor = 'postgresql' - # This dictionary maps Field objects to their associated PostgreSQL column - # types, as strings. Column-type strings can contain format strings; they'll - # be interpolated against the values of Field.__dict__ before being output. - # If a column type is set to None, it won't be included in the output. - data_types = { - 'AutoField': 'serial', - 'BinaryField': 'bytea', - 'BooleanField': 'boolean', - 'CharField': 'varchar(%(max_length)s)', - 'CommaSeparatedIntegerField': 'varchar(%(max_length)s)', - 'DateField': 'date', - 'DateTimeField': 'timestamp with time zone', - 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', - 'DurationField': 'interval', - 'FileField': 'varchar(%(max_length)s)', - 'FilePathField': 'varchar(%(max_length)s)', - 'FloatField': 'double precision', - 'IntegerField': 'integer', - 'BigIntegerField': 'bigint', - 'IPAddressField': 'inet', - 'GenericIPAddressField': 'inet', - 'NullBooleanField': 'boolean', - 'OneToOneField': 'integer', - 'PositiveIntegerField': 'integer', - 'PositiveSmallIntegerField': 'smallint', - 'SlugField': 'varchar(%(max_length)s)', - 'SmallIntegerField': 'smallint', - 'TextField': 'text', - 'TimeField': 'time', - 'UUIDField': 'uuid', - } - data_type_check_constraints = { - 'PositiveIntegerField': '"%(column)s" >= 0', - 'PositiveSmallIntegerField': '"%(column)s" >= 0', - } - operators = { - 'exact': '= %s', - 'iexact': '= UPPER(%s)', - 'contains': 'LIKE %s', - 'icontains': 'LIKE UPPER(%s)', - 'regex': '~ %s', - 'iregex': '~* %s', - 'gt': '> %s', - 'gte': '>= %s', - 'lt': '< %s', - 'lte': '<= %s', - 'startswith': 'LIKE %s', - 'endswith': 'LIKE %s', - 'istartswith': 'LIKE UPPER(%s)', - 'iendswith': 'LIKE UPPER(%s)', - } - - # The patterns below are used to generate SQL pattern lookup clauses when - # the right-hand side of the lookup isn't a raw string (it might be an expression - # or the result of a bilateral transformation). - # In those cases, special characters for LIKE operators (e.g. \, *, _) should be - # escaped on database side. - # - # Note: we use str.format() here for readability as '%' is used as a wildcard for - # the LIKE operator. - pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\', '\\'), '%%', '\%%'), '_', '\_')" - pattern_ops = { - 'contains': "LIKE '%%' || {} || '%%'", - 'icontains': "LIKE '%%' || UPPER({}) || '%%'", - 'startswith': "LIKE {} || '%%'", - 'istartswith': "LIKE UPPER({}) || '%%'", - 'endswith': "LIKE '%%' || {}", - 'iendswith': "LIKE '%%' || UPPER({})", - } - - Database = Database - SchemaEditorClass = DatabaseSchemaEditor - - def __init__(self, *args, **kwargs): - super(DatabaseWrapper, self).__init__(*args, **kwargs) - - self.features = DatabaseFeatures(self) - self.ops = DatabaseOperations(self) - self.client = DatabaseClient(self) - self.creation = DatabaseCreation(self) - self.introspection = DatabaseIntrospection(self) - self.validation = BaseDatabaseValidation(self) - - def get_connection_params(self): - settings_dict = self.settings_dict - # None may be used to connect to the default 'postgres' db - if settings_dict['NAME'] == '': - raise ImproperlyConfigured( - "settings.DATABASES is improperly configured. " - "Please supply the NAME value.") - conn_params = { - 'database': settings_dict['NAME'] or 'postgres', - } - conn_params.update(settings_dict['OPTIONS']) - conn_params.pop('isolation_level', None) - if settings_dict['USER']: - conn_params['user'] = settings_dict['USER'] - if settings_dict['PASSWORD']: - conn_params['password'] = force_str(settings_dict['PASSWORD']) - if settings_dict['HOST']: - conn_params['host'] = settings_dict['HOST'] - if settings_dict['PORT']: - conn_params['port'] = settings_dict['PORT'] - return conn_params - - def get_new_connection(self, conn_params): - connection = Database.connect(**conn_params) - - # self.isolation_level must be set: - # - after connecting to the database in order to obtain the database's - # default when no value is explicitly specified in options. - # - before calling _set_autocommit() because if autocommit is on, that - # will set connection.isolation_level to ISOLATION_LEVEL_AUTOCOMMIT. - options = self.settings_dict['OPTIONS'] - try: - self.isolation_level = options['isolation_level'] - except KeyError: - self.isolation_level = connection.isolation_level - else: - # Set the isolation level to the value from OPTIONS. - if self.isolation_level != connection.isolation_level: - connection.set_session(isolation_level=self.isolation_level) - - return connection - - def init_connection_state(self): - self.connection.set_client_encoding('UTF8') - - conn_timezone_name = self.connection.get_parameter_status('TimeZone') - - if conn_timezone_name != self.timezone_name: - cursor = self.connection.cursor() - try: - cursor.execute(self.ops.set_time_zone_sql(), [self.timezone_name]) - finally: - cursor.close() - # Commit after setting the time zone (see #17062) - if not self.get_autocommit(): - self.connection.commit() - - def create_cursor(self): - cursor = self.connection.cursor() - cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None - return cursor - - def _set_autocommit(self, autocommit): - with self.wrap_database_errors: - self.connection.autocommit = autocommit - - def check_constraints(self, table_names=None): - """ - To check constraints, we set constraints to immediate. Then, when, we're done we must ensure they - are returned to deferred. - """ - self.cursor().execute('SET CONSTRAINTS ALL IMMEDIATE') - self.cursor().execute('SET CONSTRAINTS ALL DEFERRED') - - def is_usable(self): - try: - # Use a psycopg cursor directly, bypassing Django's utilities. - self.connection.cursor().execute("SELECT 1") - except Database.Error: - return False - else: - return True - - @cached_property - def _nodb_connection(self): - nodb_connection = super(DatabaseWrapper, self)._nodb_connection - try: - nodb_connection.ensure_connection() - except (DatabaseError, WrappedDatabaseError): - warnings.warn( - "Normally Django will use a connection to the 'postgres' database " - "to avoid running initialization queries against the production " - "database when it's not needed (for example, when running tests). " - "Django was unable to create a connection to the 'postgres' database " - "and will use the default database instead.", - RuntimeWarning - ) - settings_dict = self.settings_dict.copy() - settings_dict['NAME'] = settings.DATABASES[DEFAULT_DB_ALIAS]['NAME'] - nodb_connection = self.__class__( - self.settings_dict.copy(), - alias=self.alias, - allow_thread_sharing=False) - return nodb_connection - - @cached_property - def psycopg2_version(self): - return PSYCOPG2_VERSION - - @cached_property - def pg_version(self): - with self.temporary_connection(): - return get_version(self.connection) diff --git a/django/db/backends/postgresql_psycopg2/client.py b/django/db/backends/postgresql_psycopg2/client.py deleted file mode 100644 index 5e3e288301..0000000000 --- a/django/db/backends/postgresql_psycopg2/client.py +++ /dev/null @@ -1,66 +0,0 @@ -import os -import subprocess - -from django.core.files.temp import NamedTemporaryFile -from django.db.backends.base.client import BaseDatabaseClient -from django.utils.six import print_ - - -def _escape_pgpass(txt): - """ - Escape a fragment of a PostgreSQL .pgpass file. - """ - return txt.replace('\\', '\\\\').replace(':', '\\:') - - -class DatabaseClient(BaseDatabaseClient): - executable_name = 'psql' - - @classmethod - def runshell_db(cls, settings_dict): - args = [cls.executable_name] - - host = settings_dict.get('HOST', '') - port = settings_dict.get('PORT', '') - name = settings_dict.get('NAME', '') - user = settings_dict.get('USER', '') - passwd = settings_dict.get('PASSWORD', '') - - if user: - args += ['-U', user] - if host: - args += ['-h', host] - if port: - args += ['-p', str(port)] - args += [name] - - temp_pgpass = None - try: - if passwd: - # Create temporary .pgpass file. - temp_pgpass = NamedTemporaryFile(mode='w+') - try: - print_( - _escape_pgpass(host) or '*', - str(port) or '*', - _escape_pgpass(name) or '*', - _escape_pgpass(user) or '*', - _escape_pgpass(passwd), - file=temp_pgpass, - sep=':', - flush=True, - ) - os.environ['PGPASSFILE'] = temp_pgpass.name - except UnicodeEncodeError: - # If the current locale can't encode the data, we let - # the user input the password manually. - pass - subprocess.call(args) - finally: - if temp_pgpass: - temp_pgpass.close() - if 'PGPASSFILE' in os.environ: # unit tests need cleanup - del os.environ['PGPASSFILE'] - - def runshell(self): - DatabaseClient.runshell_db(self.connection.settings_dict) diff --git a/django/db/backends/postgresql_psycopg2/creation.py b/django/db/backends/postgresql_psycopg2/creation.py deleted file mode 100644 index 13eea0d3b7..0000000000 --- a/django/db/backends/postgresql_psycopg2/creation.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.db.backends.base.creation import BaseDatabaseCreation - - -class DatabaseCreation(BaseDatabaseCreation): - - def sql_table_creation_suffix(self): - test_settings = self.connection.settings_dict['TEST'] - assert test_settings['COLLATION'] is None, ( - "PostgreSQL does not support collation setting at database creation time." - ) - if test_settings['CHARSET']: - return "WITH ENCODING '%s'" % test_settings['CHARSET'] - return '' diff --git a/django/db/backends/postgresql_psycopg2/features.py b/django/db/backends/postgresql_psycopg2/features.py deleted file mode 100644 index a07700e601..0000000000 --- a/django/db/backends/postgresql_psycopg2/features.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db.backends.base.features import BaseDatabaseFeatures -from django.db.utils import InterfaceError - - -class DatabaseFeatures(BaseDatabaseFeatures): - allows_group_by_selected_pks = True - can_return_id_from_insert = True - has_real_datatype = True - has_native_uuid_field = True - has_native_duration_field = True - driver_supports_timedelta_args = True - can_defer_constraint_checks = True - has_select_for_update = True - has_select_for_update_nowait = True - has_bulk_insert = True - uses_savepoints = True - can_release_savepoints = True - supports_tablespaces = True - supports_transactions = True - can_introspect_autofield = True - can_introspect_ip_address_field = True - can_introspect_small_integer_field = True - can_distinct_on_fields = True - can_rollback_ddl = True - supports_combined_alters = True - nulls_order_largest = True - closed_cursor_error_class = InterfaceError - has_case_insensitive_like = False - requires_sqlparse_for_splitting = False - greatest_least_ignores_nulls = True diff --git a/django/db/backends/postgresql_psycopg2/introspection.py b/django/db/backends/postgresql_psycopg2/introspection.py deleted file mode 100644 index 9b3e9074b2..0000000000 --- a/django/db/backends/postgresql_psycopg2/introspection.py +++ /dev/null @@ -1,229 +0,0 @@ -from __future__ import unicode_literals - -from collections import namedtuple - -from django.db.backends.base.introspection import ( - BaseDatabaseIntrospection, FieldInfo, TableInfo, -) -from django.utils.encoding import force_text - -FieldInfo = namedtuple('FieldInfo', FieldInfo._fields + ('default',)) - - -class DatabaseIntrospection(BaseDatabaseIntrospection): - # Maps type codes to Django Field types. - data_types_reverse = { - 16: 'BooleanField', - 17: 'BinaryField', - 20: 'BigIntegerField', - 21: 'SmallIntegerField', - 23: 'IntegerField', - 25: 'TextField', - 700: 'FloatField', - 701: 'FloatField', - 869: 'GenericIPAddressField', - 1042: 'CharField', # blank-padded - 1043: 'CharField', - 1082: 'DateField', - 1083: 'TimeField', - 1114: 'DateTimeField', - 1184: 'DateTimeField', - 1266: 'TimeField', - 1700: 'DecimalField', - } - - ignored_tables = [] - - _get_indexes_query = """ - SELECT attr.attname, idx.indkey, idx.indisunique, idx.indisprimary - FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, - pg_catalog.pg_index idx, pg_catalog.pg_attribute attr - WHERE c.oid = idx.indrelid - AND idx.indexrelid = c2.oid - AND attr.attrelid = c.oid - AND attr.attnum = idx.indkey[0] - AND c.relname = %s""" - - def get_field_type(self, data_type, description): - field_type = super(DatabaseIntrospection, self).get_field_type(data_type, description) - if field_type == 'IntegerField' and description.default and 'nextval' in description.default: - return 'AutoField' - return field_type - - def get_table_list(self, cursor): - """ - Returns a list of table and view names in the current database. - """ - cursor.execute(""" - SELECT c.relname, c.relkind - FROM pg_catalog.pg_class c - LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('r', 'v') - AND n.nspname NOT IN ('pg_catalog', 'pg_toast') - AND pg_catalog.pg_table_is_visible(c.oid)""") - return [TableInfo(row[0], {'r': 't', 'v': 'v'}.get(row[1])) - for row in cursor.fetchall() - if row[0] not in self.ignored_tables] - - def get_table_description(self, cursor, table_name): - "Returns a description of the table, with the DB-API cursor.description interface." - # As cursor.description does not return reliably the nullable property, - # we have to query the information_schema (#7783) - cursor.execute(""" - SELECT column_name, is_nullable, column_default - FROM information_schema.columns - WHERE table_name = %s""", [table_name]) - field_map = {line[0]: line[1:] for line in cursor.fetchall()} - cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name)) - return [FieldInfo(*((force_text(line[0]),) + line[1:6] - + (field_map[force_text(line[0])][0] == 'YES', field_map[force_text(line[0])][1]))) - for line in cursor.description] - - def get_relations(self, cursor, table_name): - """ - Returns a dictionary of {field_name: (field_name_other_table, other_table)} - representing all relationships to the given table. - """ - cursor.execute(""" - SELECT c2.relname, a1.attname, a2.attname - FROM pg_constraint con - LEFT JOIN pg_class c1 ON con.conrelid = c1.oid - LEFT JOIN pg_class c2 ON con.confrelid = c2.oid - LEFT JOIN pg_attribute a1 ON c1.oid = a1.attrelid AND a1.attnum = con.conkey[1] - LEFT JOIN pg_attribute a2 ON c2.oid = a2.attrelid AND a2.attnum = con.confkey[1] - WHERE c1.relname = %s - AND con.contype = 'f'""", [table_name]) - relations = {} - for row in cursor.fetchall(): - relations[row[1]] = (row[2], row[0]) - return relations - - def get_key_columns(self, cursor, table_name): - key_columns = [] - cursor.execute(""" - SELECT kcu.column_name, ccu.table_name AS referenced_table, ccu.column_name AS referenced_column - FROM information_schema.constraint_column_usage ccu - LEFT JOIN information_schema.key_column_usage kcu - ON ccu.constraint_catalog = kcu.constraint_catalog - AND ccu.constraint_schema = kcu.constraint_schema - AND ccu.constraint_name = kcu.constraint_name - LEFT JOIN information_schema.table_constraints tc - ON ccu.constraint_catalog = tc.constraint_catalog - AND ccu.constraint_schema = tc.constraint_schema - AND ccu.constraint_name = tc.constraint_name - WHERE kcu.table_name = %s AND tc.constraint_type = 'FOREIGN KEY'""", [table_name]) - key_columns.extend(cursor.fetchall()) - return key_columns - - def get_indexes(self, cursor, table_name): - # This query retrieves each index on the given table, including the - # first associated field name - cursor.execute(self._get_indexes_query, [table_name]) - indexes = {} - for row in cursor.fetchall(): - # row[1] (idx.indkey) is stored in the DB as an array. It comes out as - # a string of space-separated integers. This designates the field - # indexes (1-based) of the fields that have indexes on the table. - # Here, we skip any indexes across multiple fields. - if ' ' in row[1]: - continue - if row[0] not in indexes: - indexes[row[0]] = {'primary_key': False, 'unique': False} - # It's possible to have the unique and PK constraints in separate indexes. - if row[3]: - indexes[row[0]]['primary_key'] = True - if row[2]: - indexes[row[0]]['unique'] = True - return indexes - - def get_constraints(self, cursor, table_name): - """ - Retrieves any constraints or keys (unique, pk, fk, check, index) across one or more columns. - """ - constraints = {} - # Loop over the key table, collecting things as constraints - # This will get PKs, FKs, and uniques, but not CHECK - cursor.execute(""" - SELECT - kc.constraint_name, - kc.column_name, - c.constraint_type, - array(SELECT table_name::text || '.' || column_name::text - FROM information_schema.constraint_column_usage - WHERE constraint_name = kc.constraint_name) - FROM information_schema.key_column_usage AS kc - JOIN information_schema.table_constraints AS c ON - kc.table_schema = c.table_schema AND - kc.table_name = c.table_name AND - kc.constraint_name = c.constraint_name - WHERE - kc.table_schema = %s AND - kc.table_name = %s - ORDER BY kc.ordinal_position ASC - """, ["public", table_name]) - for constraint, column, kind, used_cols in cursor.fetchall(): - # If we're the first column, make the record - if constraint not in constraints: - constraints[constraint] = { - "columns": [], - "primary_key": kind.lower() == "primary key", - "unique": kind.lower() in ["primary key", "unique"], - "foreign_key": tuple(used_cols[0].split(".", 1)) if kind.lower() == "foreign key" else None, - "check": False, - "index": False, - } - # Record the details - constraints[constraint]['columns'].append(column) - # Now get CHECK constraint columns - cursor.execute(""" - SELECT kc.constraint_name, kc.column_name - FROM information_schema.constraint_column_usage AS kc - JOIN information_schema.table_constraints AS c ON - kc.table_schema = c.table_schema AND - kc.table_name = c.table_name AND - kc.constraint_name = c.constraint_name - WHERE - c.constraint_type = 'CHECK' AND - kc.table_schema = %s AND - kc.table_name = %s - """, ["public", table_name]) - for constraint, column in cursor.fetchall(): - # If we're the first column, make the record - if constraint not in constraints: - constraints[constraint] = { - "columns": [], - "primary_key": False, - "unique": False, - "foreign_key": None, - "check": True, - "index": False, - } - # Record the details - constraints[constraint]['columns'].append(column) - # Now get indexes - cursor.execute(""" - SELECT - c2.relname, - ARRAY( - SELECT (SELECT attname FROM pg_catalog.pg_attribute WHERE attnum = i AND attrelid = c.oid) - FROM unnest(idx.indkey) i - ), - idx.indisunique, - idx.indisprimary - FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, - pg_catalog.pg_index idx - WHERE c.oid = idx.indrelid - AND idx.indexrelid = c2.oid - AND c.relname = %s - """, [table_name]) - for index, columns, unique, primary in cursor.fetchall(): - if index not in constraints: - constraints[index] = { - "columns": list(columns), - "primary_key": primary, - "unique": unique, - "foreign_key": None, - "check": False, - "index": True, - } - return constraints diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py deleted file mode 100644 index 866e2ca38b..0000000000 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ /dev/null @@ -1,240 +0,0 @@ -from __future__ import unicode_literals - -from psycopg2.extras import Inet - -from django.conf import settings -from django.db.backends.base.operations import BaseDatabaseOperations - - -class DatabaseOperations(BaseDatabaseOperations): - def unification_cast_sql(self, output_field): - internal_type = output_field.get_internal_type() - if internal_type in ("GenericIPAddressField", "IPAddressField", "TimeField", "UUIDField"): - # PostgreSQL will resolve a union as type 'text' if input types are - # 'unknown'. - # http://www.postgresql.org/docs/9.4/static/typeconv-union-case.html - # These fields cannot be implicitly cast back in the default - # PostgreSQL configuration so we need to explicitly cast them. - # We must also remove components of the type within brackets: - # varchar(255) -> varchar. - return 'CAST(%%s AS %s)' % output_field.db_type(self.connection).split('(')[0] - return '%s' - - def date_extract_sql(self, lookup_type, field_name): - # http://www.postgresql.org/docs/current/static/functions-datetime.html#FUNCTIONS-DATETIME-EXTRACT - if lookup_type == 'week_day': - # For consistency across backends, we return Sunday=1, Saturday=7. - return "EXTRACT('dow' FROM %s) + 1" % field_name - else: - return "EXTRACT('%s' FROM %s)" % (lookup_type, field_name) - - def date_trunc_sql(self, lookup_type, field_name): - # http://www.postgresql.org/docs/current/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC - return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) - - def _convert_field_to_tz(self, field_name, tzname): - if settings.USE_TZ: - field_name = "%s AT TIME ZONE %%s" % field_name - params = [tzname] - else: - params = [] - return field_name, params - - def datetime_cast_date_sql(self, field_name, tzname): - field_name, params = self._convert_field_to_tz(field_name, tzname) - sql = '(%s)::date' % field_name - return sql, params - - def datetime_extract_sql(self, lookup_type, field_name, tzname): - field_name, params = self._convert_field_to_tz(field_name, tzname) - sql = self.date_extract_sql(lookup_type, field_name) - return sql, params - - def datetime_trunc_sql(self, lookup_type, field_name, tzname): - field_name, params = self._convert_field_to_tz(field_name, tzname) - # http://www.postgresql.org/docs/current/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC - sql = "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) - return sql, params - - def deferrable_sql(self): - return " DEFERRABLE INITIALLY DEFERRED" - - def lookup_cast(self, lookup_type, internal_type=None): - lookup = '%s' - - # Cast text lookups to text to allow things like filter(x__contains=4) - if lookup_type in ('iexact', 'contains', 'icontains', 'startswith', - 'istartswith', 'endswith', 'iendswith', 'regex', 'iregex'): - if internal_type in ('IPAddressField', 'GenericIPAddressField'): - lookup = "HOST(%s)" - else: - lookup = "%s::text" - - # Use UPPER(x) for case-insensitive lookups; it's faster. - if lookup_type in ('iexact', 'icontains', 'istartswith', 'iendswith'): - lookup = 'UPPER(%s)' % lookup - - return lookup - - def last_insert_id(self, cursor, table_name, pk_name): - # Use pg_get_serial_sequence to get the underlying sequence name - # from the table name and column name (available since PostgreSQL 8) - cursor.execute("SELECT CURRVAL(pg_get_serial_sequence('%s','%s'))" % ( - self.quote_name(table_name), pk_name)) - return cursor.fetchone()[0] - - def no_limit_value(self): - return None - - def prepare_sql_script(self, sql): - return [sql] - - def quote_name(self, name): - if name.startswith('"') and name.endswith('"'): - return name # Quoting once is enough. - return '"%s"' % name - - def set_time_zone_sql(self): - return "SET TIME ZONE %s" - - def sql_flush(self, style, tables, sequences, allow_cascade=False): - if tables: - # Perform a single SQL 'TRUNCATE x, y, z...;' statement. It allows - # us to truncate tables referenced by a foreign key in any other - # table. - tables_sql = ', '.join( - style.SQL_FIELD(self.quote_name(table)) for table in tables) - if allow_cascade: - sql = ['%s %s %s;' % ( - style.SQL_KEYWORD('TRUNCATE'), - tables_sql, - style.SQL_KEYWORD('CASCADE'), - )] - else: - sql = ['%s %s;' % ( - style.SQL_KEYWORD('TRUNCATE'), - tables_sql, - )] - sql.extend(self.sequence_reset_by_name_sql(style, sequences)) - return sql - else: - return [] - - def sequence_reset_by_name_sql(self, style, sequences): - # 'ALTER SEQUENCE sequence_name RESTART WITH 1;'... style SQL statements - # to reset sequence indices - sql = [] - for sequence_info in sequences: - table_name = sequence_info['table'] - column_name = sequence_info['column'] - if not (column_name and len(column_name) > 0): - # This will be the case if it's an m2m using an autogenerated - # intermediate table (see BaseDatabaseIntrospection.sequence_list) - column_name = 'id' - sql.append("%s setval(pg_get_serial_sequence('%s','%s'), 1, false);" % - (style.SQL_KEYWORD('SELECT'), - style.SQL_TABLE(self.quote_name(table_name)), - style.SQL_FIELD(column_name)) - ) - return sql - - def tablespace_sql(self, tablespace, inline=False): - if inline: - return "USING INDEX TABLESPACE %s" % self.quote_name(tablespace) - else: - return "TABLESPACE %s" % self.quote_name(tablespace) - - def sequence_reset_sql(self, style, model_list): - from django.db import models - output = [] - qn = self.quote_name - for model in model_list: - # Use `coalesce` to set the sequence for each model to the max pk value if there are records, - # or 1 if there are none. Set the `is_called` property (the third argument to `setval`) to true - # if there are records (as the max pk value is already in use), otherwise set it to false. - # Use pg_get_serial_sequence to get the underlying sequence name from the table name - # and column name (available since PostgreSQL 8) - - for f in model._meta.local_fields: - if isinstance(f, models.AutoField): - output.append( - "%s setval(pg_get_serial_sequence('%s','%s'), " - "coalesce(max(%s), 1), max(%s) %s null) %s %s;" % ( - style.SQL_KEYWORD('SELECT'), - style.SQL_TABLE(qn(model._meta.db_table)), - style.SQL_FIELD(f.column), - style.SQL_FIELD(qn(f.column)), - style.SQL_FIELD(qn(f.column)), - style.SQL_KEYWORD('IS NOT'), - style.SQL_KEYWORD('FROM'), - style.SQL_TABLE(qn(model._meta.db_table)), - ) - ) - break # Only one AutoField is allowed per model, so don't bother continuing. - for f in model._meta.many_to_many: - if not f.remote_field.through: - output.append( - "%s setval(pg_get_serial_sequence('%s','%s'), " - "coalesce(max(%s), 1), max(%s) %s null) %s %s;" % ( - style.SQL_KEYWORD('SELECT'), - style.SQL_TABLE(qn(f.m2m_db_table())), - style.SQL_FIELD('id'), - style.SQL_FIELD(qn('id')), - style.SQL_FIELD(qn('id')), - style.SQL_KEYWORD('IS NOT'), - style.SQL_KEYWORD('FROM'), - style.SQL_TABLE(qn(f.m2m_db_table())) - ) - ) - return output - - def prep_for_iexact_query(self, x): - return x - - def max_name_length(self): - """ - Returns the maximum length of an identifier. - - Note that the maximum length of an identifier is 63 by default, but can - be changed by recompiling PostgreSQL after editing the NAMEDATALEN - macro in src/include/pg_config_manual.h . - - This implementation simply returns 63, but can easily be overridden by a - custom database backend that inherits most of its behavior from this one. - """ - - return 63 - - def distinct_sql(self, fields): - if fields: - return 'DISTINCT ON (%s)' % ', '.join(fields) - else: - return 'DISTINCT' - - def last_executed_query(self, cursor, sql, params): - # http://initd.org/psycopg/docs/cursor.html#cursor.query - # The query attribute is a Psycopg extension to the DB API 2.0. - if cursor.query is not None: - return cursor.query.decode('utf-8') - return None - - def return_insert_id(self): - return "RETURNING %s", () - - def bulk_insert_sql(self, fields, num_values): - items_sql = "(%s)" % ", ".join(["%s"] * len(fields)) - return "VALUES " + ", ".join([items_sql] * num_values) - - def adapt_datefield_value(self, value): - return value - - def adapt_datetimefield_value(self, value): - return value - - def adapt_timefield_value(self, value): - return value - - def adapt_ipaddressfield_value(self, value): - if value: - return Inet(value) - return None diff --git a/django/db/backends/postgresql_psycopg2/schema.py b/django/db/backends/postgresql_psycopg2/schema.py deleted file mode 100644 index bc03a12e8d..0000000000 --- a/django/db/backends/postgresql_psycopg2/schema.py +++ /dev/null @@ -1,91 +0,0 @@ -import psycopg2 - -from django.db.backends.base.schema import BaseDatabaseSchemaEditor - - -class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): - - sql_alter_column_type = "ALTER COLUMN %(column)s TYPE %(type)s USING %(column)s::%(type)s" - - sql_create_sequence = "CREATE SEQUENCE %(sequence)s" - sql_delete_sequence = "DROP SEQUENCE IF EXISTS %(sequence)s CASCADE" - sql_set_sequence_max = "SELECT setval('%(sequence)s', MAX(%(column)s)) FROM %(table)s" - - sql_create_varchar_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s varchar_pattern_ops)%(extra)s" - sql_create_text_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s text_pattern_ops)%(extra)s" - - def quote_value(self, value): - return psycopg2.extensions.adapt(value) - - def _model_indexes_sql(self, model): - output = super(DatabaseSchemaEditor, self)._model_indexes_sql(model) - if not model._meta.managed or model._meta.proxy or model._meta.swapped: - return output - - for field in model._meta.local_fields: - db_type = field.db_type(connection=self.connection) - if db_type is not None and (field.db_index or field.unique): - # Fields with database column types of `varchar` and `text` need - # a second index that specifies their operator class, which is - # needed when performing correct LIKE queries outside the - # C locale. See #12234. - if db_type.startswith('varchar'): - output.append(self._create_index_sql( - model, [field], suffix='_like', sql=self.sql_create_varchar_index)) - elif db_type.startswith('text'): - output.append(self._create_index_sql( - model, [field], suffix='_like', sql=self.sql_create_text_index)) - return output - - def _alter_column_type_sql(self, table, old_field, new_field, new_type): - """ - Makes ALTER TYPE with SERIAL make sense. - """ - if new_type.lower() == "serial": - column = new_field.column - sequence_name = "%s_%s_seq" % (table, column) - return ( - ( - self.sql_alter_column_type % { - "column": self.quote_name(column), - "type": "integer", - }, - [], - ), - [ - ( - self.sql_delete_sequence % { - "sequence": self.quote_name(sequence_name), - }, - [], - ), - ( - self.sql_create_sequence % { - "sequence": self.quote_name(sequence_name), - }, - [], - ), - ( - self.sql_alter_column % { - "table": self.quote_name(table), - "changes": self.sql_alter_column_default % { - "column": self.quote_name(column), - "default": "nextval('%s')" % self.quote_name(sequence_name), - } - }, - [], - ), - ( - self.sql_set_sequence_max % { - "table": self.quote_name(table), - "column": self.quote_name(column), - "sequence": self.quote_name(sequence_name), - }, - [], - ), - ], - ) - else: - return super(DatabaseSchemaEditor, self)._alter_column_type_sql( - table, old_field, new_field, new_type - ) diff --git a/django/db/backends/postgresql_psycopg2/utils.py b/django/db/backends/postgresql_psycopg2/utils.py deleted file mode 100644 index 2c03ab36cd..0000000000 --- a/django/db/backends/postgresql_psycopg2/utils.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.utils.timezone import utc - - -def utc_tzinfo_factory(offset): - if offset != 0: - raise AssertionError("database connection isn't set to UTC") - return utc diff --git a/django/db/backends/postgresql_psycopg2/version.py b/django/db/backends/postgresql_psycopg2/version.py deleted file mode 100644 index d558fb2e51..0000000000 --- a/django/db/backends/postgresql_psycopg2/version.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Extracts the version of the PostgreSQL server. -""" - -import re - -# This reg-exp is intentionally fairly flexible here. -# Needs to be able to handle stuff like: -# PostgreSQL #.#.# -# EnterpriseDB #.# -# PostgreSQL #.# beta# -# PostgreSQL #.#beta# -VERSION_RE = re.compile(r'\S+ (\d+)\.(\d+)\.?(\d+)?') - - -def _parse_version(text): - "Internal parsing method. Factored out for testing purposes." - major, major2, minor = VERSION_RE.search(text).groups() - try: - return int(major) * 10000 + int(major2) * 100 + int(minor) - except (ValueError, TypeError): - return int(major) * 10000 + int(major2) * 100 - - -def get_version(connection): - """ - Returns an integer representing the major, minor and revision number of the - server. Format is the one used for the return value of libpq - PQServerVersion()/``server_version`` connection attribute (available in - newer psycopg2 versions.) - - For example, 90304 for 9.3.4. The last two digits will be 00 in the case of - releases (e.g., 90400 for 'PostgreSQL 9.4') or in the case of beta and - prereleases (e.g. 90100 for 'PostgreSQL 9.1beta2'). - - PQServerVersion()/``server_version`` doesn't execute a query so try that - first, then fallback to a ``SELECT version()`` query. - """ - if hasattr(connection, 'server_version'): - return connection.server_version - else: - with connection.cursor() as cursor: - cursor.execute("SELECT version()") - return _parse_version(cursor.fetchone()[0]) diff --git a/django/db/utils.py b/django/db/utils.py index 64d44a715b..380a4b9554 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -104,7 +104,14 @@ class DatabaseErrorWrapper(object): def load_backend(backend_name): - # Look for a fully qualified database backend name + """ + Return a database backend's "base" module given a fully qualified database + backend name, or raise an error if it doesn't exist. + """ + # This backend was renamed in Django 1.9. + if backend_name == 'django.db.backends.postgresql_psycopg2': + backend_name = 'django.db.backends.postgresql' + try: return import_module('%s.base' % backend_name) except ImportError as e_user: @@ -114,7 +121,8 @@ def load_backend(backend_name): try: builtin_backends = [ name for _, name, ispkg in pkgutil.iter_modules([npath(backend_dir)]) - if ispkg and name not in {'base', 'dummy'}] + if ispkg and name not in {'base', 'dummy', 'postgresql_psycopg2'} + ] except EnvironmentError: builtin_backends = [] if backend_name not in ['django.db.backends.%s' % b for b in diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index e1dd7d0bc7..049d33c1f1 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -26,7 +26,7 @@ settings: * :setting:`ENGINE ` -- Either ``'django.db.backends.sqlite3'``, - ``'django.db.backends.postgresql_psycopg2'``, + ``'django.db.backends.postgresql'``, ``'django.db.backends.mysql'``, or ``'django.db.backends.oracle'``. Other backends are :ref:`also available `. diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index f21887c6bd..47a75ddfc6 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1512,7 +1512,7 @@ make the call non-blocking. If a conflicting lock is already acquired by another transaction, :exc:`~django.db.DatabaseError` will be raised when the queryset is evaluated. -Currently, the ``postgresql_psycopg2``, ``oracle``, and ``mysql`` database +Currently, the ``postgresql``, ``oracle``, and ``mysql`` database backends support ``select_for_update()``. However, MySQL has no support for the ``nowait`` argument. Obviously, users of external third-party backends should check with their backend's documentation for specifics in those cases. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 1077b910f2..eccba53274 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -459,7 +459,7 @@ other database types. This example is for PostgreSQL:: DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'mydatabase', 'USER': 'mydatabaseuser', 'PASSWORD': 'mypassword', @@ -500,14 +500,19 @@ Default: ``''`` (Empty string) The database backend to use. The built-in database backends are: -* ``'django.db.backends.postgresql_psycopg2'`` +* ``'django.db.backends.postgresql'`` * ``'django.db.backends.mysql'`` * ``'django.db.backends.sqlite3'`` * ``'django.db.backends.oracle'`` You can use a database backend that doesn't ship with Django by setting -``ENGINE`` to a fully-qualified path (i.e. -``mypackage.backends.whatever``). +``ENGINE`` to a fully-qualified path (i.e. ``mypackage.backends.whatever``). + +.. versionchanged:: 1.9 + + The ``django.db.backends.postgresql`` backend is named + ``django.db.backends.postgresql_psycopg2`` in older releases. For backwards + compatibility, the old name still works in newer versions. .. setting:: HOST @@ -657,8 +662,7 @@ The character set encoding used to create the test database. The value of this string is passed directly through to the database, so its format is backend-specific. -Supported for the PostgreSQL_ (``postgresql_psycopg2``) and MySQL_ (``mysql``) -backends. +Supported by the PostgreSQL_ (``postgresql``) and MySQL_ (``mysql``) backends. .. _PostgreSQL: http://www.postgresql.org/docs/current/static/multibyte.html .. _MySQL: http://dev.mysql.com/doc/refman/5.6/en/charset-database.html diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 1636fee9fc..aa85904843 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -625,7 +625,7 @@ Arguments sent with this signal: ``sender`` The database wrapper class -- i.e. - ``django.db.backends.postgresql_psycopg2.DatabaseWrapper`` or + ``django.db.backends.postgresql.DatabaseWrapper`` or ``django.db.backends.mysql.DatabaseWrapper``, etc. ``connection`` diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index 1f1183d9a7..5d017b3042 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -578,6 +578,13 @@ Validators * Added :func:`~django.core.validators.validate_unicode_slug` to validate slugs that may contain Unicode characters. +Database backends +^^^^^^^^^^^^^^^^^ + +* The PostgreSQL backend (``django.db.backends.postgresql_psycopg2``) is also + available as ``django.db.backends.postgresql``. The old name will continue to + be available for backwards compatibility. + Backwards incompatible changes in 1.9 ===================================== diff --git a/docs/topics/db/multi-db.txt b/docs/topics/db/multi-db.txt index 6b4f6a0a66..fca21388c9 100644 --- a/docs/topics/db/multi-db.txt +++ b/docs/topics/db/multi-db.txt @@ -29,7 +29,7 @@ databases -- a default PostgreSQL database and a MySQL database called DATABASES = { 'default': { 'NAME': 'app_data', - 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'ENGINE': 'django.db.backends.postgresql', 'USER': 'postgres_user', 'PASSWORD': 's3krit' }, diff --git a/tests/backends/test_utils.py b/tests/backends/test_utils.py index beec31d2f5..6f59d1b23b 100644 --- a/tests/backends/test_utils.py +++ b/tests/backends/test_utils.py @@ -9,7 +9,7 @@ class TestLoadBackend(SimpleTestCase): msg = ( "'foo' isn't an available database backend.\n" "Try using 'django.db.backends.XXX', where XXX is one of:\n" - " 'mysql', 'oracle', 'postgresql_psycopg2', 'sqlite3'\n" + " 'mysql', 'oracle', 'postgresql', 'sqlite3'\n" "Error was: No module named %s" ) % "foo.base" if six.PY2 else "'foo'" with self.assertRaisesMessage(ImproperlyConfigured, msg): diff --git a/tests/backends/tests.py b/tests/backends/tests.py index fc62c6587c..b918be3e51 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -18,7 +18,7 @@ from django.db import ( reset_queries, transaction, ) from django.db.backends.base.base import BaseDatabaseWrapper -from django.db.backends.postgresql_psycopg2 import version as pg_version +from django.db.backends.postgresql import version as pg_version from django.db.backends.signals import connection_created from django.db.backends.utils import CursorWrapper, format_number from django.db.models import Avg, StdDev, Sum, Variance @@ -313,7 +313,7 @@ class PostgreSQLTests(TestCase): self.assertEqual(a[0], b[0]) def test_lookup_cast(self): - from django.db.backends.postgresql_psycopg2.operations import DatabaseOperations + from django.db.backends.postgresql.operations import DatabaseOperations do = DatabaseOperations(connection=None) for lookup in ('iexact', 'contains', 'icontains', 'startswith', @@ -321,8 +321,8 @@ class PostgreSQLTests(TestCase): self.assertIn('::text', do.lookup_cast(lookup)) def test_correct_extraction_psycopg2_version(self): - from django.db.backends.postgresql_psycopg2.base import psycopg2_version - version_path = 'django.db.backends.postgresql_psycopg2.base.Database.__version__' + from django.db.backends.postgresql.base import psycopg2_version + version_path = 'django.db.backends.postgresql.base.Database.__version__' with mock.patch(version_path, '2.6.9'): self.assertEqual(psycopg2_version(), (2, 6, 9)) diff --git a/tests/dbshell/test_postgresql_psycopg2.py b/tests/dbshell/test_postgresql_psycopg2.py index 0bb8721b70..f74ea3ddb2 100644 --- a/tests/dbshell/test_postgresql_psycopg2.py +++ b/tests/dbshell/test_postgresql_psycopg2.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import locale import os -from django.db.backends.postgresql_psycopg2.client import DatabaseClient +from django.db.backends.postgresql.client import DatabaseClient from django.test import SimpleTestCase, mock from django.utils import six from django.utils.encoding import force_bytes, force_str -- cgit v1.3