Opened 10 years ago

Closed 10 years ago

Last modified 8 years ago

#225 closed defect (fixed)

MS SQL Server via ADO

Reported by: gmilas@… Owned by: adrian
Component: Database layer (models, ORM) Version:
Severity: normal Keywords:
Cc: Triage Stage: Design decision needed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: UI/UX:

Description

-I need Djago to work with Microsoft SQL Server, so I tried to make the changes and it seams stable, I was able to do the 3 tutorials and some playing around and it is ok for me.

-It uses adodbapi (see below), so users must also install that.

-One problem I had was with using LIMIT/OFFSET which I tried fixing and is ok for me, however is not 100% correct in that it uses a subquery to implement OFFSET and if the user adds GROUP BY or HAVING to his query (can he do that?) that should also be added to the subquery but I could not find a way to do it. (ORDER BY is fine)

Herer is a patch for core.meta

Index: meta.py

===================================================================
--- meta.py	(revision 321)
+++ meta.py	(working copy)
@@ -1268,12 +1268,22 @@
     order_by = ", ".join(order_by)
 
     # LIMIT and OFFSET clauses
-    if kwargs.get('limit') is not None:
-        limit_sql = " LIMIT %s " % kwargs['limit']
-        if kwargs.get('offset') is not None and kwargs['offset'] != 0:
-            limit_sql += "OFFSET %s " % kwargs['offset']
+    #gheorghe:
+    from django.conf.settings import DATABASE_ENGINE
+    limit_sql = ""
+    if DATABASE_ENGINE=="ado_mssql":
+        if kwargs.get('limit') is not None:
+            select[0] = 'TOP %s %s' % (kwargs['limit'], select[0])
+            if kwargs.get('offset') is not None and kwargs['offset'] != 0:
+                #the problem is if PK is not ID and also if user adds GROUP BY, HAVING etc (can a user do that?), 
+                #those should be added to the subquery too and they can't be 
+                #or at least I don't know how
+                where.append("id NOT IN (SELECT TOP %s id FROM %s%s)" % (kwargs['offset'], opts.db_table, (order_by and " ORDER BY " + order_by or "")))        
     else:
-        limit_sql = ""
+        if kwargs.get('limit') is not None:
+            limit_sql = " LIMIT %s " % kwargs['limit']
+            if kwargs.get('offset') is not None and kwargs['offset'] != 0:
+                limit_sql += "OFFSET %s " % kwargs['offset']
 
     return select, " FROM " + ",".join(tables) + (where and " WHERE " + " AND ".join(where) or "") + (order_by and " ORDER BY " + order_by or "") + limit_sql, params
 
@@ -1302,7 +1312,15 @@
     if field.null:
         kwargs.setdefault('where', []).append('%s.%s IS NOT NULL' % (opts.db_table, field.name))
     select, sql, params = function_get_sql_clause(opts, **kwargs)
-    sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1' % (db.get_date_trunc_sql(kind, '%s.%s' % (opts.db_table, field.name)), sql)
+    
+    #gheorghe
+    from django.conf.settings import DATABASE_ENGINE
+    if DATABASE_ENGINE=="ado_mssql":
+        datepart = db.get_date_trunc_sql(kind, '%s.%s' % (opts.db_table, field.name))
+        sql = 'SELECT %s %s GROUP BY %s ORDER BY %s' % (datepart, sql, datepart, datepart)
+    else:
+        sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1' % (db.get_date_trunc_sql(kind, '%s.%s' % (opts.db_table, field.name)), sql)
+    
     cursor = db.db.cursor()
     cursor.execute(sql, params)
     # We have to manually run typecast_timestamp(str()) on the results, because

And here is the content of the new ado_mssql.py backend:

"""
ADO MSSQL database backend for Django.
requires adodbapi 2.0.1 from http://adodbapi.sourceforge.net/
"""

from django.core.db import base
from django.core.db.dicthelpers import *
import adodbapi
try:
    import mx
except:
    mx = None
import datetime
DatabaseError = adodbapi.DatabaseError

CSTR = "PROVIDER=SQLOLEDB;DATA SOURCE=%s;UID=%s;PWD=%s;DATABASE=%s"

class Connection(adodbapi.Connection):
    def cursor(self):
        return Cursor(self)
#the connect function in adodbapi must call this Connection
adodbapi.Connection = Connection  

class Cursor(adodbapi.Cursor):
    def executeHelper(self, operation, isStoredProcedureCall, parameters=None):
        if parameters != None and "%s" in operation:
            operation = operation.replace("%s", "?")
        adodbapi.Cursor.executeHelper(self, operation, isStoredProcedureCall, parameters)
        
origCVtoP = adodbapi.convertVariantToPython
def variantToPython(variant, adType):
    if type(variant)==type(True) and adType==11: 
        return variant  #bool not 1/0
    res = origCVtoP(variant, adType)
    if not mx is None and type(res) == mx.DateTime.mxDateTime.DateTimeType:
        tv = list(res.tuple()[:7])  #if mxdate then datetime
        tv[-2] = int(tv[-2])    #DepreciationWarning -> int instead of float 
        return datetime.datetime(*tuple(tv))

    if type(res) == type(1.0) and str(res)[-2:]==".0":
        return int(res)     #if float but int then int
    return res
adodbapi.convertVariantToPython = variantToPython

class DatabaseWrapper:
    def __init__(self):
        self.connection = None
        self.queries = []

    def cursor(self):
        from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PASSWORD, DEBUG
        if self.connection is None:
            if DATABASE_NAME == '' or DATABASE_USER == '':
                from django.core.exceptions import ImproperlyConfigured
                raise ImproperlyConfigured, "You need to specify both DATABASE_NAME and DATABASE_USER in your Django settings file."
            if not DATABASE_HOST:
                DATABASE_HOST = "127.0.0.1"
            conn_string = CSTR % (DATABASE_HOST, DATABASE_USER, DATABASE_PASSWORD, DATABASE_NAME)
            
            self.connection = adodbapi.connect(conn_string)
        cursor = self.connection.cursor()
        if DEBUG:
            return base.CursorDebugWrapper(cursor, self)
        return cursor

    def commit(self):
        return self.connection.commit()

    def rollback(self):
        if self.connection:
            return self.connection.rollback()

    def close(self):
        if self.connection is not None:
            self.connection.close()
            self.connection = None

def get_last_insert_id(cursor, table_name, pk_name):
    cursor.execute("SELECT %s FROM %s WHERE %s = @@IDENTITY" % (pk_name, table_name, pk_name))
    return cursor.fetchone()[0]

# lookup_type is 'year', 'month', 'day'
def get_date_extract_sql(lookup_type, table_name):
    return "DATEPART(%s, %s)" % (lookup_type, table_name)

def get_date_trunc_sql(lookup_type, field_name):
    if lookup_type=='year':
        return "Convert(datetime, Convert(varchar, DATEPART(year, %s)) + '/01/01')" % field_name
    if lookup_type=='month':
        return "Convert(datetime, Convert(varchar, DATEPART(year, %s)) + '/' + Convert(varchar, DATEPART(month, %s)) + '/01')" % (field_name, field_name)
    if lookup_type=='day':
        return "Convert(datetime, Convert(varchar(12), %s))" % field_name
        

OPERATOR_MAPPING = {
    'exact': '=',
    'iexact': 'LIKE',
    'contains': 'LIKE',
    'icontains': 'LIKE',
    'ne': '!=',
    'gt': '>',
    'gte': '>=',
    'lt': '<',
    'lte': '<=',
    'startswith': 'LIKE',
    'endswith': 'LIKE',
    'istartswith': 'LIKE',
    'iendswith': 'LIKE',
}

DATA_TYPES = {
    'AutoField':         'int IDENTITY (1, 1)',
    'BooleanField':      'bit', 
    'CharField':         'varchar(%(maxlength)s)',
    'CommaSeparatedIntegerField': 'varchar(%(maxlength)s)',
    'DateField':         'smalldatetime',
    'DateTimeField':     'smalldatetime',
    'EmailField':        'varchar(75)',
    'FileField':         'varchar(100)',
    'FloatField':        'numeric(%(max_digits)s, %(decimal_places)s)',
    'ImageField':        'varchar(100)',
    'IntegerField':      'int',
    'IPAddressField':    'char(15)',
    'ManyToManyField':   None,
    'NullBooleanField':  'bit',
    'OneToOneField':     'int',
    'PhoneNumberField':  'varchar(20)',
    'PositiveIntegerField': 'int CONSTRAINT [CK_int_pos_%(name)s] CHECK ([%(name)s] > 0)',
    'PositiveSmallIntegerField': 'smallint CONSTRAINT [CK_smallint_pos_%(name)s] CHECK ([%(name)s] > 0)',
    'SlugField':         'varchar(50)',
    'SmallIntegerField': 'smallint',
    'TextField':         'text',
    'TimeField':         'time',
    'URLField':          'varchar(200)',

    'USStateField':      'varchar(2)',
    'XMLField':          'text',
}

Attachments (4)

meta (2.5 KB) - added by anonymous 10 years ago.
ado_mssql.py (5.0 KB) - added by anonymous 10 years ago.
meta.2 (3.4 KB) - added by anonymous 10 years ago.
meta.3 (3.4 KB) - added by anonymous 10 years ago.

Download all attachments as: .zip

Change History (7)

Changed 10 years ago by anonymous

Changed 10 years ago by anonymous

Changed 10 years ago by anonymous

comment:1 Changed 10 years ago by anonymous

actualy the patch for the core/meta.py in the text above is not complete, use the meta2 attachment instead

comment:2 Changed 10 years ago by anonymous

in ado_mssql.py DATA_TYPES there is :
'TimeField': 'time'

and I guess it must be
'TimeField': 'timestamp'

Changed 10 years ago by anonymous

comment:3 Changed 10 years ago by adrian

  • Resolution set to fixed
  • Status changed from new to closed

(In [879]) Fixed #225 -- Added first stab at MS SQL Server support (via ADO). Thanks to gmilas@… for the patch

Note: See TracTickets for help on using tickets.
Back to Top