# -*- coding: utf8 -*-
'''
SQL SELECT MODULE
***********************
***  SQL SELECT MODULE
***********************
$RCSfile: select.py,v $
$Revision: 905 $
$Author: markus $
$Date: 2008-04-30 11:57:10 +0200 (Mi, 30. Apr 2008) $

Functions used to query the database and create a result set on which the logic module works.
'''


from types                                import *
from string                            import replace, join
from biocase.wrapper.errorclasses        import *
from biocase.tools.various_functions   import unique
from biocase.wrapper.sql.operators        import logicalOperator, comparisonOperator, getWhereObjTables, compressOperator
from biocase.wrapper.sql.base           import sqlBase
from biocase.wrapper.typeconverter       import DBtransformerClass, XMLtransformerClass


import logging
log=logging.getLogger("pywrapper.sql")



# ---------------------------------------------------------------------------        
class sqlSelect(sqlBase):
    '''The query class containing ANSI standard SQL and routines useful for all DBMS. They may be overridden by inheriting classes spezialized on one DBMS.'''
    
    def __init__(self, psfObj):
        # store reference to psf object
        self.psfObj = psfObj
        # create a type-transformer instance
        self.trans = DBtransformerClass()
        
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def select(self, selectList, whereObj, limit=None, orderByList=[], selectReplacementString=None):
        '''Creates a SQL select statement that optionally limits the ordered result.
        All parameters (values) of this string are replaced by an "?" and passed seperately as a list.
        This form is common for ODBC / JDBC drivers.
        Takes a list of DBAttribute objects as the selectList and orderByList part
        and a whereObj(COP or LOP) as the where clause.
        Limit should be a number and orderByList.
        '''
        paras = []
        sql = ''
        # remove Nones from select list
        selectList = [s for s in selectList if s is not None]
        orderByList = [s for s in orderByList if s is not None]
        
        try:
            #
            # build SELECT clause
            if selectReplacementString == None:
                parts = []
                for sel in selectList:
                    parts.append(self.joinDBAttrObj(sel))
                sql_select = join(parts, ', ')
            else:
                sql_select = selectReplacementString
            #
            # discover all used tables for FROM clause
            usedTables = []
            for DBAttrObj in selectList:
                usedTables.append(DBAttrObj.table)
            usedTables = unique(usedTables + getWhereObjTables(whereObj))
            #
            # build FROM clause. additional objects for where clause may origin here.
            (sql_from, addWhereObj) = self.fromClause(usedTables)
            # check if additional where objects were retrieved:
            if addWhereObj <> None:
                whereObj = logicalOperator('and',[addWhereObj, whereObj])
            #
            # build WHERE part
            whereObj = compressOperator(whereObj)
            sql_where, paras = self.whereClause(whereObj, limit)
            #
            # build ORDER BY part
            parts = []
            for dbao in orderByList:
                parts.append(self.joinDBAttrObj(dbao))
            orderby = join(parts, ', ')
            # test limit value
            if limit > 0:
                # use select-limit
                # build complete statement
                if sql_where != None:
                    sql = self.apply_template('where', [sql_from, sql_where])
                else:
                    sql = sql_from
                    
                if len(orderby) > 1:
                    sql = self.apply_template('order-by', [sql, orderby])
                sql = self.apply_template('select-limit', [sql_select, sql, limit+1])
            else:
                # use regular select
                # build complete statement
                sql = self.apply_template('select', [sql_select, sql_from])
                if whereObj != None:
                    sql = self.apply_template('where', [sql, sql_where])
                if len(orderby) > 1:
                    sql = self.apply_template('order-by', [sql, orderby])
            # replace wildcards and transform parameters:
            for p in paras:
                #log.debug("targettype=%s, type=%s, val=%s" %(str(p.type), str(type(p.value)), str(p.value)))
                try:
                    p.value = self.trans.transformPythonVal2DB(p)
                    if self.trans.integrateParameters == 1:
                        sql = sql.replace('?',p.value,1)
                except:
                    log.debug("Error transforming a SQL parameter.")
                #log.debug("after transformation: targettype=%s, type=%s, val=%s" %(str(p.type), str(type(p.value)),str(p.value)))
            paras = tuple([p.value for p in paras])
        except:
            raise SQLgenerationError()
        if self.trans.integrateParameters == 1:
            paras = None
        return (sql, paras)

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def whereClause(self, filterObj=None, limit=0):
        # compress whereobject to reduce SQL complexity
        filterObj = compressOperator(filterObj)
        if filterObj != None:
            (sql_where, paras) = filterObj.getSQLTuple()
            #log.debug("SQL_WHERE= %s" % sql_where)
            #log.debug("PARAS= %s" % paras)
        else:
            sql_where = ''
            paras      = []
        # None is the correct value for no where clause
        if sql_where == '':
            sql_where=None
        return (sql_where, paras)

    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def fromClause(self, tableList):
        '''Returns the SQL string of the FROM clause for a given table(alias) list
        and an optional WhereObject that is added to the original whereObject in the where clause.
        Makes use of the tablegraph to produce an ordered list of tables
        and their relation - the relObj (table-alias names, whereObj).
        Always links the SQL to the root table.
        '''
        TreeJoinLists = self.CMFObject.tableTreeObj.getJoinTableListOfLists(tableList)
        # build SQL string for each Tree:
        TreeSQLList = []
        for joinList in TreeJoinLists:
            treeSQL = ''
            treeSQL += self.getTableSyntax(joinList.pop(0)[0])
            for t, relObj in joinList:
                onClause = str(relObj)
                newTable = self.getTableSyntax(t)
                treeSQL = self.apply_template('join', [treeSQL, newTable, onClause])
            TreeSQLList.append(treeSQL)
        # join those SQL trees via a FULL JOIN
        fromClause = TreeSQLList.pop(0)
        for treeSQL in TreeSQLList:
            fromClause = self.apply_template('full-join', [fromClause, treeSQL])
        return (fromClause, None)
    
    # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    def getTableSyntax(self, table):
        '''returns the table as referenced in SQL including aliases'''
        realTable = self.apply_template('table', [self.psfObj.tablegraph.alias2tablename.get(table, None)])
        alias = self.apply_template('table', [table])
        return self.apply_template('alias', [realTable, alias])



