77
88from django .core .exceptions import ImproperlyConfigured
99from django import VERSION
10- if VERSION [:3 ] < (1 ,11 ,9 ) or VERSION [:2 ] >= (2 ,0 ):
10+
11+ if VERSION [:3 ] < (2 ,0 ,1 ) or VERSION [:2 ] >= (2 ,1 ):
1112 raise ImproperlyConfigured ("Django %d.%d.%d is not supported." % VERSION [:3 ])
1213
1314try :
1415 import pyodbc as Database
1516except ImportError as e :
1617 raise ImproperlyConfigured ("Error loading pyodbc module: %s" % e )
1718
18- pyodbc_ver = tuple (map (int , Database .version .split ('.' )[:2 ]))
19- if pyodbc_ver < (3 , 0 ):
20- raise ImproperlyConfigured ("pyodbc 3.0 or newer is required; you have %s" % Database .version )
19+ from django .utils .version import get_version_tuple
20+
21+ pyodbc_ver = get_version_tuple (Database .version )
22+ if pyodbc_ver < (4 ,0 ):
23+ raise ImproperlyConfigured ("pyodbc 4.0 or newer is required; you have %s" % Database .version )
2124
2225from django .conf import settings
26+ from django .db import NotSupportedError
2327from django .db .backends .base .base import BaseDatabaseWrapper
2428from django .db .backends .base .validation import BaseDatabaseValidation
2529from django .utils .encoding import smart_str
2630from django .utils .functional import cached_property
27- from django .utils .six import binary_type , text_type
2831from django .utils .timezone import utc
2932
3033if hasattr (settings , 'DATABASE_CONNECTION_POOLING' ):
@@ -64,6 +67,7 @@ def encode_value(v):
6467
6568class DatabaseWrapper (BaseDatabaseWrapper ):
6669 vendor = 'microsoft'
70+ display_name = 'SQL Server'
6771 # This dictionary maps Field objects to their associated MS SQL column
6872 # types, as strings. Column-type strings can contain format strings; they'll
6973 # be interpolated against the values of Field.__dict__ before being output.
@@ -75,7 +79,6 @@ class DatabaseWrapper(BaseDatabaseWrapper):
7579 'BinaryField' : 'varbinary(max)' ,
7680 'BooleanField' : 'bit' ,
7781 'CharField' : 'nvarchar(%(max_length)s)' ,
78- 'CommaSeparatedIntegerField' : 'nvarchar(%(max_length)s)' ,
7982 'DateField' : 'date' ,
8083 'DateTimeField' : 'datetime2' ,
8184 'DecimalField' : 'numeric(%(max_digits)s, %(decimal_places)s)' ,
@@ -149,7 +152,6 @@ class DatabaseWrapper(BaseDatabaseWrapper):
149152 '08S02' ,
150153 )
151154 _sql_server_versions = {
152- 9 : 2005 ,
153155 10 : 2008 ,
154156 11 : 2012 ,
155157 12 : 2014 ,
@@ -171,7 +173,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
171173 )
172174
173175 def __init__ (self , * args , ** kwargs ):
174- super (DatabaseWrapper , self ).__init__ (* args , ** kwargs )
176+ super ().__init__ (* args , ** kwargs )
175177
176178 opts = self .settings_dict ["OPTIONS" ]
177179
@@ -190,9 +192,6 @@ def __init__(self, *args, **kwargs):
190192 else :
191193 self .driver_charset = opts .get ('driver_charset' , None )
192194
193- # data type compatibility to databases created by old django-pyodbc
194- self .use_legacy_datetime = opts .get ('use_legacy_datetime' , False )
195-
196195 # interval to wait for recovery from network error
197196 interval = opts .get ('connection_recovery_interval_msec' , 0.0 )
198197 self .connection_recovery_interval_msec = float (interval ) / 1000
@@ -236,17 +235,14 @@ def get_new_connection(self, conn_params):
236235 password = conn_params .get ('PASSWORD' , None )
237236 port = conn_params .get ('PORT' , None )
238237
239- default_driver = 'SQL Server' if os .name == 'nt' else 'FreeTDS'
240238 options = conn_params .get ('OPTIONS' , {})
241- driver = options .get ('driver' , default_driver )
239+ driver = options .get ('driver' , 'ODBC Driver 13 for SQL Server' )
242240 dsn = options .get ('dsn' , None )
243241
244242 # Microsoft driver names assumed here are:
245- # * SQL Server
246- # * SQL Native Client
247243 # * SQL Server Native Client 10.0/11.0
248244 # * ODBC Driver 11/13 for SQL Server
249- ms_drivers = re .compile ('.* SQL ( Server$|( Server )? Native Client) ' )
245+ ms_drivers = re .compile ('^ODBC Driver .* for SQL Server$|^SQL Server Native Client' )
250246
251247 # available ODBC connection string keywords:
252248 # (Microsoft drivers for Windows)
@@ -284,9 +280,7 @@ def get_new_connection(self, conn_params):
284280
285281 cstr_parts ['DATABASE' ] = database
286282
287- if ms_drivers .match (driver ) and not driver == 'SQL Server' :
288- self .supports_mars = True
289- if self .supports_mars :
283+ if ms_drivers .match (driver ) and os .name == 'nt' :
290284 cstr_parts ['MARS_Connection' ] = 'yes'
291285
292286 connstr = encode_connection_string (cstr_parts )
@@ -327,37 +321,26 @@ def get_new_connection(self, conn_params):
327321
328322 def init_connection_state (self ):
329323 drv_name = self .connection .getinfo (Database .SQL_DRIVER_NAME ).upper ()
330- driver_is_freetds = drv_name .startswith ('LIBTDSODBC' )
331- driver_is_sqlsrv32 = drv_name == 'SQLSRV32.DLL'
332324
333- if driver_is_freetds :
334- self .supports_mars = False
325+ if drv_name .startswith ('LIBTDSODBC' ):
335326 try :
336327 drv_ver = self .connection .getinfo (Database .SQL_DRIVER_VER )
337- ver = tuple ( map ( int , drv_ver .split ('.' )[:2 ]) )
328+ ver = get_version_tuple ( drv_ver .split ('.' )[:2 ])
338329 if ver < (0 , 95 ):
339- # FreeTDS can't execute some sql queries like CREATE DATABASE etc.
340- # in multi-statement, so we need to commit the above SQL sentence(s)
341- # to avoid this
342- self .connection .commit ()
330+ raise ImproperlyConfigured (
331+ "FreeTDS 0.95 or newer is required." )
343332 except :
344333 # unknown driver version
345334 pass
346- elif driver_is_sqlsrv32 :
347- self .supports_mars = False
348335
349- ms_drv_names = re .compile ('^(LIB)?(SQLN?CLI |MSODBCSQL)' )
336+ ms_drv_names = re .compile ('^(LIB)?(SQLNCLI |MSODBCSQL)' )
350337
351- if driver_is_sqlsrv32 or ms_drv_names .match (drv_name ):
338+ if ms_drv_names .match (drv_name ):
352339 self .driver_charset = None
353-
354- # http://msdn.microsoft.com/en-us/library/ms131686.aspx
355- if self .supports_mars and ms_drv_names .match (drv_name ):
340+ # http://msdn.microsoft.com/en-us/library/ms131686.aspx
341+ self .supports_mars = True
356342 self .features .can_use_chunked_reads = True
357343
358- if self .sql_server_version < 2008 :
359- self .features .has_bulk_insert = False
360-
361344 settings_dict = self .settings_dict
362345 cursor = self .create_cursor ()
363346
@@ -370,17 +353,10 @@ def init_connection_state(self):
370353 cursor .execute ('SET DATEFORMAT ymd; SET DATEFIRST %s' % datefirst )
371354
372355 # http://blogs.msdn.com/b/sqlnativeclient/archive/2008/02/27/microsoft-sql-server-native-client-and-microsoft-sql-server-2008-native-client.aspx
373- try :
374- val = cursor .execute ('SELECT SYSDATETIME()' ).fetchone ()[0 ]
375- if isinstance (val , text_type ):
376- # the driver doesn't support the modern datetime types
377- self .use_legacy_datetime = True
378- except :
379- # the server doesn't support the modern datetime types
380- self .use_legacy_datetime = True
381- if self .use_legacy_datetime :
382- self ._use_legacy_datetime ()
383- self .features .supports_microsecond_precision = False
356+ val = cursor .execute ('SELECT SYSDATETIME()' ).fetchone ()[0 ]
357+ if isinstance (val , str ):
358+ raise ImproperlyConfigured (
359+ "The database driver doesn't support modern datatime types." )
384360
385361 def is_usable (self ):
386362 try :
@@ -401,7 +377,7 @@ def sql_server_version(self):
401377 ver = cursor .fetchone ()[0 ]
402378 ver = int (ver .split ('.' )[0 ])
403379 if not ver in self ._sql_server_versions :
404- raise NotImplementedError ('SQL Server v%d is not supported.' % ver )
380+ raise NotSupportedError ('SQL Server v%d is not supported.' % ver )
405381 return self ._sql_server_versions [ver ]
406382
407383 @cached_property
@@ -446,7 +422,7 @@ def _savepoint_commit(self, sid):
446422
447423 def _savepoint_rollback (self , sid ):
448424 with self .cursor () as cursor :
449- # FreeTDS v0.95 requires TRANCOUNT that is greater than 0
425+ # FreeTDS requires TRANCOUNT that is greater than 0
450426 cursor .execute ('SELECT @@TRANCOUNT' )
451427 trancount = cursor .fetchone ()[0 ]
452428 if trancount > 0 :
@@ -456,15 +432,11 @@ def _set_autocommit(self, autocommit):
456432 with self .wrap_database_errors :
457433 allowed = not autocommit
458434 if not allowed :
459- # FreeTDS v0.95 requires TRANCOUNT that is greater than 0
435+ # FreeTDS requires TRANCOUNT that is greater than 0
460436 allowed = self ._get_trancount () > 0
461437 if allowed :
462438 self .connection .autocommit = autocommit
463439
464- def _use_legacy_datetime (self ):
465- for field in ('DateField' , 'DateTimeField' , 'TimeField' ):
466- self .data_types [field ] = 'datetime'
467-
468440 def check_constraints (self , table_names = None ):
469441 self ._execute_foreach ('ALTER TABLE %s WITH CHECK CHECK CONSTRAINT ALL' ,
470442 table_names )
@@ -502,7 +474,7 @@ def close(self):
502474 self .cursor .close ()
503475
504476 def format_sql (self , sql , params ):
505- if self .driver_charset and isinstance (sql , text_type ):
477+ if self .driver_charset and isinstance (sql , str ):
506478 # FreeTDS (and other ODBC drivers?) doesn't support Unicode
507479 # yet, so we need to encode the SQL clause itself in utf-8
508480 sql = smart_str (sql , self .driver_charset )
@@ -517,15 +489,15 @@ def format_params(self, params):
517489 fp = []
518490 if params is not None :
519491 for p in params :
520- if isinstance (p , text_type ):
492+ if isinstance (p , str ):
521493 if self .driver_charset :
522494 # FreeTDS (and other ODBC drivers?) doesn't support Unicode
523495 # yet, so we need to encode parameters in utf-8
524496 fp .append (smart_str (p , self .driver_charset ))
525497 else :
526498 fp .append (p )
527499
528- elif isinstance (p , binary_type ):
500+ elif isinstance (p , bytes ):
529501 fp .append (p )
530502
531503 elif isinstance (p , type (True )):
@@ -568,24 +540,25 @@ def format_rows(self, rows):
568540 def format_row (self , row ):
569541 """
570542 Decode data coming from the database if needed and convert rows to tuples
571- (pyodbc Rows are not sliceable ).
543+ (pyodbc Rows are not hashable ).
572544 """
573545 if self .driver_charset :
574546 for i in range (len (row )):
575547 f = row [i ]
576548 # FreeTDS (and other ODBC drivers?) doesn't support Unicode
577549 # yet, so we need to decode utf-8 data coming from the DB
578- if isinstance (f , binary_type ):
550+ if isinstance (f , bytes ):
579551 row [i ] = f .decode (self .driver_charset )
580- return row
552+ return tuple ( row )
581553
582554 def fetchone (self ):
583555 row = self .cursor .fetchone ()
584556 if row is not None :
585557 row = self .format_row (row )
586558 # Any remaining rows in the current set must be discarded
587559 # before changing autocommit mode when you use FreeTDS
588- self .cursor .nextset ()
560+ if not self .connection .supports_mars :
561+ self .cursor .nextset ()
589562 return row
590563
591564 def fetchmany (self , chunk ):
0 commit comments