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