0001from sqlobject import sqlbuilder
0002from sqlobject import classregistry
0003from sqlobject.col import StringCol, ForeignKey
0004from sqlobject.main import sqlmeta, SQLObject, SelectResults,      makeProperties, unmakeProperties, getterName, setterName
0006import iteration
0007
0008try:
0009    set
0010except NameError: # Python 2.3
0011    from sets import Set, ImmutableSet
0012    set, frozenset = Set, ImmutableSet
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 sourceClass._connection).dbName
0035
0036        tablesSet = tablesUsedSet(clause, dbName)
0037        tablesSet.add(str(sourceClass.sqlmeta.table))
0038        orderBy = ops.get('orderBy')
0039        if inheritedTables:
0040            for tableName in inheritedTables:
0041                tablesSet.add(str(tableName))
0042        if orderBy and not isinstance(orderBy, basestring):
0043            tablesSet.update(tablesUsedSet(orderBy, dbName))
0044        #DSM: if this class has a parent, we need to link it
0045        #DSM: and be sure the parent is in the table list.
0046        #DSM: The following code is before clauseTables
0047        #DSM: because if the user uses clauseTables
0048        #DSM: (and normal string SELECT), he must know what he wants
0049        #DSM: and will do himself the relationship between classes.
0050        if not isinstance(clause, str):
0051            tableRegistry = {}
0052            allClasses = classregistry.registry(
0053                sourceClass.sqlmeta.registry).allClasses()
0054            for registryClass in allClasses:
0055                if str(registryClass.sqlmeta.table) in tablesSet:
0056                    #DSM: By default, no parents are needed for the clauses
0057                    tableRegistry[registryClass] = registryClass
0058            tableRegistryCopy = tableRegistry.copy()
0059            for childClass in tableRegistryCopy:
0060                if childClass not in tableRegistry:
0061                    continue
0062                currentClass = childClass
0063                while currentClass:
0064                    if tableRegistryCopy.has_key(currentClass):
0065                        if currentClass in tableRegistry:
0066                            #DSM: Remove this class as it is a parent one
0067                            #DSM: of a needed children
0068                            del tableRegistry[currentClass]
0069                        #DSM: Must keep the last parent needed
0070                        #DSM: (to limit the number of join needed)
0071                        tableRegistry[childClass] = currentClass
0072                    currentClass = currentClass.sqlmeta.parentClass
0073            #DSM: Table registry contains only the last children
0074            #DSM: or standalone classes
0075            parentClause = []
0076            for (currentClass, minParentClass) in tableRegistry.items():
0077                while (currentClass != minParentClass)                   and currentClass.sqlmeta.parentClass:
0079                    parentClass = currentClass.sqlmeta.parentClass
0080                    parentClause.append(currentClass.q.id == parentClass.q.id)
0081                    currentClass = parentClass
0082                    tablesSet.add(str(currentClass.sqlmeta.table))
0083            clause = reduce(sqlbuilder.AND, parentClause, clause)
0084
0085        super(InheritableSelectResults, self).__init__(sourceClass,
0086            clause, clauseTables, **ops)
0087
0088    def accumulateMany(self, *attributes, **kw):
0089        if kw.get("skipInherited"):
0090            return super(InheritableSelectResults, self).accumulateMany(*attributes)
0091        tables = []
0092        for func_name, attribute in attributes:
0093           if not isinstance(attribute, basestring):
0094                tables.append(attribute.tableName)
0095        clone = self.__class__(self.sourceClass, self.clause,
0096                          self.clauseTables, inheritedTables=tables, **self.ops)
0097        return clone.accumulateMany(skipInherited=True, *attributes)
0098
0099class InheritableSQLMeta(sqlmeta):
0100    def addColumn(sqlmeta, columnDef, changeSchema=False, connection=None, childUpdate=False):
0101        soClass = sqlmeta.soClass
0102        #DSM: Try to add parent properties to the current class
0103        #DSM: Only do this once if possible at object creation and once for
0104        #DSM: each new dynamic column to refresh the current class
0105        if sqlmeta.parentClass:
0106            for col in sqlmeta.parentClass.sqlmeta.columnList:
0107                cname = col.name
0108                if cname == 'childName': continue
0109                if cname.endswith("ID"): cname = cname[:-2]
0110                setattr(soClass, getterName(cname), eval(
0111                    'lambda self: self._parent.%s' % cname))
0112                if not col.immutable:
0113                    setattr(soClass, setterName(cname), eval(
0114                        'lambda self, val: setattr(self._parent, %s, val)'
0115                        % repr(cname)))
0116            if childUpdate:
0117                makeProperties(soClass)
0118                return
0119
0120        if columnDef:
0121            super(InheritableSQLMeta, sqlmeta).addColumn(columnDef, changeSchema, connection)
0122
0123        #DSM: Update each child class if needed and existing (only for new
0124        #DSM: dynamic column as no child classes exists at object creation)
0125        if columnDef and hasattr(soClass, "q"):
0126            q = getattr(soClass.q, columnDef.name, None)
0127        else:
0128            q = None
0129        for c in sqlmeta.childClasses.values():
0130            c.sqlmeta.addColumn(columnDef, connection=connection, childUpdate=True)
0131            if q: setattr(c.q, columnDef.name, q)
0132
0133    addColumn = classmethod(addColumn)
0134
0135    def delColumn(sqlmeta, column, changeSchema=False, connection=None, childUpdate=False):
0136        if childUpdate:
0137            soClass = sqlmeta.soClass
0138            unmakeProperties(soClass)
0139            makeProperties(soClass)
0140
0141            if isinstance(column, str):
0142                name = column
0143            else:
0144                name = column.name
0145            delattr(soClass, name)
0146            delattr(soClass.q, name)
0147            return
0148
0149        super(InheritableSQLMeta, sqlmeta).delColumn(column, changeSchema, connection)
0150
0151        #DSM: Update each child class if needed
0152        #DSM: and delete properties for this column
0153        for c in sqlmeta.childClasses.values():
0154            c.sqlmeta.delColumn(column, changeSchema=changeSchema,
0155                connection=connection, childUpdate=True)
0156
0157    delColumn = classmethod(delColumn)
0158
0159    def addJoin(sqlmeta, joinDef, childUpdate=False):
0160        soClass = sqlmeta.soClass
0161        #DSM: Try to add parent properties to the current class
0162        #DSM: Only do this once if possible at object creation and once for
0163        #DSM: each new dynamic join to refresh the current class
0164        if sqlmeta.parentClass:
0165            for join in sqlmeta.parentClass.sqlmeta.joins:
0166                jname = join.joinMethodName
0167                jarn  = join.addRemoveName
0168                setattr(soClass, getterName(jname),
0169                    eval('lambda self: self._parent.%s' % jname))
0170                if hasattr(join, 'remove'):
0171                    setattr(soClass, 'remove' + jarn,
0172                        eval('lambda self,o: self._parent.remove%s(o)' % jarn))
0173                if hasattr(join, 'add'):
0174                    setattr(soClass, 'add' + jarn,
0175                        eval('lambda self,o: self._parent.add%s(o)' % jarn))
0176            if childUpdate:
0177                makeProperties(soClass)
0178                return
0179
0180        if joinDef:
0181            super(InheritableSQLMeta, sqlmeta).addJoin(joinDef)
0182
0183        #DSM: Update each child class if needed and existing (only for new
0184        #DSM: dynamic join as no child classes exists at object creation)
0185        for c in sqlmeta.childClasses.values():
0186            c.sqlmeta.addJoin(joinDef, childUpdate=True)
0187
0188    addJoin = classmethod(addJoin)
0189
0190    def delJoin(sqlmeta, joinDef, childUpdate=False):
0191        if childUpdate:
0192            soClass = sqlmeta.soClass
0193            unmakeProperties(soClass)
0194            makeProperties(soClass)
0195            return
0196
0197        super(InheritableSQLMeta, sqlmeta).delJoin(joinDef)
0198
0199        #DSM: Update each child class if needed
0200        #DSM: and delete properties for this join
0201        for c in sqlmeta.childClasses.values():
0202            c.sqlmeta.delJoin(joinDef, childUpdate=True)
0203
0204    delJoin = classmethod(delJoin)
0205
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    def getColumns(sqlmeta):
0215        columns = sqlmeta.getAllColumns()
0216        if columns.has_key('childName'):
0217            del columns['childName']
0218        return columns
0219
0220
0221class InheritableSQLObject(SQLObject):
0222
0223    sqlmeta = InheritableSQLMeta
0224    _inheritable = True
0225    SelectResultsClass = InheritableSelectResults
0226
0227    def __classinit__(cls, new_attrs):
0228        SQLObject.__classinit__(cls, new_attrs)
0229        # if we are a child class, add sqlbuilder fields from parents
0230        currentClass = cls.sqlmeta.parentClass
0231        while currentClass:
0232            for column in currentClass.sqlmeta.columnDefinitions.values():
0233                if column.name == 'childName':
0234                    continue
0235                if isinstance(column, ForeignKey):
0236                    continue
0237                setattr(cls.q, column.name,
0238                    getattr(currentClass.q, column.name))
0239            currentClass = currentClass.sqlmeta.parentClass
0240
0241    # @classmethod
0242    def _SO_setupSqlmeta(cls, new_attrs, is_base):
0243        # Note: cannot use super(InheritableSQLObject, cls)._SO_setupSqlmeta -
0244        #       InheritableSQLObject is not defined when it's __classinit__
0245        #       is run.  Cannot use SQLObject._SO_setupSqlmeta, either:
0246        #       the method would be bound to wrong class.
0247        if cls.__name__ == "InheritableSQLObject":
0248            call_super = super(cls, cls)
0249        else:
0250            # InheritableSQLObject must be in globals yet
0251            call_super = super(InheritableSQLObject, cls)
0252        call_super._SO_setupSqlmeta(new_attrs, is_base)
0253        sqlmeta = cls.sqlmeta
0254        sqlmeta.childClasses = {}
0255        # locate parent class and register this class in it's children
0256        sqlmeta.parentClass = None
0257        for superclass in cls.__bases__:
0258            if getattr(superclass, '_inheritable', False)               and (superclass.__name__ != 'InheritableSQLObject'):
0260                if sqlmeta.parentClass:
0261                    # already have a parent class;
0262                    # cannot inherit from more than one
0263                    raise NotImplementedError(
0264                        "Multiple inheritance is not implemented")
0265                sqlmeta.parentClass = superclass
0266                superclass.sqlmeta.childClasses[cls.__name__] = cls
0267        if sqlmeta.parentClass:
0268            # remove inherited column definitions
0269            cls.sqlmeta.columns = {}
0270            cls.sqlmeta.columnList = []
0271            cls.sqlmeta.columnDefinitions = {}
0272            # default inheritance child name
0273            if not sqlmeta.childName:
0274                sqlmeta.childName = cls.__name__
0275
0276    _SO_setupSqlmeta = classmethod(_SO_setupSqlmeta)
0277
0278    def get(cls, id, connection=None, selectResults=None, childResults=None, childUpdate=False):
0279
0280        val = super(InheritableSQLObject, cls).get(id, connection, selectResults)
0281
0282        #DSM: If we are updating a child, we should never return a child...
0283        if childUpdate: return val
0284        #DSM: If this class has a child, return the child
0285        if 'childName' in cls.sqlmeta.columns:
0286            childName = val.childName
0287            if childName is not None:
0288                childClass = cls.sqlmeta.childClasses[childName]
0289                # If the class has no columns (which sometimes makes sense
0290                # and may be true for non-inheritable (leaf) classes only),
0291                # shunt the query to avoid almost meaningless SQL
0292                # like "SELECT NULL FROM child WHERE id=1".
0293                # This is based on assumption that child object exists
0294                # if parent object exists.  (If it doesn't your database
0295                # is broken and that is a job for database maintenance.)
0296                if not (childResults or childClass.sqlmeta.columns):
0297                    childResults = (None,)
0298                return childClass.get(id, connection=connection,
0299                    selectResults=childResults)
0300        #DSM: Now, we know we are alone or the last child in a family...
0301        #DSM: It's time to find our parents
0302        inst = val
0303        while inst.sqlmeta.parentClass and not inst._parent:
0304            inst._parent = inst.sqlmeta.parentClass.get(id,
0305                connection=connection, childUpdate=True)
0306            inst = inst._parent
0307        #DSM: We can now return ourself
0308        return val
0309
0310    get = classmethod(get)
0311
0312    def _notifyFinishClassCreation(cls):
0313        sqlmeta = cls.sqlmeta
0314        # verify names of added columns
0315        if sqlmeta.parentClass:
0316            # FIXME: this does not check for grandparent column overrides
0317            parentCols = sqlmeta.parentClass.sqlmeta.columns.keys()
0318            for column in sqlmeta.columnList:
0319                if column.name == 'childName':
0320                    raise AttributeError(
0321                        "The column name 'childName' is reserved")
0322                if column.name in parentCols:
0323                    raise AttributeError("The column '%s' is"
0324                        " already defined in an inheritable parent"
0325                        % column.name)
0326        # if this class is inheritable, add column for children distinction
0327        if cls._inheritable and (cls.__name__ != 'InheritableSQLObject'):
0328            sqlmeta.addColumn(StringCol(name='childName',
0329                # limit string length to get VARCHAR and not CLOB
0330                length=255, default=None))
0331        if not sqlmeta.columnList:
0332            # There are no columns - call addColumn to propagate columns
0333            # from parent classes to children
0334            sqlmeta.addColumn(None)
0335        if not sqlmeta.joins:
0336            # There are no joins - call addJoin to propagate joins
0337            # from parent classes to children
0338            sqlmeta.addJoin(None)
0339    _notifyFinishClassCreation = classmethod(_notifyFinishClassCreation)
0340
0341    def _create(self, id, **kw):
0342
0343        #DSM: If we were called by a children class,
0344        #DSM: we must retreive the properties dictionary.
0345        #DSM: Note: we can't use the ** call paremeter directly
0346        #DSM: as we must be able to delete items from the dictionary
0347        #DSM: (and our children must know that the items were removed!)
0348        if kw.has_key('kw'):
0349            kw = kw['kw']
0350        #DSM: If we are the children of an inheritable class,
0351        #DSM: we must first create our parent
0352        if self.sqlmeta.parentClass:
0353            parentClass = self.sqlmeta.parentClass
0354            new_kw = {}
0355            parent_kw = {}
0356            for (name, value) in kw.items():
0357                if (name != 'childName') and hasattr(parentClass, name):
0358                    parent_kw[name] = value
0359                else:
0360                    new_kw[name] = value
0361            kw = new_kw
0362
0363            # Need to check that we have enough data to sucesfully
0364            # create the current subclass otherwise we will leave
0365            # the database in an inconsistent state.
0366            for col in self.sqlmeta.columnList:
0367                if (col._default == sqlbuilder.NoDefault) and                           (col.name not in kw) and (col.foreignName not in kw):
0369                    raise TypeError, "%s() did not get expected keyword argument %s" % (self.__class__.__name__, col.name)
0370
0371            parent_kw['childName'] = self.sqlmeta.childName
0372            self._parent = parentClass(kw=parent_kw,
0373                connection=self._connection)
0374
0375            id = self._parent.id
0376
0377        super(InheritableSQLObject, self)._create(id, **kw)
0378
0379    def _findAlternateID(cls, name, dbName, value, connection=None):
0380        result = list(cls.selectBy(connection, **{name: value}))
0381        if not result:
0382            return result, None
0383        obj = result[0]
0384        return [obj.id], obj
0385    _findAlternateID = classmethod(_findAlternateID)
0386
0387    def select(cls, clause=None, *args, **kwargs):
0388        parentClass = cls.sqlmeta.parentClass
0389        childUpdate = kwargs.pop('childUpdate', None)
0390        # childUpdate may have one of three values:
0391        #   True:
0392        #       select was issued by parent class to create child objects.
0393        #       Execute select without modifications.
0394        #   None (default):
0395        #       select is run by application.  If this class is inheritance
0396        #       child, delegate query to the parent class to utilize
0397        #       InheritableIteration optimizations.  Selected records
0398        #       are restricted to this (child) class by adding childName
0399        #       filter to the where clause.
0400        #   False:
0401        #       select is delegated from inheritance child which is parent
0402        #       of another class.  Delegate the query to parent if possible,
0403        #       but don't add childName restriction: selected records
0404        #       will be filtered by join to the table filtered by childName.
0405        if (not childUpdate) and parentClass:
0406            if childUpdate is None:
0407                # this is the first&