Code

Add Number and Boolean types to hyperdb.
[roundup.git] / roundup / hyperdb.py
index 392363fbd3f6dc9d41b21641120c0064bcc3a9b4..2905166251310174fca98b8f340bb247d90ec462 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: hyperdb.py,v 1.68 2002-06-11 06:52:03 richard Exp $
+# $Id: hyperdb.py,v 1.76 2002-07-18 11:17:30 gmcm Exp $
 
 __doc__ = """
 Hyperdatabase implementation, especially field types.
 """
 
 # standard python modules
-import re, string, weakref, os, time
+import sys, os, time, re
 
 # roundup modules
 import date, password
@@ -33,12 +33,18 @@ class Sink:
         pass
 DEBUG = os.environ.get('HYPERDBDEBUG', '')
 if DEBUG and __debug__:
-    DEBUG = open(DEBUG, 'a')
+    if DEBUG == 'stdout':
+        DEBUG = sys.stdout
+    else:
+        DEBUG = open(DEBUG, 'a')
 else:
     DEBUG = Sink()
 TRACE = os.environ.get('HYPERDBTRACE', '')
 if TRACE and __debug__:
-    TRACE = open(TRACE, 'w')
+    if TRACE == 'stdout':
+        TRACE = sys.stdout
+    else:
+        TRACE = open(TRACE, 'w')
 else:
     TRACE = Sink()
 def traceMark():
@@ -50,6 +56,8 @@ del Sink
 #
 class String:
     """An object designating a String property."""
+    def __init__(self, indexme='no'):
+        self.indexme = indexme == 'yes'
     def __repr__(self):
         ' more useful for dumps '
         return '<%s>'%self.__class__
@@ -102,15 +110,37 @@ class Multilink:
         ' more useful for dumps '
         return '<%s to "%s">'%(self.__class__, self.classname)
 
-class DatabaseError(ValueError):
-    '''Error to be raised when there is some problem in the database code
-    '''
+class Boolean:
+    """An object designating a boolean property"""
+    def __repr__(self):
+        'more useful for dumps'
+        return '<%s>' % self.__class__
+    
+class Number:
+    """An object designating a numeric property"""
+    def __repr__(self):
+        'more useful for dumps'
+        return '<%s>' % self.__class__
+#
+# Support for splitting designators
+#
+class DesignatorError(ValueError):
     pass
-
+def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
+    ''' Take a foo123 and return ('foo', 123)
+    '''
+    m = dre.match(designator)
+    if m is None:
+        raise DesignatorError, '"%s" not a node designator'%designator
+    return m.group(1), m.group(2)
 
 #
 # the base Database class
 #
+class DatabaseError(ValueError):
+    '''Error to be raised when there is some problem in the database code
+    '''
+    pass
 class Database:
     '''A database for storing records containing flexible data types.
 
@@ -129,6 +159,13 @@ in-database value is returned in preference to the in-transaction value.
 This is necessary to determine if any values have changed during a
 transaction.
 
+
+Implementation
+--------------
+
+All methods except __repr__ and getnode must be implemented by a
+concrete backend Class.
+
 '''
 
     # flag to set on retired entries
@@ -151,6 +188,10 @@ transaction.
         """
         raise NotImplementedError
 
+    def post_init(self):
+        """Called once the schema initialisation has finished."""
+        raise NotImplementedError
+
     def __getattr__(self, classname):
         """A convenient way of calling self.getclass(classname)."""
         raise NotImplementedError
@@ -191,29 +232,7 @@ transaction.
         '''Copy the node contents, converting non-marshallable data into
            marshallable data.
         '''
-        if __debug__:
-            print >>DEBUG, 'serialise', classname, node
-        properties = self.getclass(classname).getprops()
-        d = {}
-        for k, v in node.items():
-            # if the property doesn't exist, or is the "retired" flag then
-            # it won't be in the properties dict
-            if not properties.has_key(k):
-                d[k] = v
-                continue
-
-            # get the property spec
-            prop = properties[k]
-
-            if isinstance(prop, Password):
-                d[k] = str(v)
-            elif isinstance(prop, Date) and v is not None:
-                d[k] = v.get_tuple()
-            elif isinstance(prop, Interval) and v is not None:
-                d[k] = v.get_tuple()
-            else:
-                d[k] = v
-        return d
+        return node
 
     def setnode(self, classname, nodeid, node):
         '''Change the specified node.
@@ -223,31 +242,7 @@ transaction.
     def unserialise(self, classname, node):
         '''Decode the marshalled node data
         '''
-        if __debug__:
-            print >>DEBUG, 'unserialise', classname, node
-        properties = self.getclass(classname).getprops()
-        d = {}
-        for k, v in node.items():
-            # if the property doesn't exist, or is the "retired" flag then
-            # it won't be in the properties dict
-            if not properties.has_key(k):
-                d[k] = v
-                continue
-
-            # get the property spec
-            prop = properties[k]
-
-            if isinstance(prop, Date) and v is not None:
-                d[k] = date.Date(v)
-            elif isinstance(prop, Interval) and v is not None:
-                d[k] = date.Interval(v)
-            elif isinstance(prop, Password):
-                p = password.Password()
-                p.unpack(v)
-                d[k] = p
-            else:
-                d[k] = v
-        return d
+        return node
 
     def getnode(self, classname, nodeid, db=None, cache=1):
         '''Get a node from the database.
@@ -318,12 +313,15 @@ transaction.
         '''
         raise NotImplementedError
 
-_marker = []
 #
 # The base Class class
 #
 class Class:
-    """The handle to a particular class of nodes in a hyperdatabase."""
+    """ The handle to a particular class of nodes in a hyperdatabase.
+        
+        All methods except __repr__ and getnode must be implemented by a
+        concrete backend Class.
+    """
 
     def __init__(self, db, classname, **properties):
         """Create a new class with a given name and property specification.
@@ -332,13 +330,7 @@ class Class:
         or a ValueError is raised.  The keyword arguments in 'properties'
         must map names to property objects, or a TypeError is raised.
         """
-        self.classname = classname
-        self.properties = properties
-        self.db = weakref.proxy(db)       # use a weak ref to avoid circularity
-        self.key = ''
-
-        # do the db-related init stuff
-        db.addclass(self)
+        raise NotImplementedError
 
     def __repr__(self):
         '''Slightly more useful representation
@@ -364,116 +356,9 @@ class Class:
         If an id in a link or multilink property does not refer to a valid
         node, an IndexError is raised.
         """
-        if propvalues.has_key('id'):
-            raise KeyError, '"id" is reserved'
-
-        if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
-
-        # new node's id
-        newid = self.db.newid(self.classname)
-
-        # validate propvalues
-        num_re = re.compile('^\d+$')
-        for key, value in propvalues.items():
-            if key == self.key:
-                try:
-                    self.lookup(value)
-                except KeyError:
-                    pass
-                else:
-                    raise ValueError, 'node with key "%s" exists'%value
-
-            # try to handle this property
-            try:
-                prop = self.properties[key]
-            except KeyError:
-                raise KeyError, '"%s" has no property "%s"'%(self.classname,
-                    key)
-
-            if isinstance(prop, Link):
-                if type(value) != type(''):
-                    raise ValueError, 'link value must be String'
-                link_class = self.properties[key].classname
-                # if it isn't a number, it's a key
-                if not num_re.match(value):
-                    try:
-                        value = self.db.classes[link_class].lookup(value)
-                    except (TypeError, KeyError):
-                        raise IndexError, 'new property "%s": %s not a %s'%(
-                            key, value, link_class)
-                elif not self.db.hasnode(link_class, value):
-                    raise IndexError, '%s has no node %s'%(link_class, value)
-
-                # save off the value
-                propvalues[key] = value
-
-                # register the link with the newly linked node
-                if self.properties[key].do_journal:
-                    self.db.addjournal(link_class, value, 'link',
-                        (self.classname, newid, key))
-
-            elif isinstance(prop, Multilink):
-                if type(value) != type([]):
-                    raise TypeError, 'new property "%s" not a list of ids'%key
-                link_class = self.properties[key].classname
-                l = []
-                for entry in value:
-                    if type(entry) != type(''):
-                        raise ValueError, 'link value must be String'
-                    # if it isn't a number, it's a key
-                    if not num_re.match(entry):
-                        try:
-                            entry = self.db.classes[link_class].lookup(entry)
-                        except (TypeError, KeyError):
-                            raise IndexError, 'new property "%s": %s not a %s'%(
-                                key, entry, self.properties[key].classname)
-                    l.append(entry)
-                value = l
-                propvalues[key] = value
-
-                # handle additions
-                for id in value:
-                    if not self.db.hasnode(link_class, id):
-                        raise IndexError, '%s has no node %s'%(link_class, id)
-                    # register the link with the newly linked node
-                    if self.properties[key].do_journal:
-                        self.db.addjournal(link_class, id, 'link',
-                            (self.classname, newid, key))
-
-            elif isinstance(prop, String):
-                if type(value) != type(''):
-                    raise TypeError, 'new property "%s" not a string'%key
-
-            elif isinstance(prop, Password):
-                if not isinstance(value, password.Password):
-                    raise TypeError, 'new property "%s" not a Password'%key
-
-            elif isinstance(prop, Date):
-                if value is not None and not isinstance(value, date.Date):
-                    raise TypeError, 'new property "%s" not a Date'%key
-
-            elif isinstance(prop, Interval):
-                if value is not None and not isinstance(value, date.Interval):
-                    raise TypeError, 'new property "%s" not an Interval'%key
-
-        # make sure there's data where there needs to be
-        for key, prop in self.properties.items():
-            if propvalues.has_key(key):
-                continue
-            if key == self.key:
-                raise ValueError, 'key property "%s" is required'%key
-            if isinstance(prop, Multilink):
-                propvalues[key] = []
-            else:
-                # TODO: None isn't right here, I think...
-                propvalues[key] = None
-
-        # done
-        self.db.addnode(self.classname, newid, propvalues)
-        self.db.addjournal(self.classname, newid, 'create', propvalues)
-        return newid
+        raise NotImplementedError
 
+    _marker = []
     def get(self, nodeid, propname, default=_marker, cache=1):
         """Get the value of a property on an existing node of this class.
 
@@ -486,26 +371,7 @@ class Class:
         determine what its values prior to modification are, you need to
         set cache=0.
         """
-        if propname == 'id':
-            return nodeid
-
-        # get the property (raises KeyErorr if invalid)
-        prop = self.properties[propname]
-
-        # get the node's dict
-        d = self.db.getnode(self.classname, nodeid, cache=cache)
-
-        if not d.has_key(propname):
-            if default is _marker:
-                if isinstance(prop, Multilink):
-                    return []
-                else:
-                    # TODO: None isn't right here, I think...
-                    return None
-            else:
-                return default
-
-        return d[propname]
+        raise NotImplementedError
 
     # XXX not in spec
     def getnode(self, nodeid, cache=1):
@@ -539,142 +405,7 @@ class Class:
         If the value of a Link or Multilink property contains an invalid
         node id, a ValueError is raised.
         """
-        if not propvalues:
-            return
-
-        if propvalues.has_key('id'):
-            raise KeyError, '"id" is reserved'
-
-        if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
-
-        node = self.db.getnode(self.classname, nodeid)
-        if node.has_key(self.db.RETIRED_FLAG):
-            raise IndexError
-        num_re = re.compile('^\d+$')
-        for key, value in propvalues.items():
-            # check to make sure we're not duplicating an existing key
-            if key == self.key and node[key] != value:
-                try:
-                    self.lookup(value)
-                except KeyError:
-                    pass
-                else:
-                    raise ValueError, 'node with key "%s" exists'%value
-
-            # this will raise the KeyError if the property isn't valid
-            # ... we don't use getprops() here because we only care about
-            # the writeable properties.
-            prop = self.properties[key]
-
-            # if the value's the same as the existing value, no sense in
-            # doing anything
-            if node.has_key(key) and value == node[key]:
-                del propvalues[key]
-                continue
-
-            # do stuff based on the prop type
-            if isinstance(prop, Link):
-                link_class = self.properties[key].classname
-                # if it isn't a number, it's a key
-                if type(value) != type(''):
-                    raise ValueError, 'link value must be String'
-                if not num_re.match(value):
-                    try:
-                        value = self.db.classes[link_class].lookup(value)
-                    except (TypeError, KeyError):
-                        raise IndexError, 'new property "%s": %s not a %s'%(
-                            key, value, self.properties[key].classname)
-
-                if not self.db.hasnode(link_class, value):
-                    raise IndexError, '%s has no node %s'%(link_class, value)
-
-                if self.properties[key].do_journal:
-                    # register the unlink with the old linked node
-                    if node[key] is not None:
-                        self.db.addjournal(link_class, node[key], 'unlink',
-                            (self.classname, nodeid, key))
-
-                    # register the link with the newly linked node
-                    if value is not None:
-                        self.db.addjournal(link_class, value, 'link',
-                            (self.classname, nodeid, key))
-
-            elif isinstance(prop, Multilink):
-                if type(value) != type([]):
-                    raise TypeError, 'new property "%s" not a list of ids'%key
-                link_class = self.properties[key].classname
-                l = []
-                for entry in value:
-                    # if it isn't a number, it's a key
-                    if type(entry) != type(''):
-                        raise ValueError, 'new property "%s" link value ' \
-                            'must be a string'%key
-                    if not num_re.match(entry):
-                        try:
-                            entry = self.db.classes[link_class].lookup(entry)
-                        except (TypeError, KeyError):
-                            raise IndexError, 'new property "%s": %s not a %s'%(
-                                key, entry, self.properties[key].classname)
-                    l.append(entry)
-                value = l
-                propvalues[key] = value
-
-                # handle removals
-                if node.has_key(key):
-                    l = node[key]
-                else:
-                    l = []
-                for id in l[:]:
-                    if id in value:
-                        continue
-                    # register the unlink with the old linked node
-                    if self.properties[key].do_journal:
-                        self.db.addjournal(link_class, id, 'unlink',
-                            (self.classname, nodeid, key))
-                    l.remove(id)
-
-                # handle additions
-                for id in value:
-                    if not self.db.hasnode(link_class, id):
-                        raise IndexError, '%s has no node %s'%(
-                            link_class, id)
-                    if id in l:
-                        continue
-                    # register the link with the newly linked node
-                    if self.properties[key].do_journal:
-                        self.db.addjournal(link_class, id, 'link',
-                            (self.classname, nodeid, key))
-                    l.append(id)
-
-            elif isinstance(prop, String):
-                if value is not None and type(value) != type(''):
-                    raise TypeError, 'new property "%s" not a string'%key
-
-            elif isinstance(prop, Password):
-                if not isinstance(value, password.Password):
-                    raise TypeError, 'new property "%s" not a Password'% key
-                propvalues[key] = value
-
-            elif value is not None and isinstance(prop, Date):
-                if not isinstance(value, date.Date):
-                    raise TypeError, 'new property "%s" not a Date'% key
-                propvalues[key] = value
-
-            elif value is not None and isinstance(prop, Interval):
-                if not isinstance(value, date.Interval):
-                    raise TypeError, 'new property "%s" not an Interval'% key
-                propvalues[key] = value
-
-            node[key] = value
-
-        # nothing to do?
-        if not propvalues:
-            return
-
-        # do the set, and journal it
-        self.db.setnode(self.classname, nodeid, node)
-        self.db.addjournal(self.classname, nodeid, 'set', propvalues)
+        raise NotImplementedError
 
     def retire(self, nodeid):
         """Retire a node.
@@ -685,12 +416,7 @@ class Class:
         Retired nodes are not returned by the find(), list(), or lookup()
         methods, and other nodes may reuse the values of their key properties.
         """
-        if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
-        node = self.db.getnode(self.classname, nodeid)
-        node[self.db.RETIRED_FLAG] = 1
-        self.db.setnode(self.classname, nodeid, node)
-        self.db.addjournal(self.classname, nodeid, 'retired', None)
+        raise NotImplementedError
 
     def history(self, nodeid):
         """Retrieve the journal of edits on a particular node.
@@ -705,13 +431,13 @@ class Class:
         'date' is a Timestamp object specifying the time of the change and
         'tag' is the journaltag specified when the database was opened.
         """
-        return self.db.getjournal(self.classname, nodeid)
+        raise NotImplementedError
 
     # Locating nodes:
     def hasnode(self, nodeid):
         '''Determine if the given nodeid actually exists
         '''
-        return self.db.hasnode(self.classname, nodeid)
+        raise NotImplementedError
 
     def setkey(self, propname):
         """Select a String property of this class to be the key property.
@@ -720,12 +446,11 @@ class Class:
         None, or a TypeError is raised.  The values of the key property on
         all existing nodes must be unique or a ValueError is raised.
         """
-        # TODO: validate that the property is a String!
-        self.key = propname
+        raise NotImplementedError
 
     def getkey(self):
         """Return the name of the key property for this class or None."""
-        return self.key
+        raise NotImplementedError
 
     def labelprop(self, default_to_id=0):
         ''' Return the property name for a label for the given node.
@@ -737,21 +462,8 @@ class Class:
             3. "title" property
             4. first property from the sorted property name list
         '''
-        k = self.getkey()
-        if  k:
-            return k
-        props = self.getprops()
-        if props.has_key('name'):
-            return 'name'
-        elif props.has_key('title'):
-            return 'title'
-        if default_to_id:
-            return 'id'
-        props = props.keys()
-        props.sort()
-        return props[0]
-
-    # TODO: set up a separate index db file for this? profile?
+        raise NotImplementedError
+
     def lookup(self, keyvalue):
         """Locate a particular node by its key property and return its id.
 
@@ -760,93 +472,25 @@ class Class:
         the nodes in this class, the matching node's id is returned;
         otherwise a KeyError is raised.
         """
-        cldb = self.db.getclassdb(self.classname)
-        for nodeid in self.db.getnodeids(self.classname, cldb):
-            node = self.db.getnode(self.classname, nodeid, cldb)
-            if node.has_key(self.db.RETIRED_FLAG):
-                continue
-            if node[self.key] == keyvalue:
-                return nodeid
-        raise KeyError, keyvalue
+        raise NotImplementedError
 
     # XXX: change from spec - allows multiple props to match
     def find(self, **propspec):
-        """Get the ids of nodes in this class which link to a given node.
+        """Get the ids of nodes in this class which link to the given nodes.
 
-        'propspec' consists of keyword args propname=nodeid   
+        'propspec' consists of keyword args propname={nodeid:1,}   
           'propname' must be the name of a property in this class, or a
             KeyError is raised.  That property must be a Link or Multilink
             property, or a TypeError is raised.
 
-          'nodeid' must be the id of an existing node in the class linked
-            to by the given property, or an IndexError is raised.
-        """
-        propspec = propspec.items()
-        for propname, nodeid in propspec:
-            # check the prop is OK
-            prop = self.properties[propname]
-            if not isinstance(prop, Link) and not isinstance(prop, Multilink):
-                raise TypeError, "'%s' not a Link/Multilink property"%propname
-            if not self.db.hasnode(prop.classname, nodeid):
-                raise ValueError, '%s has no node %s'%(prop.classname, nodeid)
-
-        # ok, now do the find
-        cldb = self.db.getclassdb(self.classname)
-        l = []
-        for id in self.db.getnodeids(self.classname, db=cldb):
-            node = self.db.getnode(self.classname, id, db=cldb)
-            if node.has_key(self.db.RETIRED_FLAG):
-                continue
-            for propname, nodeid in propspec:
-                # can't test if the node doesn't have this property
-                if not node.has_key(propname):
-                    continue
-                prop = self.properties[propname]
-                property = node[propname]
-                if isinstance(prop, Link) and nodeid == property:
-                    l.append(id)
-                elif isinstance(prop, Multilink) and nodeid in property:
-                    l.append(id)
-        return l
-
-    def stringFind(self, **requirements):
-        """Locate a particular node by matching a set of its String
-        properties in a caseless search.
+        Any node in this class whose 'propname' property links to any of the
+        nodeids will be returned. Used by the full text indexing, which knows
+        that "foo" occurs in msg1, msg3 and file7, so we have hits on these
+        issues:
 
-        If the property is not a String property, a TypeError is raised.
-        
-        The return is a list of the id of all nodes that match.
+            db.issue.find(messages={'1':1,'3':1}, files={'7':1})
         """
-        for propname in requirements.keys():
-            prop = self.properties[propname]
-            if isinstance(not prop, String):
-                raise TypeError, "'%s' not a String property"%propname
-            requirements[propname] = requirements[propname].lower()
-        l = []
-        cldb = self.db.getclassdb(self.classname)
-        for nodeid in self.db.getnodeids(self.classname, cldb):
-            node = self.db.getnode(self.classname, nodeid, cldb)
-            if node.has_key(self.db.RETIRED_FLAG):
-                continue
-            for key, value in requirements.items():
-                if node[key] and node[key].lower() != value:
-                    break
-            else:
-                l.append(nodeid)
-        return l
-
-    def list(self):
-        """Return a list of the ids of the active nodes in this class."""
-        l = []
-        cn = self.classname
-        cldb = self.db.getclassdb(cn)
-        for nodeid in self.db.getnodeids(cn, cldb):
-            node = self.db.getnode(cn, nodeid, cldb)
-            if node.has_key(self.db.RETIRED_FLAG):
-                continue
-            l.append(nodeid)
-        l.sort()
-        return l
+        raise NotImplementedError
 
     # XXX not in spec
     def filter(self, search_matches, filterspec, sort, group, 
@@ -855,220 +499,7 @@ class Class:
             match the 'filter' spec, sorted by the group spec and then the
             sort spec
         '''
-        cn = self.classname
-
-        # optimise filterspec
-        l = []
-        props = self.getprops()
-        for k, v in filterspec.items():
-            propclass = props[k]
-            if isinstance(propclass, Link):
-                if type(v) is not type([]):
-                    v = [v]
-                # replace key values with node ids
-                u = []
-                link_class =  self.db.classes[propclass.classname]
-                for entry in v:
-                    if entry == '-1': entry = None
-                    elif not num_re.match(entry):
-                        try:
-                            entry = link_class.lookup(entry)
-                        except (TypeError,KeyError):
-                            raise ValueError, 'property "%s": %s not a %s'%(
-                                k, entry, self.properties[k].classname)
-                    u.append(entry)
-
-                l.append((0, k, u))
-            elif isinstance(propclass, Multilink):
-                if type(v) is not type([]):
-                    v = [v]
-                # replace key values with node ids
-                u = []
-                link_class =  self.db.classes[propclass.classname]
-                for entry in v:
-                    if not num_re.match(entry):
-                        try:
-                            entry = link_class.lookup(entry)
-                        except (TypeError,KeyError):
-                            raise ValueError, 'new property "%s": %s not a %s'%(
-                                k, entry, self.properties[k].classname)
-                    u.append(entry)
-                l.append((1, k, u))
-            elif isinstance(propclass, String):
-                # simple glob searching
-                v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
-                v = v.replace('?', '.')
-                v = v.replace('*', '.*?')
-                l.append((2, k, re.compile(v, re.I)))
-            else:
-                l.append((6, k, v))
-        filterspec = l
-
-        # now, find all the nodes that are active and pass filtering
-        l = []
-        cldb = self.db.getclassdb(cn)
-        for nodeid in self.db.getnodeids(cn, cldb):
-            node = self.db.getnode(cn, nodeid, cldb)
-            if node.has_key(self.db.RETIRED_FLAG):
-                continue
-            # apply filter
-            for t, k, v in filterspec:
-                # this node doesn't have this property, so reject it
-                if not node.has_key(k): break
-
-                if t == 0 and node[k] not in v:
-                    # link - if this node'd property doesn't appear in the
-                    # filterspec's nodeid list, skip it
-                    break
-                elif t == 1:
-                    # multilink - if any of the nodeids required by the
-                    # filterspec aren't in this node's property, then skip
-                    # it
-                    for value in v:
-                        if value not in node[k]:
-                            break
-                    else:
-                        continue
-                    break
-                elif t == 2 and (node[k] is None or not v.search(node[k])):
-                    # RE search
-                    break
-                elif t == 6 and node[k] != v:
-                    # straight value comparison for the other types
-                    break
-            else:
-                l.append((nodeid, node))
-        l.sort()
-
-        # filter based on full text search
-        if search_matches is not None:
-            k = []
-            l_debug = []
-            for v in l:
-                l_debug.append(v[0])
-                if search_matches.has_key(v[0]):
-                    k.append(v)
-            l = k
-
-        # optimise sort
-        m = []
-        for entry in sort:
-            if entry[0] != '-':
-                m.append(('+', entry))
-            else:
-                m.append((entry[0], entry[1:]))
-        sort = m
-
-        # optimise group
-        m = []
-        for entry in group:
-            if entry[0] != '-':
-                m.append(('+', entry))
-            else:
-                m.append((entry[0], entry[1:]))
-        group = m
-        # now, sort the result
-        def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
-                db = self.db, cl=self):
-            a_id, an = a
-            b_id, bn = b
-            # sort by group and then sort
-            for list in group, sort:
-                for dir, prop in list:
-                    # sorting is class-specific
-                    propclass = properties[prop]
-
-                    # handle the properties that might be "faked"
-                    # also, handle possible missing properties
-                    try:
-                        if not an.has_key(prop):
-                            an[prop] = cl.get(a_id, prop)
-                        av = an[prop]
-                    except KeyError:
-                        # the node doesn't have a value for this property
-                        if isinstance(propclass, Multilink): av = []
-                        else: av = ''
-                    try:
-                        if not bn.has_key(prop):
-                            bn[prop] = cl.get(b_id, prop)
-                        bv = bn[prop]
-                    except KeyError:
-                        # the node doesn't have a value for this property
-                        if isinstance(propclass, Multilink): bv = []
-                        else: bv = ''
-
-                    # String and Date values are sorted in the natural way
-                    if isinstance(propclass, String):
-                        # clean up the strings
-                        if av and av[0] in string.uppercase:
-                            av = an[prop] = av.lower()
-                        if bv and bv[0] in string.uppercase:
-                            bv = bn[prop] = bv.lower()
-                    if (isinstance(propclass, String) or
-                            isinstance(propclass, Date)):
-                        # it might be a string that's really an integer
-                        try:
-                            av = int(av)
-                            bv = int(bv)
-                        except:
-                            pass
-                        if dir == '+':
-                            r = cmp(av, bv)
-                            if r != 0: return r
-                        elif dir == '-':
-                            r = cmp(bv, av)
-                            if r != 0: return r
-
-                    # Link properties are sorted according to the value of
-                    # the "order" property on the linked nodes if it is
-                    # present; or otherwise on the key string of the linked
-                    # nodes; or finally on  the node ids.
-                    elif isinstance(propclass, Link):
-                        link = db.classes[propclass.classname]
-                        if av is None and bv is not None: return -1
-                        if av is not None and bv is None: return 1
-                        if av is None and bv is None: continue
-                        if link.getprops().has_key('order'):
-                            if dir == '+':
-                                r = cmp(link.get(av, 'order'),
-                                    link.get(bv, 'order'))
-                                if r != 0: return r
-                            elif dir == '-':
-                                r = cmp(link.get(bv, 'order'),
-                                    link.get(av, 'order'))
-                                if r != 0: return r
-                        elif link.getkey():
-                            key = link.getkey()
-                            if dir == '+':
-                                r = cmp(link.get(av, key), link.get(bv, key))
-                                if r != 0: return r
-                            elif dir == '-':
-                                r = cmp(link.get(bv, key), link.get(av, key))
-                                if r != 0: return r
-                        else:
-                            if dir == '+':
-                                r = cmp(av, bv)
-                                if r != 0: return r
-                            elif dir == '-':
-                                r = cmp(bv, av)
-                                if r != 0: return r
-
-                    # Multilink properties are sorted according to how many
-                    # links are present.
-                    elif isinstance(propclass, Multilink):
-                        if dir == '+':
-                            r = cmp(len(av), len(bv))
-                            if r != 0: return r
-                        elif dir == '-':
-                            r = cmp(len(bv), len(av))
-                            if r != 0: return r
-                # end for dir, prop in list:
-            # end for list in sort, group:
-            # if all else fails, compare the ids
-            return cmp(a[0], b[0])
-
-        l.sort(sortfun)
-        return [i[0] for i in l]
+        raise NotImplementedError
 
     def count(self):
         """Get the number of nodes in this class.
@@ -1077,18 +508,15 @@ class Class:
         in this class run from 1 to numnodes, and numnodes+1 will be the
         id of the next node to be created in this class.
         """
-        return self.db.countnodes(self.classname)
+        raise NotImplementedError
 
     # Manipulating properties:
-
     def getprops(self, protected=1):
         """Return a dictionary mapping property names to property objects.
            If the "protected" flag is true, we include protected properties -
-           those which may not be modified."""
-        d = self.properties.copy()
-        if protected:
-            d['id'] = String()
-        return d
+           those which may not be modified.
+        """
+        raise NotImplementedError
 
     def addprop(self, **properties):
         """Add properties to this class.
@@ -1098,10 +526,12 @@ class Class:
         may collide with the names of existing properties, or a ValueError
         is raised before any properties have been added.
         """
-        for key in properties.keys():
-            if self.properties.has_key(key):
-                raise ValueError, key
-        self.properties.update(properties)
+        raise NotImplementedError
+
+    def index(self, nodeid):
+        '''Add (or refresh) the node to search indexes
+        '''
+        raise NotImplementedError
 
 # XXX not in spec
 class Node:
@@ -1161,6 +591,46 @@ def Choice(name, db, *options):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.75  2002/07/14 02:05:53  richard
+# . all storage-specific code (ie. backend) is now implemented by the backends
+#
+# Revision 1.74  2002/07/10 00:24:10  richard
+# braino
+#
+# Revision 1.73  2002/07/10 00:19:48  richard
+# Added explicit closing of backend database handles.
+#
+# Revision 1.72  2002/07/09 21:53:38  gmcm
+# Optimize Class.find so that the propspec can contain a set of ids to match.
+# This is used by indexer.search so it can do just one find for all the index matches.
+# This was already confusing code, but for common terms (lots of index matches),
+# it is enormously faster.
+#
+# Revision 1.71  2002/07/09 03:02:52  richard
+# More indexer work:
+# - all String properties may now be indexed too. Currently there's a bit of
+#   "issue" specific code in the actual searching which needs to be
+#   addressed. In a nutshell:
+#   + pass 'indexme="yes"' as a String() property initialisation arg, eg:
+#         file = FileClass(db, "file", name=String(), type=String(),
+#             comment=String(indexme="yes"))
+#   + the comment will then be indexed and be searchable, with the results
+#     related back to the issue that the file is linked to
+# - as a result of this work, the FileClass has a default MIME type that may
+#   be overridden in a subclass, or by the use of a "type" property as is
+#   done in the default templates.
+# - the regeneration of the indexes (if necessary) is done once the schema is
+#   set up in the dbinit.
+#
+# Revision 1.70  2002/06/27 12:06:20  gmcm
+# Improve an error message.
+#
+# Revision 1.69  2002/06/17 23:15:29  richard
+# Can debug to stdout now
+#
+# Revision 1.68  2002/06/11 06:52:03  richard
+#  . #564271 ] find() and new properties
+#
 # Revision 1.67  2002/06/11 05:02:37  richard
 #  . #565979 ] code error in hyperdb.Class.find
 #