[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