0001from functools import reduce
0002
0003from sqlobject import dbconnection
0004from sqlobject import classregistry
0005from sqlobject import events
0006from sqlobject import sqlbuilder
0007from sqlobject.col import StringCol, ForeignKey
0008from sqlobject.main import sqlmeta, SQLObject, SelectResults,       makeProperties, unmakeProperties, getterName, setterName
0010from sqlobject.compat import string_type
0011from . import iteration
0012
0013
0014def tablesUsedSet(obj, db):
0015    if hasattr(obj, "tablesUsedSet"):
0016        return obj.tablesUsedSet(db)
0017    elif isinstance(obj, (tuple, list, set, frozenset)):
0018        s = set()
0019        for component in obj:
0020            s.update(tablesUsedSet(component, db))
0021        return s
0022    else:
0023        return set()
0024
0025
0026class InheritableSelectResults(SelectResults):
0027    IterationClass = iteration.InheritableIteration
0028
0029    def __init__(self, sourceClass, clause, clauseTables=None,
0030                 inheritedTables=None, **ops):
0031        if clause is None or isinstance(clause, str) and clause == 'all':
0032            clause = sqlbuilder.SQLTrueClause
0033
0034        dbName = (ops.get('connection', None) or
0035                  sourceClass._connection).dbName
0036
0037        tablesSet = tablesUsedSet(clause, dbName)
0038        tablesSet.add(str(sourceClass.sqlmeta.table))
0039        orderBy = ops.get('orderBy')
0040        if inheritedTables:
0041            for tableName in inheritedTables:
0042                tablesSet.add(str(tableName))
0043        if orderBy and not isinstance(orderBy, string_type):
0044            tablesSet.update(tablesUsedSet(orderBy, dbName))
0045        # DSM: if this class has a parent, we need to link it
0046        # DSM: and be sure the parent is in the table list.
0047        # DSM: The following code is before clauseTables
0048        # DSM: because if the user uses clauseTables
0049        # DSM: (and normal string SELECT), he must know what he wants
0050        # DSM: and will do himself the relationship between classes.
0051        if not isinstance(clause, str):
0052            tableRegistry = {}
0053            allClasses = classregistry.registry(
0054                sourceClass.sqlmeta.registry).allClasses()
0055            for registryClass in allClasses:
0056                if str(registryClass.sqlmeta.table) in tablesSet:
0057                    # DSM: By default, no parents are needed for the clauses
0058                    tableRegistry[registryClass] = registryClass
0059            tableRegistryCopy = tableRegistry.copy()
0060            for childClass in tableRegistryCopy:
0061                if childClass not in tableRegistry:
0062                    continue
0063                currentClass = childClass
0064                while currentClass:
0065                    if currentClass in tableRegistryCopy:
0066                        if currentClass in tableRegistry:
0067                            # DSM: Remove this class as it is a parent one
0068                            # DSM: of a needed children
0069                            del tableRegistry[currentClass]
0070                        # DSM: Must keep the last parent needed
0071                        # DSM: (to limit the number of join needed)
0072                        tableRegistry[childClass] = currentClass
0073                    currentClass = currentClass.sqlmeta.parentClass
0074            # DSM: Table registry contains only the last children
0075            # DSM: or standalone classes
0076            parentClause = []
0077            for (currentClass, minParentClass) in tableRegistry.items():
0078                while (currentClass != minParentClass)                           and currentClass.sqlmeta.parentClass:
0080                    parentClass = currentClass.sqlmeta.parentClass
0081                    parentClause.append(currentClass.q.id == parentClass.q.id)
0082                    currentClass = parentClass
0083                    tablesSet.add(str(currentClass.sqlmeta.table))
0084            clause = reduce(sqlbuilder.AND, parentClause, clause)
0085
0086        super(InheritableSelectResults, self).__init__(
0087            sourceClass, clause, clauseTables, **ops)
0088
0089    def accumulateMany(self, *attributes, **kw):
0090        if kw.get("skipInherited"):
0091            return super(InheritableSelectResults, self).                  accumulateMany(*attributes)
0093        tables = []
0094        for func_name, attribute in attributes:
0095            if not isinstance(attribute, string_type):
0096                tables.append(attribute.tableName)
0097        clone = self.__class__(self.sourceClass, self.clause,
0098                               self.clauseTables, inheritedTables=tables,
0099                               **self.ops)
0100        return clone.accumulateMany(skipInherited=True, *attributes)
0101
0102
0103class InheritableSQLMeta(sqlmeta):
0104    @classmethod
0105    def addColumn(sqlmeta, columnDef, changeSchema=False, connection=None,
0106                  childUpdate=False):
0107        soClass = sqlmeta.soClass
0108        # DSM: Try to add parent properties to the current class
0109        # DSM: Only do this once if possible at object creation and once for
0110        # DSM: each new dynamic column to refresh the current class
0111        if sqlmeta.parentClass:
0112            for col in sqlmeta.parentClass.sqlmeta.columnList:
0113                cname = col.name
0114                if cname == 'childName':
0115                    continue
0116                if cname.endswith("ID"):
0117                    cname = cname[:-2]
0118                setattr(soClass, getterName(cname), eval(
0119                    'lambda self: self._parent.%s' % cname))
0120                if not col.immutable:
0121                    def make_setfunc(cname):
0122                        def setfunc(self, val):
0123                            if not self.sqlmeta._creating and                                  not getattr(self.sqlmeta,
0125                                           "row_update_sig_suppress", False):
0126                                self.sqlmeta.send(events.RowUpdateSignal, self,
0127                                                  {cname: val})
0128
0129                            setattr(self._parent, cname, val)
0130                        return setfunc
0131
0132                    setfunc = make_setfunc(cname)
0133                    setattr(soClass, setterName(cname), setfunc)
0134            if childUpdate:
0135                makeProperties(soClass)
0136                return
0137
0138        if columnDef:
0139            super(InheritableSQLMeta, sqlmeta).addColumn(columnDef,
0140                                                         changeSchema,
0141                                                         connection)
0142
0143        # DSM: Update each child class if needed and existing (only for new
0144        # DSM: dynamic column as no child classes exists at object creation)
0145        if columnDef and hasattr(soClass, "q"):
0146            q = getattr(soClass.q, columnDef.name, None)
0147        else:
0148            q = None
0149        for c in sqlmeta.childClasses.values():
0150            c.sqlmeta.addColumn(columnDef, connection=connection,
0151                                childUpdate=True)
0152            if q:
0153                setattr(c.q, columnDef.name, q)
0154
0155    @classmethod
0156    def delColumn(sqlmeta, column, changeSchema=False, connection=None,
0157                  childUpdate=False):
0158        if childUpdate:
0159            soClass = sqlmeta.soClass
0160            unmakeProperties(soClass)
0161            makeProperties(soClass)
0162
0163            if isinstance(column, str):
0164                name = column
0165            else:
0166                name = column.name
0167            delattr(soClass, name)
0168            delattr(soClass.q, name)
0169            return
0170
0171        super(InheritableSQLMeta, sqlmeta).delColumn(column, changeSchema,
0172                                                     connection)
0173
0174        # DSM: Update each child class if needed
0175        # DSM: and delete properties for this column
0176        for c in sqlmeta.childClasses.values():
0177            c.sqlmeta.delColumn(column, changeSchema=changeSchema,
0178                                connection=connection, childUpdate=True)
0179
0180    @classmethod
0181    def addJoin(sqlmeta, joinDef, childUpdate=False):
0182        soClass = sqlmeta.soClass
0183        # DSM: Try to add parent properties to the current class
0184        # DSM: Only do this once if possible at object creation and once for
0185        # DSM: each new dynamic join to refresh the current class
0186        if sqlmeta.parentClass:
0187            for join in sqlmeta.parentClass.sqlmeta.joins:
0188                jname = join.joinMethodName
0189                jarn = join.addRemoveName
0190                setattr(
0191                    soClass, getterName(jname),
0192                    eval('lambda self: self._parent.%s' % jname))
0193                if hasattr(join, 'remove'):
0194                    setattr(
0195                        soClass, 'remove' + jarn,
0196                        eval('lambda self,o: self._parent.remove%s(o)' % jarn))
0197                if hasattr(join, 'add'):
0198                    setattr(
0199                        soClass, 'add' + jarn,
0200                        eval('lambda self,o: self._parent.add%s(o)' % jarn))
0201            if childUpdate:
0202                makeProperties(soClass)
0203                return
0204
0205        if joinDef:
0206            super(InheritableSQLMeta, sqlmeta).addJoin(joinDef)
0207
0208        # DSM: Update each child class if needed and existing (only for new
0209        # DSM: dynamic join as no child classes exists at object creation)
0210        for c in sqlmeta.childClasses.values():
0211            c.sqlmeta.addJoin(joinDef, childUpdate=True)
0212
0213    @classmethod
0214    def delJoin(sqlmeta, joinDef, childUpdate=False):
0215        if childUpdate:
0216            soClass = sqlmeta.soClass
0217            unmakeProperties(soClass)
0218            makeProperties(soClass)
0219            return
0220
0221        super(InheritableSQLMeta, sqlmeta).delJoin(joinDef)
0222
0223        # DSM: Update each child class if needed
0224        # DSM: and delete properties for this join
0225        for c in sqlmeta.childClasses.values():
0226            c.sqlmeta.delJoin(joinDef, childUpdate=True)
0227
0228    @classmethod
0229    def getAllColumns(sqlmeta):
0230        columns = sqlmeta.columns.copy()
0231        sm = sqlmeta
0232        while sm.parentClass:
0233            columns.update(sm.parentClass.sqlmeta.columns)
0234            sm = sm.parentClass.sqlmeta
0235        return columns
0236
0237    @classmethod
0238    def getColumns(sqlmeta):
0239        columns = sqlmeta.getAllColumns()
0240        if 'childName' in columns:
0241            del columns['childName']
0242        return columns
0243
0244
0245class InheritableSQLObject(SQLObject):
0246
0247    sqlmeta = InheritableSQLMeta
0248    _inheritable = True
0249    SelectResultsClass = InheritableSelectResults
0250
0251    def set(self, **kw):
0252        if self._parent:
0253            SQLObject.set(self, _suppress_set_sig=True, **kw)
0254        else:
0255            SQLObject.set(self, **kw)
0256
0257    def __classinit__(cls, new_attrs):
0258        SQLObject.__classinit__(cls, new_attrs)
0259        # if we are a child class, add sqlbuilder fields from parents
0260        currentClass = cls.sqlmeta.parentClass
0261        while currentClass:
0262            for column in currentClass.sqlmeta.columnDefinitions.values():
0263                if column.name == 'childName':
0264                    continue
0265                if isinstance(column, ForeignKey):
0266                    continue
0267                setattr(cls.q, column.name,
0268                        getattr(currentClass.q, column.name))
0269            currentClass = currentClass.sqlmeta.parentClass
0270
0271    @classmethod
0272    def _SO_setupSqlmeta(cls, new_attrs, is_base):
0273        # Note: cannot use super(InheritableSQLObject, cls)._SO_setupSqlmeta -
0274        #       InheritableSQLObject is not defined when it's __classinit__
0275        #       is run.  Cannot use SQLObject._SO_setupSqlmeta, either:
0276        #       the method would be bound to wrong class.
0277        if cls.__name__ == "InheritableSQLObject":
0278            call_super = super(cls, cls)
0279        else:
0280            # InheritableSQLObject must be in globals yet
0281            call_super = super(InheritableSQLObject, cls)
0282        call_super._SO_setupSqlmeta(new_attrs, is_base)
0283        sqlmeta = cls.sqlmeta
0284        sqlmeta.childClasses = {}
0285        # locate parent class and register this class in it's children
0286        sqlmeta.parentClass = None
0287        for superclass in cls.__bases__:
0288            if getattr(superclass, '_inheritable', False)                       and (superclass.__name__ != 'InheritableSQLObject'):
0290                if sqlmeta.parentClass:
0291                    # already have a parent class;
0292                    # cannot inherit from more than one
0293                    raise NotImplementedError(
0294                        "Multiple inheritance is not implemented")
0295                sqlmeta.parentClass = superclass
0296                superclass.sqlmeta.childClasses[cls.__name__] = cls
0297        if sqlmeta.parentClass:
0298            # remove inherited column definitions
0299            cls.sqlmeta.columns = {}
0300            cls.sqlmeta.columnList = []
0301            cls.sqlmeta.columnDefinitions = {}
0302            # default inheritance child name
0303            if not sqlmeta.childName:
0304                sqlmeta.childName = cls.__name__
0305
0306    @classmethod
0307    def get(cls, id, connection=None, selectResults=None,
0308            childResults=None, childUpdate=False):
0309
0310        val = super(InheritableSQLObject, cls).get(id, connection,
0311                                                   selectResults)
0312
0313        # DSM: If we are updating a child, we should never return a child...
0314        if childUpdate:
0315            return val
0316        # DSM: If this class has a child, return the child
0317        if 'childName' in cls.sqlmeta.columns:
0318            childName = val.childName
0319            if childName is not None:
0320                childClass = cls.sqlmeta.childClasses[childName]
0321                # If the class has no columns (which sometimes makes sense
0322                # and may be true for non-inheritable (leaf) classes only),
0323                # shunt the query to avoid almost meaningless SQL
0324                # like "SELECT NULL FROM child WHERE id=1".
0325                # This is based on assumption that child object exists
0326                # if parent object exists.  (If it doesn't your database
0327                # is broken and that is a job for database maintenance.)
0328                if not (childResults or childClass.sqlmeta.columns):
0329                    childResults = (None,)
0330                return childClass.get(id, connection=connection,
0331                                      selectResults=childResults)
0332        # DSM: Now, we know we are alone or the last child in a family...
0333        # DSM: It's time to find our parents
0334        inst = val
0335        while inst.sqlmeta.parentClass and not inst._parent:
0336            inst._parent = inst.sqlmeta.parentClass.get(
0337                id, connection=connection, childUpdate=True)
0338            inst = inst._parent
0339        # DSM: We can now return ourself
0340        return val
0341
0342    @classmethod
0343    def _notifyFinishClassCreation(cls):
0344        sqlmeta = cls.sqlmeta
0345        # verify names of added columns
0346        if sqlmeta.parentClass:
0347            # FIXME: this does not check for grandparent column overrides
0348            parentCols = sqlmeta.parentClass.sqlmeta.columns.keys()
0349            for column in sqlmeta.columnList:
0350                if column.name == 'childName':
0351                    raise AttributeError(
0352                        "The column name 'childName' is reserved")
0353                if column.name in parentCols:
0354                    raise AttributeError(
0355                        "The column '%s' is already defined "
0356                        "in an inheritable parent" % column.name)
0357        # if this class is inheritable, add column for children distinction
0358        if cls._inheritable and (cls.__name__ != 'InheritableSQLObject'):
0359            sqlmeta.addColumn(
0360                StringCol(name='childName',
0361                          # limit string length to get VARCHAR and not CLOB
0362                          length=255, default=None))
0363        if not sqlmeta.columnList:
0364            # There are no columns - call addColumn to propagate columns
0365            # from parent classes to children
0366            sqlmeta.addColumn(None)
0367        if not sqlmeta.joins:
0368            # There are no joins - call addJoin to propagate joins
0369            # from parent classes to children
0370            sqlmeta.addJoin(None)
0371
0372    def _create(self, id, **kw):
0373
0374        # DSM: If we were called by a children class,
0375        # DSM: we must retreive the properties dictionary.
0376        # DSM: Note: we can't use the ** call paremeter directly
0377        # DSM: as we must be able to delete items from the dictionary
0378        # DSM: (and our children must know that the items were removed!)
0379        if 'kw' in kw:
0380            kw = kw['kw']
0381        # DSM: If we are the children of an inheritable class,
0382        # DSM: we must first create our parent
0383        if self.sqlmeta.parentClass:
0384            parentClass = self.sqlmeta.parentClass
0385            new_kw = {}
0386            parent_kw = {}
0387            for (name, value) in kw.items():
0388                if (name != 'childName') and hasattr(parentClass, name):
0389                    parent_kw[name] = value
0390                else:
0391                    new_kw[name] = value
0392            kw = new_kw
0393
0394            # Need to check that we have enough data to sucesfully
0395            # create the current subclass otherwise we will leave
0396            # the database in an inconsistent state.
0397            for col in self.sqlmeta.columnList:
0398                if (col._default == sqlbuilder.NoDefault) and                           (col.name not in kw) and (col.foreignName not in kw):
0400                    raise TypeError(
0401                        "%s() did not get expected keyword argument "
0402                        "%s" % (self.__class__.__name__, col.name))
0403
0404            parent_kw['childName'] = self.sqlmeta.childName
0405            self._parent = parentClass(kw=parent_kw,
0406                                       connection=self._connection)
0407
0408            id = self._parent.id
0409
0410        # TC: Create this record and catch all exceptions in order to destroy
0411        # TC: the parent if the child can not be created.
0412        try:
0413            super(InheritableSQLObject, self)._create(id, **kw)
0414        except:
0415            # If we are outside a transaction and this is a child,
0416            # destroy the parent
0417            connection = self._connection
0418            if (not isinstance(connection, dbconnection.Transaction) and
0419                    connection.autoCommit) and self.sqlmeta.parentClass:
0420                self._parent.destroySelf()
0421                # TC: Do we need to do this??
0422                self._parent = None
0423            # TC: Reraise the original exception
0424            raise
0425
0426    @classmethod
0427    def _findAlternateID(cls, name, dbName, value, connection=None):
0428        result = list(cls.selectBy(connection, **{name: value}))
0429        if not result:
0430            return result, None
0431        obj = result[0]
0432        return [obj.id], obj
0433
0434    @classmethod
0435    def select(cls, clause=None, *args, **kwargs):
0436        parentClass = cls.sqlmeta.parentClass
0437        childUpdate = kwargs.pop('childUpdate', None)
0438        # childUpdate may have one of three values:
0439        #   True:
0440        #       select was issued by parent class to create child objects.
0441        #       Execute select without modifications.
0442        #   None (default):
0443        #       select is run by application.  If this class is inheritance
0444        #       child, delegate query to the parent class to utilize
0445        #       InheritableIteration optimizations.  Selected records
0446        #       are restricted to this (child) class by adding childName
0447        #       filter to the where clause.
0448        #   False:
0449        #       select is delegated from inheritance child which is parent
0450        #       of another class.  Delegate the query to parent if possible,
0451        #       but don't add childName restriction: selected records
0452        #       will be filtered by join to the table filtered by childName.
0453        if (not childUpdate) and parentClass:
0454            if childUpdate is None:
0455                # this is the first parent in deep hierarchy
0456                addClause = parentClass.q.childName == cls.sqlmeta.childName
0457                # if the clause was one of TRUE varians, replace it
0458                if (clause is None) or (clause is sqlbuilder.SQLTrueClause)                           or (
0460                            isinstance(clause, string_type) and
0461                            (clause == 'all')):
0462                    clause = addClause
0463                else:
0464                    # patch WHERE condition:
0465                    # change ID field of this class to ID of parent class
0466                    # XXX the clause is patched in place; it would be better
0467                    #     to build a new one if we have to replace field
0468                    clsID = cls.q.id
0469                    parentID = parentClass.q.id
0470
0471                    def _get_patched(clause):
0472                        if isinstance(clause, sqlbuilder.SQLOp):
0473                            _patch_id_clause(clause)
0474                            return None
0475                        elif not isinstance(clause, sqlbuilder.Field):
0476                            return None
0477                        elif (clause.tableName == clsID.tableName)                                   and (clause.fieldName == clsID.fieldName):
0479                            return parentID
0480                        else:
0481                            return None
0482
0483                    def _patch_id_clause(clause):
0484                        if not isinstance(clause, sqlbuilder.SQLOp):
0485                            return
0486                        expr = _get_patched(clause.expr1)
0487                        if expr:
0488                            clause.expr1 = expr
0489                        expr = _get_patched(clause.expr2)
0490                        if expr:
0491                            clause.expr2 = expr
0492                    _patch_id_clause(clause)
0493                    # add childName filter
0494                    clause = sqlbuilder.AND(clause, addClause)
0495            return parentClass.select(clause, childUpdate=False,
0496                                      *args, **kwargs)
0497        else:
0498            return super(InheritableSQLObject, cls).select(
0499                clause, *args, **kwargs)
0500
0501    @classmethod
0502    def selectBy(cls, connection=None, **kw):
0503        clause = []
0504        foreignColumns = {}
0505        currentClass = cls
0506        while currentClass:
0507            foreignColumns.update(dict(
0508                [(column.foreignName, name)
0509                    for (name, column) in currentClass.sqlmeta.columns.items()
0510                    if column.foreignKey
0511                 ]))
0512            currentClass = currentClass.sqlmeta.parentClass
0513        for name, value in kw.items():
0514            if name in foreignColumns:
0515                name = foreignColumns[name]  # translate "key" to "keyID"
0516                if isinstance(value, SQLObject):
0517                    value = value.id
0518            currentClass = cls
0519            while currentClass:
0520                try:
0521                    clause.append(getattr(currentClass.q, name) == value)
0522                    break
0523                except AttributeError:
0524                    pass
0525                currentClass = currentClass.sqlmeta.parentClass
0526            else:
0527                raise AttributeError(
0528                    "'%s' instance has no attribute '%s'" % (
0529                        cls.__name__, name))
0530        if clause:
0531            clause = reduce(sqlbuilder.AND, clause)
0532        else:
0533            clause = None  # select all
0534        conn = connection or cls._connection
0535        return cls.SelectResultsClass(cls, clause, connection=conn)
0536
0537    def destroySelf(self):
0538        # DSM: If this object has parents, recursivly kill them
0539        if hasattr(self, '_parent') and self._parent:
0540            self._parent.destroySelf()
0541        super(InheritableSQLObject, self).destroySelf()
0542
0543    def _reprItems(self):
0544        items = super(InheritableSQLObject, self)._reprItems()
0545        # add parent attributes (if any)
0546        if self.sqlmeta.parentClass:
0547            items.extend(self._parent._reprItems())
0548        # filter out our special column
0549        return [item for item in items if item[0] != 'childName']
0550
0551__all__ = ['InheritableSQLObject']