--- SQLObject.orig/SQLObject.py 2003-11-12 12:03:45.000000000 -0500
+++ SQLObject/SQLObject.py 2004-01-14 23:50:50.000000000 -0500
@@ -4,6 +4,10 @@
SQLObject is a object-relational mapper. See SQLObject.html or
SQLObject.txt for more.
+Modified by
+ Daniel Savard, Xsoli Inc <sqlobject xsoli.com> 14 Jan 2004
+ - Added support for simple table inheritance.
+
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2.1 of the
@@ -155,10 +159,37 @@
classRegistry[registry] = {}
classRegistry[registry][className] = newClass
+ #DSM: Need to keep the name of the class for easy access later
+ newClass._className = className
+ #DSM: Need to know very soon if the class is a children of an inheritable class.
+ #DSM: If so, we keep a link to our parent class.
+ for cls in bases:
+ if hasattr(cls, '_inheritable') and cls._inheritable:
+ newClass._parentClass = cls
+
# We append to _columns, but we don't want to change the
# superclass's _columns list, so we make a copy if necessary
if not d.has_key('_columns'):
newClass._columns = newClass._columns[:]
+ #DSM: If this class is a child of a parent class, we need to do some
+ #DSM: attribute check and a a foreign key to the parent.
+ if newClass._parentClass:
+ #DSM: First, look for invalid column name: reserved ones or same as a parent
+ parentCols = [column.kw['name'] for column in newClass._columns]
+ for col in implicitColumns:
+ cname = col.kw['name']
+ if cname in ['parent', 'parentID', 'childID', 'childName']:
+ raise AttributeError, "The column name '%s' is reserved" % cname
+ if cname in parentCols:
+ raise AttributeError, "The column '%s' is already defined in an inheritable parent" % cname
+ #DSM: Remove columns if inherited from an inheritable class as we don't want them
+ #DSM: All we want is a foreign key that will link to our parent
+ newClass._columns = []
+ newClass._columns.append(Col.ForeignKey(name='parent', foreignKey=newClass._parentClass._className))
+ #DSM: If this is inheritable, add some default columns to be able to link to children
+ if hasattr(newClass, '_inheritable') and newClass._inheritable:
+ newClass._columns.append(Col.IntCol(name='childID',default=None))
+ newClass._columns.append(Col.StringCol(name='childName',default=None))
newClass._columns.extend(implicitColumns)
if not d.has_key('_joins'):
newClass._joins = newClass._joins[:]
@@ -218,6 +249,13 @@
# SQL where-clause generation. See the sql module for
# more.
newClass.q = SQLBuilder.SQLObjectTable(newClass)
+ #DSM: If we are a child, get the q magic from the parent
+ currentClass = newClass
+ while currentClass._parentClass:
+ currentClass = currentClass._parentClass
+ for col in currentClass._columns:
+ if type(col) == Col.ForeignKey: continue
+ setattr(newClass.q, col.kw['name'], getattr(currentClass.q, col.kw['name']))
for column in newClass._columns[:]:
newClass.addColumn(column)
@@ -349,6 +387,14 @@
# it's set (default 1).
_cacheValues = True
+ #DSM: The _inheritable attribute controls wheter the class can by
+ #DSM: inherited 'logically' with a foreignKey and back reference.
+ _inheritable = False # Does this class is inheritable
+ _parentClass = None # A reference to the parent class
+ parent = None # Foreign key to the parent
+ childID = None # Id to the children
+ childName = None # Children name (to be able to get a subclass)
+
# The _defaultOrder is used by SelectResults
_defaultOrder = None
@@ -442,7 +488,7 @@
# Here if the _get_columnName method isn't in the
# definition, we add it with the default
# _SO_get_columnName definition.
- if not hasattr(cls, getterName(name)):
+ if not hasattr(cls, getterName(name)) or name == 'parentID' or name == 'childID' or name == 'childName':
setattr(cls, getterName(name), getter)
cls._SO_plainGetters[name] = 1
@@ -459,7 +505,7 @@
setattr(cls, '_SO_fromPython_%s' % name, column.fromPython)
setattr(cls, rawSetterName(name), setter)
# Then do the aliasing
- if not hasattr(cls, setterName(name)):
+ if not hasattr(cls, setterName(name)) or name == 'parentID' or name == 'childID' or name == 'childName':
setattr(cls, setterName(name), setter)
# We keep track of setters that haven't been
# overridden, because we can combine these
@@ -488,10 +534,19 @@
# And we set the _get_columnName version
# (sans ID ending)
- if not hasattr(cls, getterName(name)[:-2]):
+ if not hasattr(cls, getterName(name)[:-2]) or name == 'parentID':
setattr(cls, getterName(name)[:-2], getter)
cls._SO_plainForeignGetters[name[:-2]] = 1
+ #DSM: Try to add parent properties to the current class
+ if name=='parentID':
+ for col in cls._parentClass._columns:
+ cname = col.kw['name']
+ if cname == 'parent' or cname == 'childID' or cname == 'childName': continue
+ setattr(cls, getterName(cname), eval('lambda self: self.parent.%s' % cname))
+ if not col.kw.has_key('immutable') or not col.kw['immutable']:
+ setattr(cls, setterName(cname), eval('lambda self, val: setattr(self.parent, %s, val)' % repr(cname)))
+
if not column.immutable:
# The setter just gets the ID of the object,
# and then sets the real column.
@@ -835,6 +890,18 @@
else:
id = None
+ #DSM: If we were called by a children class, we must retreive the properties dictionnary.
+ #DSM: Note: we can't use the ** call paremeter directly as we must be able to delete items from
+ #DSM: the dictionary (and our children must know that the items were removed!)
+ fromChild = False
+ if kw.has_key('kw'):
+ kw = kw['kw']
+ fromChild = True
+ #DSM: If we are the children of an inheritable class, we must first create our parent
+ if cls._parentClass:
+ parent = cls._parentClass.new(kw=kw)
+ kw['parent'] = parent
+
# First we do a little fix-up on the keywords we were
# passed:
for column in inst._SO_columns:
@@ -864,7 +931,10 @@
for name, value in kw.items():
if name in inst._SO_plainSetters:
forDB[name] = value
- else:
+ #DSM: If this is a call from the child, we must remove the parameter for the database
+ if fromChild: del kw[name]
+ elif not fromChild:
+ #DSM: Only use other items if this isn't a call from the child
others[name] = value
# We take all the straight-to-DB values and use set() to
@@ -881,9 +951,21 @@
# Then we finalize the process:
inst._SO_finishCreate(id)
+
+ #DSM: If we are a child, we must set our parent link
+ if cls._parentClass:
+ parent.childID = inst.id
+ parent.childName = inst._className
+
return inst
new = classmethod(new)
+ #DSM: return the subclass if asked for
+ def getSubClass(self):
+ if not hasattr(self, 'childID'): return None
+ if self.childID is None: return None
+ return findClass(self.childName)(self.childID)
+
def _SO_finishCreate(self, id=None):
# Here's where an INSERT is finalized.
# These are all the column values that were supposed
@@ -1008,6 +1090,13 @@
clearTable = classmethod(clearTable)
def destroySelf(self):
+ #DSM: If this object has children, find the last one and destroy it. If not, simply destroy this object
+ while self.getSubClass(): self = self.getSubClass()
+ self._destroySelf()
+
+ def _destroySelf(self):
+ #DSM: If this object has parents, recursivly kill them
+ if self.parent: self.parent._destroySelf()
# Kills this object. Kills it dead!
self._SO_obsolete = True
self._connection._SO_delete(self)
@@ -1066,6 +1155,33 @@
self.clause = clause
tablesDict = SQLBuilder.tablesUsedDict(self.clause)
tablesDict[sourceClass._table] = 1
+ #DSM: if this class has a parent, we need to link it and be sure the parent is in the table list
+ #DSM: the following code is before clauseTables because if the user uses clauseTables (and normal string SELECT), he
+ #DSM: must know what he wants and will do himself the relationship between classes.
+ if type(self.clause) is not str:
+ tableRegistry = {}
+ for registryClass in classRegistry[None].values():
+ if registryClass._table in tablesDict:
+ #DSM: By default, no parent are needed for the clauses
+ tableRegistry[registryClass] = registryClass
+ currentClass = registryClass
+ while currentClass._parentClass:
+ currentClass = currentClass._parentClass
+ if tableRegistry.has_key(currentClass):
+ #DSM: Must keep the last parent needed (to limit the number of join needed)
+ tableRegistry[registryClass] = currentClass
+ #DSM: Remove this class as it is a parent one of a needed children
+ del tableRegistry[currentClass]
+ #DSM: Table registry contains only the last children or standalone classes
+ parentClause = []
+ for (currentClass, minParentClass) in tableRegistry.items():
+ while currentClass != minParentClass and currentClass._parentClass:
+ parentClass = currentClass._parentClass
+ parentClause.append(currentClass.q.parentID == parentClass.q.id)
+ currentClass = parentClass
+ tablesDict[currentClass._table] = 1
+ self.clause = reduce(SQLBuilder.AND, parentClause, clause)
+
if clauseTables:
for table in clauseTables:
tablesDict[table] = 1
|