|
- """
- 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 self.timezone_name and 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
-
- @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)
|