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