Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

base.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. """
  2. PostgreSQL database backend for Django.
  3. Requires psycopg 2: http://initd.org/projects/psycopg2
  4. """
  5. import threading
  6. import warnings
  7. from django.conf import settings
  8. from django.core.exceptions import ImproperlyConfigured
  9. from django.db import connections
  10. from django.db.backends.base.base import BaseDatabaseWrapper
  11. from django.db.utils import DatabaseError as WrappedDatabaseError
  12. from django.utils.functional import cached_property
  13. from django.utils.safestring import SafeText
  14. from django.utils.version import get_version_tuple
  15. try:
  16. import psycopg2 as Database
  17. import psycopg2.extensions
  18. import psycopg2.extras
  19. except ImportError as e:
  20. raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e)
  21. def psycopg2_version():
  22. version = psycopg2.__version__.split(' ', 1)[0]
  23. return get_version_tuple(version)
  24. PSYCOPG2_VERSION = psycopg2_version()
  25. if PSYCOPG2_VERSION < (2, 5, 4):
  26. raise ImproperlyConfigured("psycopg2_version 2.5.4 or newer is required; you have %s" % psycopg2.__version__)
  27. # Some of these import psycopg2, so import them after checking if it's installed.
  28. from .client import DatabaseClient # NOQA isort:skip
  29. from .creation import DatabaseCreation # NOQA isort:skip
  30. from .features import DatabaseFeatures # NOQA isort:skip
  31. from .introspection import DatabaseIntrospection # NOQA isort:skip
  32. from .operations import DatabaseOperations # NOQA isort:skip
  33. from .schema import DatabaseSchemaEditor # NOQA isort:skip
  34. from .utils import utc_tzinfo_factory # NOQA isort:skip
  35. psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString)
  36. psycopg2.extras.register_uuid()
  37. # Register support for inet[] manually so we don't have to handle the Inet()
  38. # object on load all the time.
  39. INETARRAY_OID = 1041
  40. INETARRAY = psycopg2.extensions.new_array_type(
  41. (INETARRAY_OID,),
  42. 'INETARRAY',
  43. psycopg2.extensions.UNICODE,
  44. )
  45. psycopg2.extensions.register_type(INETARRAY)
  46. class DatabaseWrapper(BaseDatabaseWrapper):
  47. vendor = 'postgresql'
  48. display_name = 'PostgreSQL'
  49. # This dictionary maps Field objects to their associated PostgreSQL column
  50. # types, as strings. Column-type strings can contain format strings; they'll
  51. # be interpolated against the values of Field.__dict__ before being output.
  52. # If a column type is set to None, it won't be included in the output.
  53. data_types = {
  54. 'AutoField': 'serial',
  55. 'BigAutoField': 'bigserial',
  56. 'BinaryField': 'bytea',
  57. 'BooleanField': 'boolean',
  58. 'CharField': 'varchar(%(max_length)s)',
  59. 'DateField': 'date',
  60. 'DateTimeField': 'timestamp with time zone',
  61. 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)',
  62. 'DurationField': 'interval',
  63. 'FileField': 'varchar(%(max_length)s)',
  64. 'FilePathField': 'varchar(%(max_length)s)',
  65. 'FloatField': 'double precision',
  66. 'IntegerField': 'integer',
  67. 'BigIntegerField': 'bigint',
  68. 'IPAddressField': 'inet',
  69. 'GenericIPAddressField': 'inet',
  70. 'NullBooleanField': 'boolean',
  71. 'OneToOneField': 'integer',
  72. 'PositiveIntegerField': 'integer',
  73. 'PositiveSmallIntegerField': 'smallint',
  74. 'SlugField': 'varchar(%(max_length)s)',
  75. 'SmallIntegerField': 'smallint',
  76. 'TextField': 'text',
  77. 'TimeField': 'time',
  78. 'UUIDField': 'uuid',
  79. }
  80. data_type_check_constraints = {
  81. 'PositiveIntegerField': '"%(column)s" >= 0',
  82. 'PositiveSmallIntegerField': '"%(column)s" >= 0',
  83. }
  84. operators = {
  85. 'exact': '= %s',
  86. 'iexact': '= UPPER(%s)',
  87. 'contains': 'LIKE %s',
  88. 'icontains': 'LIKE UPPER(%s)',
  89. 'regex': '~ %s',
  90. 'iregex': '~* %s',
  91. 'gt': '> %s',
  92. 'gte': '>= %s',
  93. 'lt': '< %s',
  94. 'lte': '<= %s',
  95. 'startswith': 'LIKE %s',
  96. 'endswith': 'LIKE %s',
  97. 'istartswith': 'LIKE UPPER(%s)',
  98. 'iendswith': 'LIKE UPPER(%s)',
  99. }
  100. # The patterns below are used to generate SQL pattern lookup clauses when
  101. # the right-hand side of the lookup isn't a raw string (it might be an expression
  102. # or the result of a bilateral transformation).
  103. # In those cases, special characters for LIKE operators (e.g. \, *, _) should be
  104. # escaped on database side.
  105. #
  106. # Note: we use str.format() here for readability as '%' is used as a wildcard for
  107. # the LIKE operator.
  108. pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\', '\\'), '%%', '\%%'), '_', '\_')"
  109. pattern_ops = {
  110. 'contains': "LIKE '%%' || {} || '%%'",
  111. 'icontains': "LIKE '%%' || UPPER({}) || '%%'",
  112. 'startswith': "LIKE {} || '%%'",
  113. 'istartswith': "LIKE UPPER({}) || '%%'",
  114. 'endswith': "LIKE '%%' || {}",
  115. 'iendswith': "LIKE '%%' || UPPER({})",
  116. }
  117. Database = Database
  118. SchemaEditorClass = DatabaseSchemaEditor
  119. # Classes instantiated in __init__().
  120. client_class = DatabaseClient
  121. creation_class = DatabaseCreation
  122. features_class = DatabaseFeatures
  123. introspection_class = DatabaseIntrospection
  124. ops_class = DatabaseOperations
  125. # PostgreSQL backend-specific attributes.
  126. _named_cursor_idx = 0
  127. def get_connection_params(self):
  128. settings_dict = self.settings_dict
  129. # None may be used to connect to the default 'postgres' db
  130. if settings_dict['NAME'] == '':
  131. raise ImproperlyConfigured(
  132. "settings.DATABASES is improperly configured. "
  133. "Please supply the NAME value.")
  134. if len(settings_dict['NAME'] or '') > self.ops.max_name_length():
  135. raise ImproperlyConfigured(
  136. "The database name '%s' (%d characters) is longer than "
  137. "PostgreSQL's limit of %d characters. Supply a shorter NAME "
  138. "in settings.DATABASES." % (
  139. settings_dict['NAME'],
  140. len(settings_dict['NAME']),
  141. self.ops.max_name_length(),
  142. )
  143. )
  144. conn_params = {
  145. 'database': settings_dict['NAME'] or 'postgres',
  146. **settings_dict['OPTIONS'],
  147. }
  148. conn_params.pop('isolation_level', None)
  149. if settings_dict['USER']:
  150. conn_params['user'] = settings_dict['USER']
  151. if settings_dict['PASSWORD']:
  152. conn_params['password'] = settings_dict['PASSWORD']
  153. if settings_dict['HOST']:
  154. conn_params['host'] = settings_dict['HOST']
  155. if settings_dict['PORT']:
  156. conn_params['port'] = settings_dict['PORT']
  157. return conn_params
  158. def get_new_connection(self, conn_params):
  159. connection = Database.connect(**conn_params)
  160. # self.isolation_level must be set:
  161. # - after connecting to the database in order to obtain the database's
  162. # default when no value is explicitly specified in options.
  163. # - before calling _set_autocommit() because if autocommit is on, that
  164. # will set connection.isolation_level to ISOLATION_LEVEL_AUTOCOMMIT.
  165. options = self.settings_dict['OPTIONS']
  166. try:
  167. self.isolation_level = options['isolation_level']
  168. except KeyError:
  169. self.isolation_level = connection.isolation_level
  170. else:
  171. # Set the isolation level to the value from OPTIONS.
  172. if self.isolation_level != connection.isolation_level:
  173. connection.set_session(isolation_level=self.isolation_level)
  174. return connection
  175. def ensure_timezone(self):
  176. self.ensure_connection()
  177. conn_timezone_name = self.connection.get_parameter_status('TimeZone')
  178. timezone_name = self.timezone_name
  179. if timezone_name and conn_timezone_name != timezone_name:
  180. with self.connection.cursor() as cursor:
  181. cursor.execute(self.ops.set_time_zone_sql(), [timezone_name])
  182. return True
  183. return False
  184. def init_connection_state(self):
  185. self.connection.set_client_encoding('UTF8')
  186. timezone_changed = self.ensure_timezone()
  187. if timezone_changed:
  188. # Commit after setting the time zone (see #17062)
  189. if not self.get_autocommit():
  190. self.connection.commit()
  191. def create_cursor(self, name=None):
  192. if name:
  193. # In autocommit mode, the cursor will be used outside of a
  194. # transaction, hence use a holdable cursor.
  195. cursor = self.connection.cursor(name, scrollable=False, withhold=self.connection.autocommit)
  196. else:
  197. cursor = self.connection.cursor()
  198. cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
  199. return cursor
  200. def chunked_cursor(self):
  201. self._named_cursor_idx += 1
  202. return self._cursor(
  203. name='_django_curs_%d_%d' % (
  204. # Avoid reusing name in other threads
  205. threading.current_thread().ident,
  206. self._named_cursor_idx,
  207. )
  208. )
  209. def _set_autocommit(self, autocommit):
  210. with self.wrap_database_errors:
  211. self.connection.autocommit = autocommit
  212. def check_constraints(self, table_names=None):
  213. """
  214. Check constraints by setting them to immediate. Return them to deferred
  215. afterward.
  216. """
  217. self.cursor().execute('SET CONSTRAINTS ALL IMMEDIATE')
  218. self.cursor().execute('SET CONSTRAINTS ALL DEFERRED')
  219. def is_usable(self):
  220. try:
  221. # Use a psycopg cursor directly, bypassing Django's utilities.
  222. self.connection.cursor().execute("SELECT 1")
  223. except Database.Error:
  224. return False
  225. else:
  226. return True
  227. @property
  228. def _nodb_connection(self):
  229. nodb_connection = super()._nodb_connection
  230. try:
  231. nodb_connection.ensure_connection()
  232. except (Database.DatabaseError, WrappedDatabaseError):
  233. warnings.warn(
  234. "Normally Django will use a connection to the 'postgres' database "
  235. "to avoid running initialization queries against the production "
  236. "database when it's not needed (for example, when running tests). "
  237. "Django was unable to create a connection to the 'postgres' database "
  238. "and will use the first PostgreSQL database instead.",
  239. RuntimeWarning
  240. )
  241. for connection in connections.all():
  242. if connection.vendor == 'postgresql' and connection.settings_dict['NAME'] != 'postgres':
  243. return self.__class__(
  244. {**self.settings_dict, 'NAME': connection.settings_dict['NAME']},
  245. alias=self.alias,
  246. allow_thread_sharing=False,
  247. )
  248. return nodb_connection
  249. @cached_property
  250. def pg_version(self):
  251. with self.temporary_connection():
  252. return self.connection.server_version