base.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. """Microsoft SQL Server database backend for Django."""
  2. from __future__ import absolute_import, unicode_literals
  3. import warnings
  4. from django.core.exceptions import ImproperlyConfigured, ValidationError
  5. from django.core.validators import validate_ipv46_address as ip_validator
  6. from django.db.backends.base.base import BaseDatabaseWrapper
  7. from django.db.backends.base.client import BaseDatabaseClient
  8. from django.db.backends.base.validation import BaseDatabaseValidation
  9. from django.db.utils import IntegrityError
  10. from . import dbapi as Database
  11. from .introspection import DatabaseIntrospection
  12. from .creation import DatabaseCreation
  13. from .features import DatabaseFeatures
  14. from .operations import DatabaseOperations
  15. from .schema import DatabaseSchemaEditor
  16. def is_ip_address(value):
  17. """
  18. Returns True if value is a valid IP address, otherwise False.
  19. """
  20. try:
  21. ip_validator(value)
  22. except ValidationError:
  23. return False
  24. return True
  25. def connection_string_from_settings():
  26. from django.conf import settings
  27. db_settings = getattr(settings, 'DATABASES', {}).get('default', None)
  28. return make_connection_string(db_settings)
  29. def make_connection_string(settings):
  30. db_name = settings['NAME'].strip()
  31. db_host = settings['HOST'] or '127.0.0.1'
  32. db_port = settings['PORT']
  33. db_user = settings['USER']
  34. db_password = settings['PASSWORD']
  35. options = settings.get('OPTIONS', {})
  36. if len(db_name) == 0:
  37. raise ImproperlyConfigured("You need to specify a DATABASE NAME in your Django settings file.")
  38. # Connection strings courtesy of:
  39. # http://www.connectionstrings.com/?carrier=sqlserver
  40. # If a port is given, force a TCP/IP connection. The host should be an IP address in this case.
  41. if db_port:
  42. if not is_ip_address(db_host):
  43. raise ImproperlyConfigured("When using DATABASE PORT, DATABASE HOST must be an IP address.")
  44. try:
  45. db_port = int(db_port)
  46. except ValueError:
  47. raise ImproperlyConfigured("DATABASE PORT must be a number.")
  48. db_host = '{0},{1};Network Library=DBMSSOCN'.format(db_host, db_port)
  49. # If no user is specified, use integrated security.
  50. if db_user != '':
  51. auth_string = 'UID={0};PWD={1}'.format(db_user, db_password)
  52. else:
  53. auth_string = 'Integrated Security=SSPI'
  54. parts = [
  55. 'DATA SOURCE={0};Initial Catalog={1}'.format(db_host, db_name),
  56. auth_string
  57. ]
  58. if not options.get('provider', None):
  59. options['provider'] = 'sqlncli10'
  60. parts.append('PROVIDER={0}'.format(options['provider']))
  61. extra_params = options.get('extra_params', '')
  62. if 'sqlncli' in options['provider'].lower() and 'datatypecompatibility=' not in extra_params.lower():
  63. # native client needs a compatibility mode that behaves like OLEDB
  64. parts.append('DataTypeCompatibility=80')
  65. if options.get('use_mars', True) and 'mars connection=' not in extra_params.lower():
  66. parts.append('MARS Connection=True')
  67. if extra_params:
  68. parts.append(options['extra_params'])
  69. return ";".join(parts)
  70. VERSION_SQL2012 = 11
  71. class DatabaseWrapper(BaseDatabaseWrapper):
  72. vendor = 'microsoft'
  73. Database = Database
  74. SchemaEditorClass = DatabaseSchemaEditor
  75. operators = {
  76. "exact": "= %s",
  77. "iexact": "LIKE %s ESCAPE '\\'",
  78. "contains": "LIKE %s ESCAPE '\\'",
  79. "icontains": "LIKE %s ESCAPE '\\'",
  80. "gt": "> %s",
  81. "gte": ">= %s",
  82. "lt": "< %s",
  83. "lte": "<= %s",
  84. "startswith": "LIKE %s ESCAPE '\\'",
  85. "endswith": "LIKE %s ESCAPE '\\'",
  86. "istartswith": "LIKE %s ESCAPE '\\'",
  87. "iendswith": "LIKE %s ESCAPE '\\'",
  88. }
  89. # The patterns below are used to generate SQL pattern lookup clauses when
  90. # the right-hand side of the lookup isn't a raw string (it might be an expression
  91. # or the result of a bilateral transformation).
  92. # In those cases, special characters for LIKE operators (e.g. \, *, _) should be
  93. # escaped on database side.
  94. #
  95. # Note: we use str.format() here for readability as '%' is used as a wildcard for
  96. # the LIKE operator.
  97. pattern_esc = r"REPLACE(REPLACE(REPLACE({}, '\', '\\'), '%%', '\%%'), '_', '\_')"
  98. pattern_ops = {
  99. 'contains': r"LIKE CONCAT('%%', {}, '%%') ESCAPE '\'",
  100. 'icontains': r"LIKE CONCAT('%%', {}, '%%') ESCAPE '\'",
  101. 'startswith': r"LIKE CONCAT({}, '%%') ESCAPE '\'",
  102. 'istartswith': r"LIKE CONCAT({}, '%%') ESCAPE '\'",
  103. 'endswith': r"LIKE CONCAT('%%', {}) ESCAPE '\'",
  104. 'iendswith': r"LIKE CONCAT('%%', {}) ESCAPE '\'",
  105. }
  106. # This dictionary maps Field objects to their associated Server Server column
  107. # types, as strings. Column-type strings can contain format strings; they'll
  108. # be interpolated against the values of Field.__dict__.
  109. data_types = {
  110. 'AutoField': 'int IDENTITY (1, 1)',
  111. 'BigAutoField': 'bigint IDENTITY (1, 1)',
  112. 'BigIntegerField': 'bigint',
  113. 'BinaryField': 'varbinary(max)',
  114. 'BooleanField': 'bit',
  115. 'CharField': 'nvarchar(%(max_length)s)',
  116. 'CommaSeparatedIntegerField': 'nvarchar(%(max_length)s)',
  117. 'DateField': 'date',
  118. 'DateTimeField': 'datetime2',
  119. 'DateTimeOffsetField': 'datetimeoffset',
  120. 'DecimalField': 'decimal(%(max_digits)s, %(decimal_places)s)',
  121. 'DurationField': 'bigint',
  122. 'FileField': 'nvarchar(%(max_length)s)',
  123. 'FilePathField': 'nvarchar(%(max_length)s)',
  124. 'FloatField': 'double precision',
  125. 'GenericIPAddressField': 'nvarchar(39)',
  126. 'IntegerField': 'int',
  127. 'IPAddressField': 'nvarchar(15)',
  128. 'LegacyDateField': 'datetime',
  129. 'LegacyDateTimeField': 'datetime',
  130. 'LegacyTimeField': 'time',
  131. 'NewDateField': 'date',
  132. 'NewDateTimeField': 'datetime2',
  133. 'NewTimeField': 'time',
  134. 'NullBooleanField': 'bit',
  135. 'OneToOneField': 'int',
  136. 'PositiveIntegerField': 'int',
  137. 'PositiveSmallIntegerField': 'smallint',
  138. 'SlugField': 'nvarchar(%(max_length)s)',
  139. 'SmallIntegerField': 'smallint',
  140. 'TextField': 'nvarchar(max)',
  141. 'TimeField': 'time',
  142. 'URLField': 'nvarchar(%(max_length)s)',
  143. 'UUIDField': 'uniqueidentifier',
  144. }
  145. data_type_check_constraints = {
  146. 'PositiveIntegerField': '%(qn_column)s >= 0',
  147. 'PositiveSmallIntegerField': '%(qn_column)s >= 0',
  148. }
  149. def __init__(self, *args, **kwargs):
  150. self.use_transactions = kwargs.pop('use_transactions', None)
  151. super(DatabaseWrapper, self).__init__(*args, **kwargs)
  152. try:
  153. self.command_timeout = int(self.settings_dict.get('COMMAND_TIMEOUT', 30))
  154. except ValueError:
  155. self.command_timeout = 30
  156. options = self.settings_dict.get('OPTIONS', {})
  157. try:
  158. self.cast_avg_to_float = not bool(options.get('disable_avg_cast', False))
  159. except ValueError:
  160. self.cast_avg_to_float = False
  161. if 'use_legacy_date_fields' in options:
  162. warnings.warn(
  163. "The `use_legacy_date_fields` setting is no longer supported. "
  164. "If you need to use the legacy SQL 'datetime' datatype, "
  165. "you must replace them with the provided model fields.",
  166. DeprecationWarning)
  167. self.features = DatabaseFeatures(self)
  168. self.ops = DatabaseOperations(self)
  169. self.client = BaseDatabaseClient(self)
  170. self.creation = DatabaseCreation(self)
  171. self.introspection = DatabaseIntrospection(self)
  172. self.validation = BaseDatabaseValidation(self)
  173. def get_connection_params(self):
  174. """Returns a dict of parameters suitable for get_new_connection."""
  175. settings_dict = self.settings_dict.copy()
  176. if settings_dict['NAME'] == '':
  177. from django.core.exceptions import ImproperlyConfigured
  178. raise ImproperlyConfigured(
  179. "settings.DATABASES is improperly configured. "
  180. "Please supply the NAME value.")
  181. if not settings_dict['NAME']:
  182. # if _nodb_connection, connect to master
  183. settings_dict['NAME'] = 'master'
  184. autocommit = settings_dict.get('OPTIONS', {}).get('autocommit', False)
  185. return {
  186. 'connection_string': make_connection_string(settings_dict),
  187. 'timeout': self.command_timeout,
  188. 'use_transactions': not autocommit,
  189. }
  190. def get_new_connection(self, conn_params):
  191. """Opens a connection to the database."""
  192. self.__connection_string = conn_params.get('connection_string', '')
  193. conn = Database.connect(**conn_params)
  194. return conn
  195. def init_connection_state(self):
  196. """Initializes the database connection settings."""
  197. # if 'mars connection=true' in self.__connection_string.lower():
  198. # # Issue #41 - Cannot use MARS with savepoints
  199. # self.features.uses_savepoints = False
  200. # cache the properties on the connection
  201. self.connection.adoConnProperties = dict([(x.Name, x.Value) for x in self.connection.adoConn.Properties])
  202. try:
  203. sql_version = int(self.__get_dbms_version().split('.', 2)[0])
  204. except (IndexError, ValueError):
  205. warnings.warn(
  206. "Unable to determine MS SQL server version. Only SQL 2012 or "
  207. "newer is supported.", DeprecationWarning)
  208. else:
  209. if sql_version < VERSION_SQL2012:
  210. warnings.warn(
  211. "This version of MS SQL server is no longer tested with "
  212. "django-mssql and not officially supported/maintained.",
  213. DeprecationWarning)
  214. def create_cursor(self):
  215. """Creates a cursor. Assumes that a connection is established."""
  216. cursor = self.connection.cursor()
  217. return cursor
  218. def _set_autocommit(self, value):
  219. self.connection.set_autocommit(value)
  220. def __get_dbms_version(self, make_connection=True):
  221. """
  222. Returns the 'DBMS Version' string, or ''. If a connection to the database has not already
  223. been established, a connection will be made when `make_connection` is True.
  224. """
  225. if not self.connection and make_connection:
  226. self.connect()
  227. return self.connection.adoConnProperties.get('DBMS Version', '') if self.connection else ''
  228. def disable_constraint_checking(self):
  229. """
  230. Turn off constraint checking for every table
  231. """
  232. if self.connection:
  233. cursor = self.connection.cursor()
  234. else:
  235. cursor = self._cursor()
  236. cursor.execute('EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"')
  237. return True
  238. def enable_constraint_checking(self):
  239. """
  240. Turn on constraint checking for every table
  241. """
  242. if self.connection:
  243. cursor = self.connection.cursor()
  244. else:
  245. cursor = self._cursor()
  246. # don't check the data, just turn them on
  247. cursor.execute('EXEC sp_MSforeachtable "ALTER TABLE ? WITH NOCHECK CHECK CONSTRAINT all"')
  248. def check_constraints(self, table_names=None):
  249. """
  250. Check the table constraints.
  251. """
  252. if self.connection:
  253. cursor = self.connection.cursor()
  254. else:
  255. cursor = self._cursor()
  256. if not table_names:
  257. cursor.execute('DBCC CHECKCONSTRAINTS WITH ALL_CONSTRAINTS')
  258. if cursor.description:
  259. raise IntegrityError(cursor.fetchall())
  260. else:
  261. qn = self.ops.quote_name
  262. for name in table_names:
  263. cursor.execute('DBCC CHECKCONSTRAINTS({0}) WITH ALL_CONSTRAINTS'.format(
  264. qn(name)
  265. ))
  266. if cursor.description:
  267. raise IntegrityError(cursor.fetchall())
  268. # # MS SQL Server doesn't support explicit savepoint commits; savepoints are
  269. # # implicitly committed with the transaction.
  270. # # Ignore them.
  271. def _savepoint_commit(self, sid):
  272. if self.queries_log:
  273. self.queries_log.append({
  274. 'sql': '-- RELEASE SAVEPOINT %s -- (because assertNumQueries)' % self.ops.quote_name(sid),
  275. 'time': '0.000',
  276. })
  277. def is_usable(self):
  278. try:
  279. # Use a mssql cursor directly, bypassing Django's utilities.
  280. with self.connection.cursor() as cursor:
  281. cursor.execute("SELECT 1")
  282. except self.Database.Error:
  283. return False
  284. else:
  285. return True