from sqlobject import dbconnection from sqlobject import classregistry from sqlobject import events from sqlobject import sqlbuilder from sqlobject.col import StringCol, ForeignKey from sqlobject.main import sqlmeta, SQLObject, SelectResults, \ makeProperties, unmakeProperties, getterName, setterName import iteration def tablesUsedSet(obj, db): if hasattr(obj, "tablesUsedSet"): return obj.tablesUsedSet(db) elif isinstance(obj, (tuple, list, set, frozenset)): s = set() for component in obj: s.update(tablesUsedSet(component, db)) return s else: return set() class InheritableSelectResults(SelectResults): IterationClass = iteration.InheritableIteration def __init__(self, sourceClass, clause, clauseTables=None, inheritedTables=None, **ops): if clause is None or isinstance(clause, str) and clause == 'all': clause = sqlbuilder.SQLTrueClause dbName = (ops.get('connection',None) or sourceClass._connection).dbName tablesSet = tablesUsedSet(clause, dbName) tablesSet.add(str(sourceClass.sqlmeta.table)) orderBy = ops.get('orderBy') if inheritedTables: for tableName in inheritedTables: tablesSet.add(str(tableName)) if orderBy and not isinstance(orderBy, basestring): tablesSet.update(tablesUsedSet(orderBy, dbName)) #DSM: if this class has a parent, we need to link it #DSM: and be sure the parent is in the table list. #DSM: The following code is before clauseTables #DSM: because if the user uses clauseTables #DSM: (and normal string SELECT), he must know what he wants #DSM: and will do himself the relationship between classes. if not isinstance(clause, str): tableRegistry = {} allClasses = classregistry.registry( sourceClass.sqlmeta.registry).allClasses() for registryClass in allClasses: if str(registryClass.sqlmeta.table) in tablesSet: #DSM: By default, no parents are needed for the clauses tableRegistry[registryClass] = registryClass tableRegistryCopy = tableRegistry.copy() for childClass in tableRegistryCopy: if childClass not in tableRegistry: continue currentClass = childClass while currentClass: if currentClass in tableRegistryCopy: if currentClass in tableRegistry: #DSM: Remove this class as it is a parent one #DSM: of a needed children del tableRegistry[currentClass] #DSM: Must keep the last parent needed #DSM: (to limit the number of join needed) tableRegistry[childClass] = currentClass currentClass = currentClass.sqlmeta.parentClass #DSM: Table registry contains only the last children #DSM: or standalone classes parentClause = [] for (currentClass, minParentClass) in tableRegistry.items(): while (currentClass != minParentClass) \ and currentClass.sqlmeta.parentClass: parentClass = currentClass.sqlmeta.parentClass parentClause.append(currentClass.q.id == parentClass.q.id) currentClass = parentClass tablesSet.add(str(currentClass.sqlmeta.table)) clause = reduce(sqlbuilder.AND, parentClause, clause) super(InheritableSelectResults, self).__init__(sourceClass, clause, clauseTables, **ops) def accumulateMany(self, *attributes, **kw): if kw.get("skipInherited"): return super(InheritableSelectResults, self).accumulateMany(*attributes) tables = [] for func_name, attribute in attributes: if not isinstance(attribute, basestring): tables.append(attribute.tableName) clone = self.__class__(self.sourceClass, self.clause, self.clauseTables, inheritedTables=tables, **self.ops) return clone.accumulateMany(skipInherited=True, *attributes) class InheritableSQLMeta(sqlmeta): @classmethod def addColumn(sqlmeta, columnDef, changeSchema=False, connection=None, childUpdate=False): soClass = sqlmeta.soClass #DSM: Try to add parent properties to the current class #DSM: Only do this once if possible at object creation and once for #DSM: each new dynamic column to refresh the current class if sqlmeta.parentClass: for col in sqlmeta.parentClass.sqlmeta.columnList: cname = col.name if cname == 'childName': continue if cname.endswith("ID"): cname = cname[:-2] setattr(soClass, getterName(cname), eval( 'lambda self: self._parent.%s' % cname)) if not col.immutable: def make_setfunc(cname): def setfunc(self, val): if not self.sqlmeta._creating and not getattr(self.sqlmeta, "row_update_sig_suppress", False): self.sqlmeta.send(events.RowUpdateSignal, self, {cname : val}) result = setattr(self._parent, cname, val) return setfunc setfunc = make_setfunc(cname) setattr(soClass, setterName(cname), setfunc) if childUpdate: makeProperties(soClass) return if columnDef: super(InheritableSQLMeta, sqlmeta).addColumn(columnDef, changeSchema, connection) #DSM: Update each child class if needed and existing (only for new #DSM: dynamic column as no child classes exists at object creation) if columnDef and hasattr(soClass, "q"): q = getattr(soClass.q, columnDef.name, None) else: q = None for c in sqlmeta.childClasses.values(): c.sqlmeta.addColumn(columnDef, connection=connection, childUpdate=True) if q: setattr(c.q, columnDef.name, q) @classmethod def delColumn(sqlmeta, column, changeSchema=False, connection=None, childUpdate=False): if childUpdate: soClass = sqlmeta.soClass unmakeProperties(soClass) makeProperties(soClass) if isinstance(column, str): name = column else: name = column.name delattr(soClass, name) delattr(soClass.q, name) return super(InheritableSQLMeta, sqlmeta).delColumn(column, changeSchema, connection) #DSM: Update each child class if needed #DSM: and delete properties for this column for c in sqlmeta.childClasses.values(): c.sqlmeta.delColumn(column, changeSchema=changeSchema, connection=connection, childUpdate=True) @classmethod def addJoin(sqlmeta, joinDef, childUpdate=False): soClass = sqlmeta.soClass #DSM: Try to add parent properties to the current class #DSM: Only do this once if possible at object creation and once for #DSM: each new dynamic join to refresh the current class if sqlmeta.parentClass: for join in sqlmeta.parentClass.sqlmeta.joins: jname = join.joinMethodName jarn = join.addRemoveName setattr(soClass, getterName(jname), eval('lambda self: self._parent.%s' % jname)) if hasattr(join, 'remove'): setattr(soClass, 'remove' + jarn, eval('lambda self,o: self._parent.remove%s(o)' % jarn)) if hasattr(join, 'add'): setattr(soClass, 'add' + jarn, eval('lambda self,o: self._parent.add%s(o)' % jarn)) if childUpdate: makeProperties(soClass) return if joinDef: super(InheritableSQLMeta, sqlmeta).addJoin(joinDef) #DSM: Update each child class if needed and existing (only for new #DSM: dynamic join as no child classes exists at object creation) for c in sqlmeta.childClasses.values(): c.sqlmeta.addJoin(joinDef, childUpdate=True) @classmethod def delJoin(sqlmeta, joinDef, childUpdate=False): if childUpdate: soClass = sqlmeta.soClass unmakeProperties(soClass) makeProperties(soClass) return super(InheritableSQLMeta, sqlmeta).delJoin(joinDef) #DSM: Update each child class if needed #DSM: and delete properties for this join for c in sqlmeta.childClasses.values(): c.sqlmeta.delJoin(joinDef, childUpdate=True) @classmethod def getAllColumns(sqlmeta): columns = sqlmeta.columns.copy() sm = sqlmeta while sm.parentClass: columns.update(sm.parentClass.sqlmeta.columns) sm = sm.parentClass.sqlmeta return columns @classmethod def getColumns(sqlmeta): columns = sqlmeta.getAllColumns() if 'childName' in columns: del columns['childName'] return columns class InheritableSQLObject(SQLObject): sqlmeta = InheritableSQLMeta _inheritable = True SelectResultsClass = InheritableSelectResults def set(self, **kw): if self._parent: SQLObject.set(self, _suppress_set_sig=True, **kw) else: SQLObject.set(self, **kw) def __classinit__(cls, new_attrs): SQLObject.__classinit__(cls, new_attrs) # if we are a child class, add sqlbuilder fields from parents currentClass = cls.sqlmeta.parentClass while currentClass: for column in currentClass.sqlmeta.columnDefinitions.values(): if column.name == 'childName': continue if isinstance(column, ForeignKey): continue setattr(cls.q, column.name, getattr(currentClass.q, column.name)) currentClass = currentClass.sqlmeta.parentClass @classmethod def _SO_setupSqlmeta(cls, new_attrs, is_base): # Note: cannot use super(InheritableSQLObject, cls)._SO_setupSqlmeta - # InheritableSQLObject is not defined when it's __classinit__ # is run. Cannot use SQLObject._SO_setupSqlmeta, either: # the method would be bound to wrong class. if cls.__name__ == "InheritableSQLObject": call_super = super(cls, cls) else: # InheritableSQLObject must be in globals yet call_super = super(InheritableSQLObject, cls) call_super._SO_setupSqlmeta(new_attrs, is_base) sqlmeta = cls.sqlmeta sqlmeta.childClasses = {} # locate parent class and register this class in it's children sqlmeta.parentClass = None for superclass in cls.__bases__: if getattr(superclass, '_inheritable', False) \ and (superclass.__name__ != 'InheritableSQLObject'): if sqlmeta.parentClass: # already have a parent class; # cannot inherit from more than one raise NotImplementedError( "Multiple inheritance is not implemented") sqlmeta.parentClass = superclass superclass.sqlmeta.childClasses[cls.__name__] = cls if sqlmeta.parentClass: # remove inherited column definitions cls.sqlmeta.columns = {} cls.sqlmeta.columnList = [] cls.sqlmeta.columnDefinitions = {} # default inheritance child name if not sqlmeta.childName: sqlmeta.childName = cls.__name__ @classmethod def get(cls, id, connection=None, selectResults=None, childResults=None, childUpdate=False): val = super(InheritableSQLObject, cls).get(id, connection, selectResults) #DSM: If we are updating a child, we should never return a child... if childUpdate: return val #DSM: If this class has a child, return the child if 'childName' in cls.sqlmeta.columns: childName = val.childName if childName is not None: childClass = cls.sqlmeta.childClasses[childName] # If the class has no columns (which sometimes makes sense # and may be true for non-inheritable (leaf) classes only), # shunt the query to avoid almost meaningless SQL # like "SELECT NULL FROM child WHERE id=1". # This is based on assumption that child object exists # if parent object exists. (If it doesn't your database # is broken and that is a job for database maintenance.) if not (childResults or childClass.sqlmeta.columns): childResults = (None,) return childClass.get(id, connection=connection, selectResults=childResults) #DSM: Now, we know we are alone or the last child in a family... #DSM: It's time to find our parents inst = val while inst.sqlmeta.parentClass and not inst._parent: inst._parent = inst.sqlmeta.parentClass.get(id, connection=connection, childUpdate=True) inst = inst._parent #DSM: We can now return ourself return val @classmethod def _notifyFinishClassCreation(cls): sqlmeta = cls.sqlmeta # verify names of added columns if sqlmeta.parentClass: # FIXME: this does not check for grandparent column overrides parentCols = sqlmeta.parentClass.sqlmeta.columns.keys() for column in sqlmeta.columnList: if column.name == 'childName': raise AttributeError( "The column name 'childName' is reserved") if column.name in parentCols: raise AttributeError("The column '%s' is" " already defined in an inheritable parent" % column.name) # if this class is inheritable, add column for children distinction if cls._inheritable and (cls.__name__ != 'InheritableSQLObject'): sqlmeta.addColumn(StringCol(name='childName', # limit string length to get VARCHAR and not CLOB length=255, default=None)) if not sqlmeta.columnList: # There are no columns - call addColumn to propagate columns # from parent classes to children sqlmeta.addColumn(None) if not sqlmeta.joins: # There are no joins - call addJoin to propagate joins # from parent classes to children sqlmeta.addJoin(None) def _create(self, id, **kw): #DSM: If we were called by a children class, #DSM: we must retreive the properties dictionary. #DSM: Note: we can't use the ** call paremeter directly #DSM: as we must be able to delete items from the dictionary #DSM: (and our children must know that the items were removed!) if 'kw' in kw: kw = kw['kw'] #DSM: If we are the children of an inheritable class, #DSM: we must first create our parent if self.sqlmeta.parentClass: parentClass = self.sqlmeta.parentClass new_kw = {} parent_kw = {} for (name, value) in kw.items(): if (name != 'childName') and hasattr(parentClass, name): parent_kw[name] = value else: new_kw[name] = value kw = new_kw # Need to check that we have enough data to sucesfully # create the current subclass otherwise we will leave # the database in an inconsistent state. for col in self.sqlmeta.columnList: if (col._default == sqlbuilder.NoDefault) and \ (col.name not in kw) and (col.foreignName not in kw): raise TypeError, "%s() did not get expected keyword argument %s" % (self.__class__.__name__, col.name) parent_kw['childName'] = self.sqlmeta.childName self._parent = parentClass(kw=parent_kw, connection=self._connection) id = self._parent.id # TC: Create this record and catch all exceptions in order to destroy # TC: the parent if the child can not be created. try: super(InheritableSQLObject, self)._create(id, **kw) except: # If we are outside a transaction and this is a child, destroy the parent connection = self._connection if (not isinstance(connection, dbconnection.Transaction) and connection.autoCommit) and self.sqlmeta.parentClass: self._parent.destroySelf() #TC: Do we need to do this?? self._parent = None # TC: Reraise the original exception raise @classmethod def _findAlternateID(cls, name, dbName, value, connection=None): result = list(cls.selectBy(connection, **{name: value})) if not result: return result, None obj = result[0] return [obj.id], obj @classmethod def select(cls, clause=None, *args, **kwargs): parentClass = cls.sqlmeta.parentClass childUpdate = kwargs.pop('childUpdate', None) # childUpdate may have one of three values: # True: # select was issued by parent class to create child objects. # Execute select without modifications. # None (default): # select is run by application. If this class is inheritance # child, delegate query to the parent class to utilize # InheritableIteration optimizations. Selected records # are restricted to this (child) class by adding childName # filter to the where clause. # False: # select is delegated from inheritance child which is parent # of another class. Delegate the query to parent if possible, # but don't add childName restriction: selected records # will be filtered by join to the table filtered by childName. if (not childUpdate) and parentClass: if childUpdate is None: # this is the first parent in deep hierarchy addClause = parentClass.q.childName == cls.sqlmeta.childName # if the clause was one of TRUE varians, replace it if (clause is None) or (clause is sqlbuilder.SQLTrueClause) \ or (isinstance(clause, basestring) and (clause == 'all')): clause = addClause else: # patch WHERE condition: # change ID field of this class to ID of parent class # XXX the clause is patched in place; it would be better # to build a new one if we have to replace field clsID = cls.q.id parentID = parentClass.q.id def _get_patched(clause): if isinstance(clause, sqlbuilder.SQLOp): _patch_id_clause(clause) return None elif not isinstance(clause, sqlbuilder.Field): return None elif (clause.tableName == clsID.tableName) \ and (clause.fieldName == clsID.fieldName): return parentID else: return None def _patch_id_clause(clause): if not isinstance(clause, sqlbuilder.SQLOp): return expr = _get_patched(clause.expr1) if expr: clause.expr1 = expr expr = _get_patched(clause.expr2) if expr: clause.expr2 = expr _patch_id_clause(clause) # add childName filter clause = sqlbuilder.AND(clause, addClause) return parentClass.select(clause, childUpdate=False, *args, **kwargs) else: return super(InheritableSQLObject, cls).select( clause, *args, **kwargs) @classmethod def selectBy(cls, connection=None, **kw): clause = [] foreignColumns = {} currentClass = cls while currentClass: foreignColumns.update(dict([(column.foreignName, name) for (name, column) in currentClass.sqlmeta.columns.items() if column.foreignKey ])) currentClass = currentClass.sqlmeta.parentClass for name, value in kw.items(): if name in foreignColumns: name = foreignColumns[name] # translate "key" to "keyID" if isinstance(value, SQLObject): value = value.id currentClass = cls while currentClass: try: clause.append(getattr(currentClass.q, name) == value) break except AttributeError, err: pass currentClass = currentClass.sqlmeta.parentClass else: raise AttributeError("'%s' instance has no attribute '%s'" % (cls.__name__, name)) if clause: clause = reduce(sqlbuilder.AND, clause) else: clause = None # select all conn = connection or cls._connection return cls.SelectResultsClass(cls, clause, connection=conn) def destroySelf(self): #DSM: If this object has parents, recursivly kill them if hasattr(self, '_parent') and self._parent: self._parent.destroySelf() super(InheritableSQLObject, self).destroySelf() def _reprItems(self): items = super(InheritableSQLObject, self)._reprItems() # add parent attributes (if any) if self.sqlmeta.parentClass: items.extend(self._parent._reprItems()) # filter out our special column return [item for item in items if item[0] != 'childName'] __all__ = ['InheritableSQLObject']