--- SQLObject.orig/SQLObject.py 2003-11-12 12:03:45.000000000 -0500
+++ SQLObject/SQLObject.py 2004-02-07 00:57:58.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> 7 Feb 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
+ newClass._childClasses = {}
+ #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
+ cls._childClasses[className] = newClass
+
# 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 ['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 = []
+ #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.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,13 @@
# 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
+ _childClasses = {} # Reference to child classes
+ childName = None # Children name (to be able to get a subclass)
+
# The _defaultOrder is used by SelectResults
_defaultOrder = None
@@ -408,9 +453,29 @@
cache.put(id, cls, val)
finally:
cache.finishPut(cls)
+ #DSM: If this class has a child, return the child, if not
+ if hasattr(val, 'childName'):
+ childName = val.childName
+ if childName is not None:
+ return val._childClasses[childName](id)
+ #DSM: This class doesn't have a child... return this class
return val
- def addColumn(cls, columnDef, changeSchema=False):
+ def addColumn(cls, columnDef, changeSchema=False, childUpdate=False):
+ #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 childUpdate or cls._parentClass:
+ for col in cls._parentClass._columns:
+ cname = col.kw['name']
+ if 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 childUpdate:
+ makeProperties(cls)
+ return
+
column = columnDef.withClass(cls)
name = column.name
assert name != 'id', "The 'id' column is implicit, and should not be defined as a column"
@@ -442,7 +507,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 == 'childName':
setattr(cls, getterName(name), getter)
cls._SO_plainGetters[name] = 1
@@ -459,7 +524,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 == 'childName':
setattr(cls, setterName(name), setter)
# We keep track of setters that haven't been
# overridden, because we can combine these
@@ -517,6 +582,11 @@
if cls._SO_finishedClassCreation:
makeProperties(cls)
+ #DSM: Update each child class if needed and existing (only for new
+ #DSM: dynamic column as no child classes exists at object creation)
+ for c in cls._childClasses.values():
+ c.addColumn(columnDef, childUpdate=True)
+
addColumn = classmethod(addColumn)
def addColumnsFromDatabase(cls):
@@ -563,11 +633,30 @@
cls._connection.delColumn(cls._table, column)
if cls._SO_finishedClassCreation:
- unmakeProperties(cls)
+ delattr(cls, name)
+
+ #DSM: Update each child class if needed and delete properties for this column
+ for c in cls._childClasses.values():
+ delattr(c, name)
delColumn = classmethod(delColumn)
- def addJoin(cls, joinDef):
+ def addJoin(cls, joinDef, childUpdate=False):
+ #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 childUpdate or cls._parentClass:
+ for jdef in cls._parentClass._joins:
+ join = jdef.withClass(cls)
+ jname = join.joinMethodName
+ jarn = join.addRemoveName
+ setattr(cls, getterName(jname), eval('lambda self: self._parent.%s' % jname))
+ if hasattr(join, 'remove'): setattr(cls, 'remove' + jarn, eval('lambda self,o: self._parent.remove%s(o)' % jarn))
+ if hasattr(join, 'add'): setattr(cls, 'add' + jarn, eval('lambda self,o: self._parent.add%s(o)' % jarn))
+ if childUpdate:
+ makeProperties(cls)
+ return
+
# The name of the method we'll create. If it's
# automatically generated, it's generated by the
# join class.
@@ -615,6 +704,11 @@
if cls._SO_finishedClassCreation:
makeProperties(cls)
+
+ #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 cls._childClasses.values():
+ c.addJoin(joinDef, childUpdate=True)
addJoin = classmethod(addJoin)
@@ -641,7 +735,11 @@
delattr(cls, 'add' + join.addRemovePrefix)
if cls._SO_finishedClassCreation:
- unmakeProperties(cls)
+ delattr(cls, meth)
+
+ #DSM: Update each child class if needed and delete properties for this join
+ for c in cls._childClasses.values():
+ delattr(c, meth)
delJoin = classmethod(delJoin)
@@ -835,6 +933,20 @@
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:
+ #kw['childName'] = inst._className
+ inst._parent = cls._parentClass.new(kw=kw)
+ inst._parent.childName = inst._className
+ id = inst._parent.id
+
# First we do a little fix-up on the keywords we were
# passed:
for column in inst._SO_columns:
@@ -864,7 +976,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
@@ -1008,6 +1123,8 @@
clearTable = classmethod(clearTable)
def destroySelf(self):
+ #DSM: If this object has parents, recursivly kill them
+ if hasattr(self, '_parent') and self._parent: self._parent.destroySelf()
# Kills this object. Kills it dead!
self._SO_obsolete = True
self._connection._SO_delete(self)
@@ -1066,6 +1183,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.id == 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
|