[Zope-CVS] CVS: Products/Ape/lib/apelib/sql - properties.py:1.5
Shane Hathaway
shane at zope.com
Mon Aug 11 15:02:51 EDT 2003
Update of /cvs-repository/Products/Ape/lib/apelib/sql
In directory cvs.zope.org:/tmp/cvs-serv25640/sql
Modified Files:
properties.py
Log Message:
Added experimental SQLMultiTableProperties.
SQLMultiTableProperties stores class-bound properties in a class-specific
table, allowing for better relational properties. It also stores
instance-bound properties in the non-class-specific table.
It's experimental because the code has some inherent flaws, indicating
that we probably ought to re-work the SQL code to manage tables better.
=== Products/Ape/lib/apelib/sql/properties.py 1.4 => 1.5 ===
--- Products/Ape/lib/apelib/sql/properties.py:1.4 Mon May 19 15:32:34 2003
+++ Products/Ape/lib/apelib/sql/properties.py Mon Aug 11 14:02:16 2003
@@ -17,6 +17,7 @@
"""
from apelib.core.schemas import RowSequenceSchema
+from apelib.core.interfaces import IGateway, IDatabaseInitializer
from sqlbase import SQLGatewayBase
@@ -77,4 +78,247 @@
state = list(state)
state.sort()
return tuple(state)
+
+######################################################################
+#
+# Experimental fixed-schema property storage.
+#
+# To enable, use SQLMultiTableProperties in place of SQLProperties.
+#
+######################################################################
+
+
+class SQLFixedProperties (SQLGatewayBase):
+ """SQL fixed-schema properties gateway.
+ """
+
+ def __init__(self, conn_name, table_name, cols):
+ self.table_base_name = table_name
+ self.column_defs = cols
+ self.schema = None
+ SQLGatewayBase.__init__(self, conn_name)
+
+
+ def prepareTable(self, event):
+ """Creates the fixed property table without triggering an error.
+ """
+ # Note: event is any kind of IGatewayEvent.
+ conn = event.getConnection(self.conn_name)
+ if conn.prefix:
+ full_table_name = '%s_%s' % (conn.prefix, self.table_base_name)
+ else:
+ full_table_name = self.table_base_name
+ full_table_name = full_table_name.lower()
+ query = conn.getQuery('', (), 'table_names')
+ recs = conn.execute(query, fetch=1)
+ for rec in recs:
+ if rec[0].lower() == full_table_name:
+ # The table already exists.
+ # TODO: check the table schema.
+ break
+ else:
+ # Create the table.
+ query = conn.getQuery(self.table_base_name,
+ self.column_defs, 'create')
+ conn.execute(query)
+
+
+ def load(self, event):
+ key = long(event.getKey())
+ recs = self.execute(event, 'read', fetch=1, key=key)
+ if not recs:
+ return (), ()
+ if len(recs) > 1:
+ raise ValueError, "Multiple records where only one expected"
+ record = recs[0]
+ items = []
+ cols = self.column_defs
+ for n in range(len(cols)):
+ name, typ, unique = cols[n]
+ if name.startswith('_'):
+ prop_name = name[1:]
+ else:
+ prop_name = name
+ items.append((prop_name, typ, record[n]))
+ return items, tuple(record)
+
+
+ def store(self, event, state, leftover=None):
+ cols = self.column_defs
+ statedict = {} # prop name -> (type, value)
+ for name, typ, value in state:
+ statedict[name] = (typ, value)
+ data = {}
+ record = []
+ for col in cols:
+ name = col[0]
+ if name.startswith('_'):
+ prop_name = name[1:]
+ else:
+ prop_name = name
+ if statedict.has_key(prop_name):
+ value = statedict[prop_name][1]
+ record.append(value)
+ data[name] = value
+ del statedict[prop_name]
+ else:
+ record.append(None)
+ data[name] = None # Hopefully this translates to NULL.
+ if statedict:
+ if leftover is not None:
+ # Pass back a dictionary of properties not stored yet.
+ leftover.update(statedict)
+ else:
+ raise ValueError(
+ "Extra properties provided for fixed schema: %s"
+ % statedict.keys())
+ key = event.getKey()
+ recs = self.execute(event, 'read', fetch=1, key=key)
+ if not recs:
+ self.execute(event, 'insert', key=key, _data=data)
+ else:
+ self.execute(event, 'update', key=key, _data=data)
+ return tuple(record)
+
+
+
+class SQLMultiTableProperties:
+
+ __implements__ = IGateway, IDatabaseInitializer
+
+ schema = RowSequenceSchema()
+ schema.addField('id', 'string', 1)
+ schema.addField('type', 'string')
+ schema.addField('data', 'string')
+
+ def __init__(self, conn_name='db'):
+ self.conn_name = conn_name
+ self.var_props = SQLProperties(conn_name=conn_name)
+ self.fixed_props = {} # class name -> SQLFixedProperties instance
+
+ def getSchema(self):
+ return self.schema
+
+ def getSources(self, event):
+ return None
+
+
+ def init(self, event):
+ self.var_props.init(event)
+ if event.clearing():
+ # Clear the fixed property tables by searching for tables
+ # with a special name.
+ conn = event.getConnection(self.conn_name)
+ if conn.prefix:
+ to_find = '%s_fp_' % conn.prefix
+ else:
+ to_find = 'fp_'
+ query = conn.getQuery('', (), 'table_names')
+ recs = conn.execute(query, fetch=1)
+ for (full_table_name,) in recs:
+ if full_table_name.startswith(to_find):
+ table_name = 'fp_' + full_table_name[len(to_find):]
+ # This is a fixed property table. Clear it.
+ query = conn.getQuery(table_name, (), 'clear')
+ conn.execute(query)
+ conn.db.commit()
+
+
+ def getColumnsForClass(self, module_name, class_name):
+ """Returns the class-defined property schema.
+
+ This Zope2-ism should be made pluggable later on.
+ """
+ d = {}
+ m = __import__(module_name, d, d, ('__doc__',))
+ klass = getattr(m, class_name)
+ cols = []
+ props = getattr(klass, '_properties', ())
+ if not props:
+ return None
+ for p in props:
+ prop_name = p['id']
+ if prop_name == 'key':
+ name = '_key'
+ else:
+ name = prop_name
+ cols.append((name, p['type'], 0))
+ return tuple(cols)
+
+
+ def getFixedProps(self, event):
+ """Returns a SQLFixedProperties instance or None.
+ """
+ classification = event.getClassification()
+ if classification is None:
+ return None
+ cn = classification.get('class_name')
+ if cn is None:
+ return None
+ if self.fixed_props.has_key(cn):
+ return self.fixed_props[cn] # May be None
+ pos = cn.rfind('.')
+ if pos < 0:
+ raise ValueError, "Not a qualified class name: %s" % repr(cn)
+ module_name = cn[:pos]
+ class_name = cn[pos + 1:]
+ # XXX There's a major potential for conflicting table names,
+ # but databases like PostgreSQL truncate table names, so
+ # we shouldn't put the module name in the table name.
+ table_name = 'fp_%s' % class_name
+ cols = self.getColumnsForClass(module_name, class_name)
+ if not cols:
+ # No fixed properties for this class.
+ self.fixed_props[cn] = None
+ return None
+ fp = SQLFixedProperties(self.conn_name, table_name, cols)
+ fp.prepareTable(event)
+ # XXX If the transaction gets aborted, the table creation will
+ # be undone, but self.fixed_props won't see the change.
+ # Perhaps we need to reset self.fixed_props on abort.
+ self.fixed_props[cn] = fp
+ return fp
+
+
+ def load(self, event):
+ var_state, var_hash = self.var_props.load(event)
+ fp = self.getFixedProps(event)
+ if fp is None:
+ return var_state, var_hash
+ fixed_state, fixed_hash = fp.load(event)
+ # Merge fixed_state and var_state, letting fixed_state
+ # override var_state except when the value in fixed_state is
+ # NULL.
+ res = []
+ placement = {} # property name -> placement in results
+ for rec in fixed_state:
+ placement[rec[0]] = len(res)
+ res.append(rec)
+ for rec in var_state:
+ index = placement.get(rec[0])
+ if index is None:
+ res.append(rec)
+ elif res[index][2] is None:
+ # override the fixed value, since it was None.
+ res[index] = rec
+ return res, (fixed_hash, var_hash)
+
+
+ def store(self, event, state):
+ fp = self.getFixedProps(event)
+ if fp is None:
+ return self.var_props.store(event, state)
+ # Store the fixed state first and find out what got left over.
+ leftover = {}
+ state = list(state)
+ state.sort()
+ fixed_hash = fp.store(event, state, leftover=leftover)
+ if leftover:
+ var_state = []
+ for prop_name, (typ, value) in leftover.items():
+ var_state.append((prop_name, typ, value))
+ var_hash = self.var_props.store(event, var_state)
+ else:
+ var_hash = ()
+ return (fixed_hash, var_hash)
More information about the Zope-CVS
mailing list