0001"""
0002Contributed by Edigram SAS,Paris  France
0003Tel:01 44 77 94 00
0004Ahmed MOHAMED ALI <ahmedmoali@yahoo.com> 27 April 2004
0005This program is distributed in the hope that it will be useful,
0006but WITHOUT ANY WARRANTY; without even the implied warranty of
0007MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0008GNU General Public License for more details.
0009
0010connection creation sample 
0011__connection__ = DBConnection.maxdbConnection(
0012    host=hostname, database=dbname,
0013    user=user_name, password=user_password,autoCommit=1,debug=1)
0014"""
0015from sqlobject.dbconnection import DBAPI
0016from sqlobject import col
0017dbapi = None
0018
0019
0020
0021class maxdbException(Exception):
0022
0023    def __init__(self, value):
0024        self.value = value
0025
0026    def __str__(self):
0027        return repr(self.value)
0028
0029class LowerBoundOfSliceIsNotSupported(maxdbException):
0030    def __init__(self, value):
0031        maxdbException.__init__(self, '')
0032
0033class IncorrectIDStyleError(maxdbException) :
0034    def __init__(self,value):
0035        maxdbException.__init__(
0036            self,
0037            'This primary key name is not in the expected style, '
0038            'please rename the column to %r or switch to another style'
0039            % value)
0040
0041class StyleMismatchError(maxdbException):
0042    def __init__(self, value):
0043        maxdbException.__init__(
0044            self,
0045            'The name %r is only permitted for primary key, change the '
0046            'column name or switch to another style' % value)
0047
0048class PrimaryKeyNotFounded(maxdbException):
0049    def __init__(self, value):
0050        maxdbException.__init__(
0051            self,
0052            "No primary key was defined on table %r" % value)
0053
0054SAPDBMAX_ID_LENGTH=32
0055
0056class MaxdbConnection(DBAPI):
0057
0058    supportTransactions = True
0059    dbName = 'maxdb'
0060    schemes = [dbName]
0061
0062    def __init__ (self, user, password, database,
0063                  host='', autoCommit=1, sqlmode='internal',
0064                  isolation=None, timeout=None, **kw):
0065        global dbapi
0066        if dbapi is None:
0067            from sapdb import dbapi
0068        self.module = dbapi
0069        self.autoCommit = autoCommit
0070        self.user      = user
0071        self.password  = password
0072        self.database  = database
0073        self.host      = host
0074        self.sqlmode   = sqlmode
0075        self.isolation = isolation
0076        self.timeout   = timeout
0077
0078        DBAPI.__init__(self, **kw)
0079
0080    def connectionFromURI(cls, uri):
0081        auth, password, host, port, path, args = cls._parseURI(uri)
0082        path = path.replace('/', os.path.sep)
0083        return cls(host, db=path, user=auth, passwd=password, **args)
0084    connectionFromURI = classmethod(connectionFromURI)
0085
0086    def _getConfigParams(self,sqlmode,auto):
0087        autocommit='off'
0088        if auto:
0089            autocommit='on'
0090        opt = {}
0091        opt["autocommit"] = autocommit
0092        opt["sqlmode"] = sqlmode
0093        if self.isolation:
0094            opt["isolation"]=self.isolation
0095        if self.timeout :
0096            opt["timeout"]=self.timeout
0097        return opt
0098
0099    def _setAutoCommit(self, conn, auto):
0100        conn.close()
0101        conn.__init__(self.user, self.password, self.database,
0102                      self.host,
0103                      **self._getConfigParams(self.sqlmode,auto))
0104
0105    def createSequenceName(self,table):
0106        """
0107        sequence name are builded with the concatenation of the table
0108        name with '_SEQ' word we truncate the name of the
0109        sequence_name because sapdb identifier cannot exceed 32
0110        characters so that the name of the sequence does not exceed 32
0111        characters
0112        """
0113        return '%s_SEQ'%(table[:SAPDBMAX_ID_LENGTH -4])
0114
0115    def makeConnection(self):
0116        conn = dbapi.Connection(
0117            self.user, self.password, self.database, self.host,
0118            **self._getConfigParams(self.sqlmode,self.autoCommit))
0119        return conn
0120
0121    def _queryInsertID(self, conn, soInstance, id, names, values):
0122        table = soInstance.sqlmeta.table
0123        idName = soInstance.sqlmeta.idName
0124        c = conn.cursor()
0125        if id is None:
0126            c.execute('SELECT %s.NEXTVAL FROM DUAL' % (self.createSequenceName(table)))
0127            id = c.fetchone()[0]
0128        names = [idName] + names
0129        values = [id] + values
0130        q = self._insertSQL(table, names, values)
0131        if self.debug:
0132            self.printDebug(conn, q, 'QueryIns')
0133        c.execute(q)
0134        if self.debugOutput:
0135            self.printDebug(conn, id, 'QueryIns', 'result')
0136        return id
0137
0138    def sqlAddLimit(self,query,limit):
0139        sql = query
0140        sql = sql.replace("SELECT","SELECT ROWNO, ")
0141        if sql.find('WHERE') != -1:
0142            sql = sql + ' AND ' + limit
0143        else:
0144            sql = sql + 'WHERE ' + limit
0145        return sql
0146
0147    def _queryAddLimitOffset(self, query, start, end):
0148        if start:
0149            raise LowerBoundOfSliceIsNotSupported
0150        limit = ' ROWNO   <= %d ' % (end)
0151        return self.sqlAddLimit(query,limit)
0152
0153
0154    def createTable(self, soClass):
0155        #we create the table in a transaction because the addition of the
0156        #table and the sequence must be atomic 
0157
0158        #i tried to use the transaction class but i get a recursion limit error    
0159        #t=self.transaction()
0160        # t.query('CREATE TABLE %s (\n%s\n)' % \
0161        #            (soClass.sqlmeta.table, self.createColumns(soClass)))
0162        
0163        # t.query("CREATE SEQUENCE %s" % self.createSequenceName(soClass.sqlmeta.table))
0164        # t.commit()
0165        #so use transaction when the problem will be solved
0166        self.query('CREATE TABLE %s (\n%s\n)' %                       (soClass.sqlmeta.table, self.createColumns(soClass)))
0168        self.query("CREATE SEQUENCE %s"
0169                   % self.createSequenceName(soClass.sqlmeta.table))
0170
0171    def createColumn(self, soClass, col):
0172        return col.maxdbCreateSQL()
0173
0174    def createIDColumn(self, soClass):
0175        return '%s INT PRIMARY KEY' % soClass.sqlmeta.idName
0176
0177    def createIndexSQL(self, soClass, index):
0178        return index.maxdbCreateIndexSQL(soClass)
0179
0180    def dropTable(self, tableName,cascade=False):
0181        #we drop the table in a transaction because the removal of the
0182        #table and the sequence must be atomic 
0183        #i tried to use the transaction class but i get a recursion limit error
0184        # try:
0185        #     t=self.transaction()
0186        #     t.query("DROP TABLE %s" % tableName)
0187        #     t.query("DROP SEQUENCE %s" % self.createSequenceName(tableName))
0188        #     t.commit()
0189        # except:
0190        #     t.rollback()
0191        #so use transaction when the problem will be solved
0192        self.query("DROP TABLE %s" % tableName)
0193        self.query("DROP SEQUENCE %s" % self.createSequenceName(tableName))
0194
0195    def joinSQLType(self, join):
0196        return 'INT NOT NULL'
0197
0198    def tableExists(self, tableName):
0199        for (table,) in self.queryAll("SELECT OBJECT_NAME FROM ALL_OBJECTS WHERE OBJECT_TYPE='TABLE'"):
0200            if table.lower() == tableName.lower():
0201                return True
0202        return False
0203
0204    def addColumn(self, tableName, column):
0205        self.query('ALTER TABLE %s ADD %s' %
0206                   (tableName,
0207                    column.maxdbCreateSQL()))
0208
0209    def delColumn(self, tableName, column):
0210        self.query('ALTER TABLE %s DROP COLUMN %s' %
0211                   (tableName,
0212                    column.dbName))
0213
0214    GET_COLUMNS = """
0215    SELECT COLUMN_NAME, NULLABLE, DATA_DEFAULT, DATA_TYPE,
0216           DATA_LENGTH, DATA_SCALE
0217    FROM USER_TAB_COLUMNS WHERE TABLE_NAME=UPPER('%s')"""
0218
0219    GET_PK_AND_FK = """
0220    SELECT constraint_cols.column_name, constraints.constraint_type,
0221           refname,reftablename
0222    FROM user_cons_columns constraint_cols 
0223    INNER JOIN user_constraints constraints
0224    ON constraint_cols.constraint_name = constraints.constraint_name
0225    LEFT OUTER JOIN show_foreign_key fk
0226    ON constraint_cols.column_name = fk.columnname
0227    WHERE constraints.table_name =UPPER('%s')"""
0228
0229    def columnsFromSchema(self, tableName, soClass):
0230        colData = self.queryAll(self.GET_COLUMNS
0231                                % tableName)
0232
0233        results = []
0234        keymap = {}
0235        pkmap={}
0236        fkData = self.queryAll(self.GET_PK_AND_FK% tableName)
0237        for col, cons_type, refcol, reftable in fkData:
0238            col_name= col.lower()
0239            pkmap[col_name]=False
0240            if cons_type == 'R':
0241                keymap[col_name]=reftable.lower()
0242
0243            elif cons_type == 'P':
0244                pkmap[col_name]=True
0245
0246        if len(pkmap) == 0:
0247            raise PrimaryKeyNotFounded, tableName
0248
0249        for (field, nullAllowed, default, data_type, data_len,
0250             data_scale) in colData:
0251            # id is defined as primary key --> ok
0252            # We let sqlobject raise error if the 'id' is used for another column
0253            field_name = field.lower()
0254            if field_name == 'id' and  pkmap[field_name]:
0255                continue
0256
0257            colClass, kw = self.guessClass(data_type,data_len,data_scale)
0258            kw['name'] = field_name
0259
0260            if nullAllowed == 'Y' :
0261                nullAllowed=False
0262            else:
0263                nullAllowed=True
0264
0265            kw['notNone'] = nullAllowed
0266            if default is not None:
0267                kw['default'] = default
0268
0269            if keymap.has_key(field_name):
0270                kw['foreignKey'] = keymap[field_name]
0271
0272            results.append(colClass(**kw))
0273
0274        return results
0275
0276    _numericTypes=['INTEGER', 'INT','SMALLINT']
0277    _dateTypes=['DATE','TIME','TIMESTAMP']
0278
0279    def guessClass(self, t, flength, fscale=None):
0280        """
0281        An internal method that tries to figure out what Col subclass
0282        is appropriate given whatever introspective information is
0283        available -- both very database-specific.
0284        """
0285        if t in self._numericTypes:
0286            return col.IntCol, {}
0287        # The type returned by the sapdb library for LONG is
0288        # SapDB_LongReader To get the data call the read member with
0289        # desired size (default =-1 means get all)
0290
0291        elif t.find('LONG') != -1:
0292            return col.StringCol, {'length': flength,
0293                                   'varchar': False}
0294        elif t in self._dateTypes:
0295            return col.DateTimeCol, {}
0296        elif t == 'FIXED':
0297            return CurrencyCol,{'size':flength,
0298                                'precision':fscale}
0299        else:
0300            return col.Col, {}