Code

implemented multilink changes (and a unit test)
[roundup.git] / roundup / backends / back_metakit.py
index 9d8d53f20badacd88c53a1b1406e7243cb0c9555..d78b27f9f8034f176cf7c29718d4c42938faad02 100755 (executable)
@@ -1,34 +1,48 @@
-from roundup import hyperdb, date, password, roundupdb
+from roundup import hyperdb, date, password, roundupdb, security
 import metakit
 import metakit
+from sessions import Sessions
 import re, marshal, os, sys, weakref, time, calendar
 import re, marshal, os, sys, weakref, time, calendar
-from roundup.indexer import Indexer
+from roundup import indexer
+import locking
 
 
-_instances = weakref.WeakValueDictionary()
+_dbs = {}
 
 def Database(config, journaltag=None):
 
 def Database(config, journaltag=None):
-    if _instances.has_key(id(config)):
-        db = _instances[id(config)]
-        old = db.journaltag
+    db = _dbs.get(config.DATABASE, None)
+    if db is None or db._db is None:
+        db = _Database(config, journaltag)
+        _dbs[config.DATABASE] = db
+    else:
         db.journaltag = journaltag
         db.journaltag = journaltag
-        if hasattr(db, 'curuserid'):
+        try:
             delattr(db, 'curuserid')
             delattr(db, 'curuserid')
-        return db
-    else:
-        db = _Database(config, journaltag)
-        _instances[id(config)] = db
-        return db
+        except AttributeError:
+            pass
+    return db
 
 class _Database(hyperdb.Database):
     def __init__(self, config, journaltag=None):
         self.config = config
         self.journaltag = journaltag
         self.classes = {}
 
 class _Database(hyperdb.Database):
     def __init__(self, config, journaltag=None):
         self.config = config
         self.journaltag = journaltag
         self.classes = {}
-        self._classes = []
         self.dirty = 0
         self.dirty = 0
-        self.__RW = 0
+        self.lockfile = None
         self._db = self.__open()
         self._db = self.__open()
-        self.indexer = Indexer(self.config.DATABASE)
+        self.indexer = Indexer(self.config.DATABASE, self._db)
+        self.sessions = Sessions(self.config)
+        self.security = security.Security(self)
+
         os.umask(0002)
         os.umask(0002)
+    def post_init(self):
+        if self.indexer.should_reindex():
+            self.reindex()
+
+    def reindex(self):
+        for klass in self.classes.values():
+            for nodeid in klass.list():
+                klass.index(nodeid)
+        self.indexer.save_index()
+        
             
     # --- defined in ping's spec
     def __getattr__(self, classname):
             
     # --- defined in ping's spec
     def __getattr__(self, classname):
@@ -38,6 +52,8 @@ class _Database(hyperdb.Database):
             except KeyError:
                 x = 0
             return x
             except KeyError:
                 x = 0
             return x
+        elif classname == 'transactions':
+            return self.dirty
         return self.getclass(classname)
     def getclass(self, classname):
         return self.classes[classname]
         return self.getclass(classname)
     def getclass(self, classname):
         return self.classes[classname]
@@ -47,12 +63,10 @@ class _Database(hyperdb.Database):
     # --- exposed methods
     def commit(self):
         if self.dirty:
     # --- exposed methods
     def commit(self):
         if self.dirty:
-            if self.__RW:
-                self._db.commit()
-                for cl in self.classes.values():
-                    cl._commit()
-            else:
-                raise RuntimeError, "metakit is open RO"
+            self._db.commit()
+            for cl in self.classes.values():
+                cl._commit()
+            self.indexer.save_index()
         self.dirty = 0
     def rollback(self):
         if self.dirty:
         self.dirty = 0
     def rollback(self):
         if self.dirty:
@@ -64,11 +78,13 @@ class _Database(hyperdb.Database):
         for cl in self.classes.values():
             cl._clear()
     def hasnode(self, classname, nodeid):
         for cl in self.classes.values():
             cl._clear()
     def hasnode(self, classname, nodeid):
-        return self.getclass(clasname).hasnode(nodeid)
+        return self.getclass(classname).hasnode(nodeid)
     def pack(self, pack_before):
         pass
     def addclass(self, cl):
     def pack(self, pack_before):
         pass
     def addclass(self, cl):
-        self.classes[cl.name] = cl
+        self.classes[cl.classname] = cl
+        if self.tables.find(name=cl.classname) < 0:
+            self.tables.append(name=cl.classname)
     def addjournal(self, tablenm, nodeid, action, params):
         tblid = self.tables.find(name=tablenm)
         if tblid == -1:
     def addjournal(self, tablenm, nodeid, action, params):
         tblid = self.tables.find(name=tablenm)
         if tblid == -1:
@@ -101,25 +117,22 @@ class _Database(hyperdb.Database):
         return rslt
             
     def close(self):
         return rslt
             
     def close(self):
-        import time
-        now = time.time
-        start = now()
         for cl in self.classes.values():
             cl.db = None
         for cl in self.classes.values():
             cl.db = None
-        #self._db.rollback()
-        #print "pre-close cleanup of DB(%d) took %2.2f secs" % (self.__RW, now()-start)
         self._db = None
         self._db = None
-        #print "close of DB(%d) took %2.2f secs" % (self.__RW, now()-start)
+        locking.release_lock(self.lockfile)
+        del _dbs[self.config.DATABASE]
+        self.lockfile.close()
         self.classes = {}
         self.classes = {}
-        try:
-            del _instances[id(self.config)]
-        except KeyError:
-            pass
-        self.__RW = 0
-        
+        self.indexer = None
+
     # --- internal
     def __open(self):
         self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
     # --- internal
     def __open(self):
         self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
+        lockfilenm = db[:-3]+'lck'
+        self.lockfile = locking.acquire_lock(lockfilenm)
+        self.lockfile.write(str(os.getpid()))
+        self.lockfile.flush()
         self.fastopen = 0
         if os.path.exists(db):
             dbtm = os.path.getmtime(db)
         self.fastopen = 0
         if os.path.exists(db):
             dbtm = os.path.getmtime(db)
@@ -134,9 +147,7 @@ class _Database(hyperdb.Database):
                 else:
                      # can't find schemamod - must be frozen
                     self.fastopen = 1
                 else:
                      # can't find schemamod - must be frozen
                     self.fastopen = 1
-        else:
-            self.__RW = 1
-        db = metakit.storage(db, self.__RW)
+        db = metakit.storage(db, 1)
         hist = db.view('history')
         tables = db.view('tables')
         if not self.fastopen:
         hist = db.view('history')
         tables = db.view('tables')
         if not self.fastopen:
@@ -147,21 +158,7 @@ class _Database(hyperdb.Database):
         self.tables = tables
         self.hist = hist
         return db
         self.tables = tables
         self.hist = hist
         return db
-    def isReadOnly(self):
-        return self.__RW == 0
-    def getWriteAccess(self):
-        if self.journaltag is not None and self.__RW == 0:
-            now = time.time
-            start = now()
-            self._db = None
-            #print "closing the file took %2.2f secs" % (now()-start)
-            start = now()
-            self._db = metakit.storage(self.dbnm, 1)
-            self.__RW = 1
-            self.hist = self._db.view('history')
-            self.tables = self._db.view('tables')
-            #print "getting RW access took %2.2f secs" % (now()-start)
-    
+        
 _STRINGTYPE = type('')
 _LISTTYPE = type([])
 _CREATE, _SET, _RETIRE, _LINK, _UNLINK = range(5)
 _STRINGTYPE = type('')
 _LISTTYPE = type([])
 _CREATE, _SET, _RETIRE, _LINK, _UNLINK = range(5)
@@ -178,11 +175,12 @@ _marker = []
 
 _ALLOWSETTINGPRIVATEPROPS = 0
 
 
 _ALLOWSETTINGPRIVATEPROPS = 0
 
-class Class:    # no, I'm not going to subclass the existing!
+class Class:    
     privateprops = None
     def __init__(self, db, classname, **properties):
     privateprops = None
     def __init__(self, db, classname, **properties):
-        self.db = weakref.proxy(db)
-        self.name = classname
+        #self.db = weakref.proxy(db)
+        self.db = db
+        self.classname = classname
         self.keyname = None
         self.ruprops = properties
         self.privateprops = { 'id' : hyperdb.String(),
         self.keyname = None
         self.ruprops = properties
         self.privateprops = { 'id' : hyperdb.String(),
@@ -201,6 +199,19 @@ class Class:    # no, I'm not going to subclass the existing!
         self.properties = self.ruprops
         self.db.addclass(self)
         self.idcache = {}
         self.properties = self.ruprops
         self.db.addclass(self)
         self.idcache = {}
+
+        # default is to journal changes
+        self.do_journal = 1
+
+    def enableJournalling(self):
+        '''Turn journalling on for this class
+        '''
+        self.do_journal = 1
+
+    def disableJournalling(self):
+        '''Turn journalling off for this class
+        '''
+        self.do_journal = 0
         
     # --- the roundup.Class methods
     def audit(self, event, detector):
         
     # --- the roundup.Class methods
     def audit(self, event, detector):
@@ -219,6 +230,7 @@ class Class:    # no, I'm not going to subclass the existing!
             self.reactors[event].append(detector)
     # --- the hyperdb.Class methods
     def create(self, **propvalues):
             self.reactors[event].append(detector)
     # --- the hyperdb.Class methods
     def create(self, **propvalues):
+        self.fireAuditors('create', None, propvalues)
         rowdict = {}
         rowdict['id'] = newid = self.maxid
         self.maxid += 1
         rowdict = {}
         rowdict['id'] = newid = self.maxid
         self.maxid += 1
@@ -245,9 +257,12 @@ class Class:    # no, I'm not going to subclass the existing!
         if ndx is None:
             ndx = view.find(id=id)
             if ndx < 0:
         if ndx is None:
             ndx = view.find(id=id)
             if ndx < 0:
-                raise IndexError, "%s has no node %s" % (self.name, nodeid)
+                raise IndexError, "%s has no node %s" % (self.classname, nodeid)
             self.idcache[id] = ndx
             self.idcache[id] = ndx
-        raw = getattr(view[ndx], propname)
+        try:
+            raw = getattr(view[ndx], propname)
+        except AttributeError:
+            raise KeyError, propname
         rutyp = self.ruprops.get(propname, None)
         if rutyp is None:
             rutyp = self.privateprops[propname]
         rutyp = self.ruprops.get(propname, None)
         if rutyp is None:
             rutyp = self.privateprops[propname]
@@ -257,13 +272,14 @@ class Class:    # no, I'm not going to subclass the existing!
         return raw
         
     def set(self, nodeid, **propvalues):
         return raw
         
     def set(self, nodeid, **propvalues):
-        
         isnew = 0
         if propvalues.has_key('#ISNEW'):
             isnew = 1
             del propvalues['#ISNEW']
         isnew = 0
         if propvalues.has_key('#ISNEW'):
             isnew = 1
             del propvalues['#ISNEW']
+        if not isnew:
+            self.fireAuditors('set', nodeid, propvalues)
         if not propvalues:
         if not propvalues:
-            return
+            return propvalues
         if propvalues.has_key('id'):
             raise KeyError, '"id" is reserved'
         if self.db.journaltag is None:
         if propvalues.has_key('id'):
             raise KeyError, '"id" is reserved'
         if self.db.journaltag is None:
@@ -273,10 +289,10 @@ class Class:    # no, I'm not going to subclass the existing!
         id = int(nodeid)
         ndx = view.find(id=id)
         if ndx < 0:
         id = int(nodeid)
         ndx = view.find(id=id)
         if ndx < 0:
-            raise IndexError, "%s has no node %s" % (self.name, nodeid)
+            raise IndexError, "%s has no node %s" % (self.classname, nodeid)
         row = view[ndx]
         if row._isdel:
         row = view[ndx]
         if row._isdel:
-            raise IndexError, "%s has no node %s" % (self.name, nodeid)
+            raise IndexError, "%s has no node %s" % (self.classname, nodeid)
         oldnode = self.uncommitted.setdefault(id, {})
         changes = {}
         
         oldnode = self.uncommitted.setdefault(id, {})
         changes = {}
         
@@ -314,9 +330,14 @@ class Class:    # no, I'm not going to subclass the existing!
             # do stuff based on the prop type
             if isinstance(prop, hyperdb.Link):
                 link_class = prop.classname
             # do stuff based on the prop type
             if isinstance(prop, hyperdb.Link):
                 link_class = prop.classname
+                # must be a string or None
+                if value is not None and not isinstance(value, type('')):
+                    raise ValueError, 'property "%s" link value be a string'%(
+                        propname)
+                # Roundup sets to "unselected" by passing None
+                if value is None:
+                    value = 0   
                 # if it isn't a number, it's a key
                 # if it isn't a number, it's a key
-                if type(value) != _STRINGTYPE:
-                    raise ValueError, 'link value must be String'
                 try:
                     int(value)
                 except ValueError:
                 try:
                     int(value)
                 except ValueError:
@@ -326,20 +347,23 @@ class Class:    # no, I'm not going to subclass the existing!
                         raise IndexError, 'new property "%s": %s not a %s'%(
                             key, value, prop.classname)
 
                         raise IndexError, 'new property "%s": %s not a %s'%(
                             key, value, prop.classname)
 
-                if not self.db.getclass(link_class).hasnode(value):
+                if (value is not None and
+                        not self.db.getclass(link_class).hasnode(value)):
                     raise IndexError, '%s has no node %s'%(link_class, value)
 
                 setattr(row, key, int(value))
                 changes[key] = oldvalue
                 
                     raise IndexError, '%s has no node %s'%(link_class, value)
 
                 setattr(row, key, int(value))
                 changes[key] = oldvalue
                 
-                if prop.do_journal:
+                if self.do_journal and prop.do_journal:
                     # register the unlink with the old linked node
                     if oldvalue:
                     # register the unlink with the old linked node
                     if oldvalue:
-                        self.db.addjournal(link_class, value, _UNLINK, (self.name, str(row.id), key))
+                        self.db.addjournal(link_class, value, _UNLINK,
+                            (self.classname, str(row.id), key))
 
                     # register the link with the newly linked node
                     if value:
 
                     # register the link with the newly linked node
                     if value:
-                        self.db.addjournal(link_class, value, _LINK, (self.name, str(row.id), key))
+                        self.db.addjournal(link_class, value, _LINK,
+                            (self.classname, str(row.id), key))
 
             elif isinstance(prop, hyperdb.Multilink):
                 if type(value) != _LISTTYPE:
 
             elif isinstance(prop, hyperdb.Multilink):
                 if type(value) != _LISTTYPE:
@@ -368,8 +392,8 @@ class Class:    # no, I'm not going to subclass the existing!
                     if id not in value:
                         rmvd.append(id)
                         # register the unlink with the old linked node
                     if id not in value:
                         rmvd.append(id)
                         # register the unlink with the old linked node
-                        if prop.do_journal:
-                            self.db.addjournal(link_class, id, _UNLINK, (self.name, str(row.id), key))
+                        if self.do_journal and prop.do_journal:
+                            self.db.addjournal(link_class, id, _UNLINK, (self.classname, str(row.id), key))
 
                 # handle additions
                 adds = []
 
                 # handle additions
                 adds = []
@@ -380,8 +404,8 @@ class Class:    # no, I'm not going to subclass the existing!
                                 link_class, id)
                         adds.append(id)
                         # register the link with the newly linked node
                                 link_class, id)
                         adds.append(id)
                         # register the link with the newly linked node
-                        if prop.do_journal:
-                            self.db.addjournal(link_class, id, _LINK, (self.name, str(row.id), key))
+                        if self.do_journal and prop.do_journal:
+                            self.db.addjournal(link_class, id, _LINK, (self.classname, str(row.id), key))
                             
                 sv = getattr(row, key)
                 i = 0
                             
                 sv = getattr(row, key)
                 i = 0
@@ -393,6 +417,8 @@ class Class:    # no, I'm not going to subclass the existing!
                 for id in adds:
                     sv.append(fid=int(id))
                 changes[key] = oldvalue
                 for id in adds:
                     sv.append(fid=int(id))
                 changes[key] = oldvalue
+                if not rmvd and not adds:
+                    del propvalues[key]
                     
 
             elif isinstance(prop, hyperdb.String):
                     
 
             elif isinstance(prop, hyperdb.String):
@@ -402,6 +428,8 @@ class Class:    # no, I'm not going to subclass the existing!
                 changes[key] = oldvalue
                 if hasattr(prop, 'isfilename') and prop.isfilename:
                     propvalues[key] = os.path.basename(value)
                 changes[key] = oldvalue
                 if hasattr(prop, 'isfilename') and prop.isfilename:
                     propvalues[key] = os.path.basename(value)
+                if prop.indexme:
+                    self.db.indexer.add_text((self.classname, nodeid, key), value, 'text/plain')
 
             elif isinstance(prop, hyperdb.Password):
                 if not isinstance(value, password.Password):
 
             elif isinstance(prop, hyperdb.Password):
                 if not isinstance(value, password.Password):
@@ -423,13 +451,24 @@ class Class:    # no, I'm not going to subclass the existing!
                 setattr(row, key, str(value))
                 changes[key] = str(oldvalue)
                 propvalues[key] = str(value)
                 setattr(row, key, str(value))
                 changes[key] = str(oldvalue)
                 propvalues[key] = str(value)
+                
+            elif value is not None and isinstance(prop, hyperdb.Number):
+                setattr(row, key, int(value))
+                changes[key] = oldvalue
+                propvalues[key] = value
+                
+            elif value is not None and isinstance(prop, hyperdb.Boolean):
+                bv = value != 0
+                setattr(row, key, bv)
+                changes[key] = oldvalue
+                propvalues[key] = value
 
             oldnode[key] = oldvalue
 
         # nothing to do?
         if not propvalues:
 
             oldnode[key] = oldvalue
 
         # nothing to do?
         if not propvalues:
-            return
-        if not row.activity:
+            return propvalues
+        if not propvalues.has_key('activity'):
             row.activity = int(time.time())
         if isnew:
             if not row.creation:
             row.activity = int(time.time())
         if isnew:
             if not row.creation:
@@ -438,12 +477,18 @@ class Class:    # no, I'm not going to subclass the existing!
                 row.creator = self.db.curuserid
             
         self.db.dirty = 1
                 row.creator = self.db.curuserid
             
         self.db.dirty = 1
-        if isnew:
-            self.db.addjournal(self.name, nodeid, _CREATE, {})
-        else:
-            self.db.addjournal(self.name, nodeid, _SET, changes)
+        if self.do_journal:
+            if isnew:
+                self.db.addjournal(self.classname, nodeid, _CREATE, {})
+                self.fireReactors('create', nodeid, None)
+            else:
+                self.db.addjournal(self.classname, nodeid, _SET, changes)
+                self.fireReactors('set', nodeid, oldnode)
 
 
+        return propvalues
+    
     def retire(self, nodeid):
     def retire(self, nodeid):
+        self.fireAuditors('retire', nodeid, None)
         view = self.getview(1)
         ndx = view.find(id=int(nodeid))
         if ndx < 0:
         view = self.getview(1)
         ndx = view.find(id=int(nodeid))
         if ndx < 0:
@@ -452,31 +497,37 @@ class Class:    # no, I'm not going to subclass the existing!
         oldvalues = self.uncommitted.setdefault(row.id, {})
         oldval = oldvalues['_isdel'] = row._isdel
         row._isdel = 1
         oldvalues = self.uncommitted.setdefault(row.id, {})
         oldval = oldvalues['_isdel'] = row._isdel
         row._isdel = 1
-        self.db.addjournal(self.name, nodeid, _RETIRE, {})
-        iv = self.getindexview(1)
-        ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
-        if ndx > -1:
-            iv.delete(ndx)
+        if self.do_journal:
+            self.db.addjournal(self.classname, nodeid, _RETIRE, {})
+        if self.keyname:
+            iv = self.getindexview(1)
+            ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
+            if ndx > -1:
+                iv.delete(ndx)
         self.db.dirty = 1
         self.db.dirty = 1
+        self.fireReactors('retire', nodeid, None)
     def history(self, nodeid):
     def history(self, nodeid):
-        return self.db.gethistory(self.name, nodeid)
+        if not self.do_journal:
+            raise ValueError, 'Journalling is disabled for this class'
+        return self.db.gethistory(self.classname, nodeid)
     def setkey(self, propname):
         if self.keyname:
             if propname == self.keyname:
                 return
     def setkey(self, propname):
         if self.keyname:
             if propname == self.keyname:
                 return
-            raise ValueError, "%s already indexed on %s" % (self.name, self.keyname)
+            raise ValueError, "%s already indexed on %s" % (self.classname, self.keyname)
         # first setkey for this run
         self.keyname = propname
         # first setkey for this run
         self.keyname = propname
-        iv = self.db._db.view('_%s' % self.name)
-        if self.db.fastopen or iv.structure():
+        iv = self.db._db.view('_%s' % self.classname)
+        if self.db.fastopen and iv.structure():
             return
         # very first setkey ever
             return
         # very first setkey ever
-        iv = self.db._db.getas('_%s[k:S,i:I]' % self.name)
+        self.db.dirty = 1
+        iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
         iv = iv.ordered(1)
         iv = iv.ordered(1)
-        #XXX
-        print "setkey building index"
+#        print "setkey building index"
         for row in self.getview():
             iv.append(k=getattr(row, propname), i=row.id)
         for row in self.getview():
             iv.append(k=getattr(row, propname), i=row.id)
+        self.db.commit()
     def getkey(self):
         return self.keyname
     def lookup(self, keyvalue):
     def getkey(self):
         return self.keyname
     def lookup(self, keyvalue):
@@ -493,27 +544,48 @@ class Class:    # no, I'm not going to subclass the existing!
             if ndx > -1:
                 return str(view[ndx].id)
         raise KeyError, keyvalue
             if ndx > -1:
                 return str(view[ndx].id)
         raise KeyError, keyvalue
+
+    def destroy(self, keyvalue):
+        #TODO clean this up once Richard's said how it should work
+        iv = self.getindexview()
+        if iv:
+            ndx = iv.find(k=keyvalue)
+            if ndx > -1:
+                id = iv[ndx].i
+                iv.delete(ndx)
+                view = self.getview()
+                ndx = view.find(id=id)
+                if ndx > -1:
+                    view.delete(ndx)
+
     def find(self, **propspec):
         """Get the ids of nodes in this class which link to the given nodes.
 
         'propspec' consists of keyword args propname={nodeid:1,}   
     def find(self, **propspec):
         """Get the ids of nodes in this class which link to the given nodes.
 
         '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.
+        '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.
+
         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
         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:
+        that "foo" occurs in msg1, msg3 and file7; so we have hits on these
+        issues:
+
             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
+
         """
         propspec = propspec.items()
         for propname, nodeid in propspec:
             # check the prop is OK
             prop = self.ruprops[propname]
         """
         propspec = propspec.items()
         for propname, nodeid in propspec:
             # check the prop is OK
             prop = self.ruprops[propname]
-            if not isinstance(prop, hyperdb.Link) and not isinstance(prop, hyperdb.Multilink):
+            if (not isinstance(prop, hyperdb.Link) and
+                    not isinstance(prop, hyperdb.Multilink)):
                 raise TypeError, "'%s' not a Link/Multilink property"%propname
 
         vws = []
         for propname, ids in propspec:
                 raise TypeError, "'%s' not a Link/Multilink property"%propname
 
         vws = []
         for propname, ids in propspec:
+            if type(ids) is _STRINGTYPE:
+                ids = {ids:1}
             prop = self.ruprops[propname]
             view = self.getview()
             if isinstance(prop, hyperdb.Multilink):
             prop = self.ruprops[propname]
             view = self.getview()
             if isinstance(prop, hyperdb.Multilink):
@@ -525,6 +597,11 @@ class Class:    # no, I'm not going to subclass the existing!
                     return ids.has_key(str(getattr(row, nm)))
             ndxview = view.filter(ff)
             vws.append(ndxview.unique())
                     return ids.has_key(str(getattr(row, nm)))
             ndxview = view.filter(ff)
             vws.append(ndxview.unique())
+
+        # handle the empty match case
+        if not vws:
+            return []
+
         ndxview = vws[0]
         for v in vws[1:]:
             ndxview = ndxview.union(v)
         ndxview = vws[0]
         for v in vws[1:]:
             ndxview = ndxview.union(v)
@@ -551,9 +628,11 @@ class Class:    # no, I'm not going to subclass the existing!
     def addprop(self, **properties):
         for key in properties.keys():
             if self.ruprops.has_key(key):
     def addprop(self, **properties):
         for key in properties.keys():
             if self.ruprops.has_key(key):
-                raise ValueError, "%s is already a property of %s" % (key, self.name)
+                raise ValueError, "%s is already a property of %s" % (key, self.classname)
         self.ruprops.update(properties)
         self.ruprops.update(properties)
+        self.db.fastopen = 0
         view = self.__getview()
         view = self.__getview()
+        self.db.commit()
     # ---- end of ping's spec
     def filter(self, search_matches, filterspec, sort, group):
         # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
     # ---- end of ping's spec
     def filter(self, search_matches, filterspec, sort, group):
         # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
@@ -607,6 +686,14 @@ class Class:    # no, I'm not going to subclass the existing!
                 regexes[propname] = re.compile(v, re.I)
             elif propname == 'id':
                 where[propname] = int(value)
                 regexes[propname] = re.compile(v, re.I)
             elif propname == 'id':
                 where[propname] = int(value)
+            elif isinstance(prop, hyperdb.Boolean):
+                if type(value) is _STRINGTYPE:
+                    bv = value.lower() in ('yes', 'true', 'on', '1')
+                else:
+                    bv = value
+                where[propname] = bv
+            elif isinstance(prop, hyperdb.Number):
+                where[propname] = int(value)
             else:
                 where[propname] = str(value)
         v = self.getview()
             else:
                 where[propname] = str(value)
         v = self.getview()
@@ -666,12 +753,28 @@ class Class:    # no, I'm not going to subclass the existing!
                 try:
                     prop = getattr(v, propname)
                 except AttributeError:
                 try:
                     prop = getattr(v, propname)
                 except AttributeError:
-                    # I can't sort on 'activity', cause it's psuedo!!
+                    print "MK has no property %s" % propname
                     continue
                     continue
+                propclass = self.ruprops.get(propname, None)
+                if propclass is None:
+                    propclass = self.privateprops.get(propname, None)
+                    if propclass is None:
+                        print "Schema has no property %s" % propname
+                        continue
+                if isinstance(propclass, hyperdb.Link):
+                    linkclass = self.db.getclass(propclass.classname)
+                    lv = linkclass.getview()
+                    lv = lv.rename('id', propname)
+                    v = v.join(lv, prop, 1)
+                    if linkclass.getprops().has_key('order'):
+                        propname = 'order'
+                    else:
+                        propname = linkclass.labelprop()
+                    prop = getattr(v, propname)
                 if isreversed:
                     rev.append(prop)
                 sortspec.append(prop)
                 if isreversed:
                     rev.append(prop)
                 sortspec.append(prop)
-            v = v.sortrev(sortspec, rev)[:] #XXX Aaaabh
+            v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
         #print "filter sort   at %s" % time.time() 
             
         rslt = []
         #print "filter sort   at %s" % time.time() 
             
         rslt = []
@@ -731,7 +834,17 @@ class Class:    # no, I'm not going to subclass the existing!
         return l
 
     def addjournal(self, nodeid, action, params):
         return l
 
     def addjournal(self, nodeid, action, params):
-        self.db.addjournal(self.name, nodeid, action, params)
+        self.db.addjournal(self.classname, nodeid, action, params)
+
+    def index(self, nodeid):
+        ''' Add (or refresh) the node to search indexes '''
+        # find all the String properties that have indexme
+        for prop, propclass in self.getprops().items():
+            if isinstance(propclass, hyperdb.String) and propclass.indexme:
+                # index them under (classname, nodeid, property)
+                self.db.indexer.add_text((self.classname, nodeid, prop),
+                                str(self.get(nodeid, prop)))
+
     # --- used by Database
     def _commit(self):
         """ called post commit of the DB.
     # --- used by Database
     def _commit(self):
         """ called post commit of the DB.
@@ -762,40 +875,40 @@ class Class:    # no, I'm not going to subclass the existing!
     # --- internal
     def __getview(self):
         db = self.db._db
     # --- internal
     def __getview(self):
         db = self.db._db
-        view = db.view(self.name)
-        if self.db.fastopen:
+        view = db.view(self.classname)
+        mkprops = view.structure()
+        if mkprops and self.db.fastopen:
             return view.ordered(1)
         # is the definition the same?
         for nm, rutyp in self.ruprops.items():
             return view.ordered(1)
         # is the definition the same?
         for nm, rutyp in self.ruprops.items():
-            mkprop = getattr(view, nm, None)
+            for mkprop in mkprops:
+                if mkprop.name == nm:
+                    break
+            else:
+                mkprop = None
             if mkprop is None:
             if mkprop is None:
-                #print "%s missing prop %s (%s)" % (self.name, nm, rutyp.__class__.__name__)
                 break
             if _typmap[rutyp.__class__] != mkprop.type:
                 break
             if _typmap[rutyp.__class__] != mkprop.type:
-                #print "%s - prop %s (%s) has wrong mktyp (%s)" % (self.name, nm, rutyp.__class__.__name__, mkprop.type)
                 break
         else:
             return view.ordered(1)
         # need to create or restructure the mk view
         # id comes first, so MK will order it for us
         self.db.dirty = 1
                 break
         else:
             return view.ordered(1)
         # need to create or restructure the mk view
         # id comes first, so MK will order it for us
         self.db.dirty = 1
-        s = ["%s[id:I" % self.name]
+        s = ["%s[id:I" % self.classname]
         for nm, rutyp in self.ruprops.items():
             mktyp = _typmap[rutyp.__class__]
             s.append('%s:%s' % (nm, mktyp))
             if mktyp == 'V':
                 s[-1] += ('[fid:I]')
         s.append('_isdel:I,activity:I,creation:I,creator:I]')
         for nm, rutyp in self.ruprops.items():
             mktyp = _typmap[rutyp.__class__]
             s.append('%s:%s' % (nm, mktyp))
             if mktyp == 'V':
                 s[-1] += ('[fid:I]')
         s.append('_isdel:I,activity:I,creation:I,creator:I]')
-        v = db.getas(','.join(s))
+        v = self.db._db.getas(','.join(s))
+        self.db.commit()
         return v.ordered(1)
     def getview(self, RW=0):
         return v.ordered(1)
     def getview(self, RW=0):
-        if RW and self.db.isReadOnly():
-            self.db.getWriteAccess()
-        return self.db._db.view(self.name).ordered(1)
+        return self.db._db.view(self.classname).ordered(1)
     def getindexview(self, RW=0):
     def getindexview(self, RW=0):
-        if RW and self.db.isReadOnly():
-            self.db.getWriteAccess()
-        return self.db._db.view("_%s" % self.name).ordered(1)
+        return self.db._db.view("_%s" % self.classname).ordered(1)
     
 def _fetchML(sv):
     l = []
     
 def _fetchML(sv):
     l = []
@@ -821,6 +934,9 @@ _converters = {
     hyperdb.Multilink : _fetchML,
     hyperdb.Interval  : date.Interval,
     hyperdb.Password  : _fetchPW,
     hyperdb.Multilink : _fetchML,
     hyperdb.Interval  : date.Interval,
     hyperdb.Password  : _fetchPW,
+    hyperdb.Boolean   : lambda n: n,
+    hyperdb.Number    : lambda n: n,
+    hyperdb.String    : str,
 }                
 
 class FileName(hyperdb.String):
 }                
 
 class FileName(hyperdb.String):
@@ -834,11 +950,16 @@ _typmap = {
     hyperdb.Multilink : 'V',
     hyperdb.Interval  : 'S',
     hyperdb.Password  : 'S',
     hyperdb.Multilink : 'V',
     hyperdb.Interval  : 'S',
     hyperdb.Password  : 'S',
+    hyperdb.Boolean   : 'I',
+    hyperdb.Number    : 'I',
 }
 class FileClass(Class):
     ' like Class but with a content property '
 }
 class FileClass(Class):
     ' like Class but with a content property '
+    default_mime_type = 'text/plain'
     def __init__(self, db, classname, **properties):
         properties['content'] = FileName()
     def __init__(self, db, classname, **properties):
         properties['content'] = FileName()
+        if not properties.has_key('type'):
+            properties['type'] = hyperdb.String()
         Class.__init__(self, db, classname, **properties)
     def get(self, nodeid, propname, default=_marker, cache=1):
         x = Class.get(self, nodeid, propname, default, cache)
         Class.__init__(self, db, classname, **properties)
     def get(self, nodeid, propname, default=_marker, cache=1):
         x = Class.get(self, nodeid, propname, default, cache)
@@ -856,37 +977,37 @@ class FileClass(Class):
         newid = Class.create(self, **propvalues)
         if not content:
             return newid
         newid = Class.create(self, **propvalues)
         if not content:
             return newid
-        if content.startswith('/tracker/download.php?'):
-            self.set(newid, content='http://sourceforge.net'+content)
-            return newid
-        nm = bnm = '%s%s' % (self.name, newid)
+        nm = bnm = '%s%s' % (self.classname, newid)
         sd = str(int(int(newid) / 1000))
         sd = str(int(int(newid) / 1000))
-        d = os.path.join(self.db.config.DATABASE, 'files', self.name, sd)
+        d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
         if not os.path.exists(d):
             os.makedirs(d)
         nm = os.path.join(d, nm)
         open(nm, 'wb').write(content)
         self.set(newid, content = 'file:'+nm)
         if not os.path.exists(d):
             os.makedirs(d)
         nm = os.path.join(d, nm)
         open(nm, 'wb').write(content)
         self.set(newid, content = 'file:'+nm)
-        self.db.indexer.add_files(d, bnm)
-        self.db.indexer.save_index()
+        mimetype = propvalues.get('type', self.default_mime_type)
+        self.db.indexer.add_text((self.classname, newid, 'content'), content, mimetype)
         def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
         def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
-            remove(fnm)
-            indexer.purge_entry(fnm, indexer.files, indexer.words)
+            action1(fnm)
         self.rollbackaction(undo)
         return newid
         self.rollbackaction(undo)
         return newid
-
-# Yuck - c&p to avoid getting hyperdb.Class
-class IssueClass(Class):
-
+    def index(self, nodeid):
+        Class.index(self, nodeid)
+        mimetype = self.get(nodeid, 'type')
+        if not mimetype:
+            mimetype = self.default_mime_type
+        self.db.indexer.add_text((self.classname, nodeid, 'content'),
+                    self.get(nodeid, 'content'), mimetype)
+class IssueClass(Class, roundupdb.IssueClass):
     # Overridden methods:
     # Overridden methods:
-
     def __init__(self, db, classname, **properties):
         """The newly-created class automatically includes the "messages",
         "files", "nosy", and "superseder" properties.  If the 'properties'
         dictionary attempts to specify any of these properties or a
         "creation" or "activity" property, a ValueError is raised."""
         if not properties.has_key('title'):
     def __init__(self, db, classname, **properties):
         """The newly-created class automatically includes the "messages",
         "files", "nosy", and "superseder" properties.  If the 'properties'
         dictionary attempts to specify any of these properties or a
         "creation" or "activity" property, a ValueError is raised."""
         if not properties.has_key('title'):
-            properties['title'] = hyperdb.String()
+            properties['title'] = hyperdb.String(indexme='yes')
         if not properties.has_key('messages'):
             properties['messages'] = hyperdb.Multilink("msg")
         if not properties.has_key('files'):
         if not properties.has_key('messages'):
             properties['messages'] = hyperdb.Multilink("msg")
         if not properties.has_key('files'):
@@ -896,348 +1017,108 @@ class IssueClass(Class):
         if not properties.has_key('superseder'):
             properties['superseder'] = hyperdb.Multilink(classname)
         Class.__init__(self, db, classname, **properties)
         if not properties.has_key('superseder'):
             properties['superseder'] = hyperdb.Multilink(classname)
         Class.__init__(self, db, classname, **properties)
-
-    # New methods:
-
-    def addmessage(self, nodeid, summary, text):
-        """Add a message to an issue's mail spool.
-
-        A new "msg" node is constructed using the current date, the user that
-        owns the database connection as the author, and the specified summary
-        text.
-
-        The "files" and "recipients" fields are left empty.
-
-        The given text is saved as the body of the message and the node is
-        appended to the "messages" field of the specified issue.
-        """
-
-    def nosymessage(self, nodeid, msgid, oldvalues):
-        """Send a message to the members of an issue's nosy list.
-
-        The message is sent only to users on the nosy list who are not
-        already on the "recipients" list for the message.
         
         
-        These users are then added to the message's "recipients" list.
-        """
-        users = self.db.user
-        messages = self.db.msg
-
-        # figure the recipient ids
-        sendto = []
-        r = {}
-        recipients = messages.get(msgid, 'recipients')
-        for recipid in messages.get(msgid, 'recipients'):
-            r[recipid] = 1
-
-        # figure the author's id, and indicate they've received the message
-        authid = messages.get(msgid, 'author')
-
-        # possibly send the message to the author, as long as they aren't
-        # anonymous
-        if (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and
-                users.get(authid, 'username') != 'anonymous'):
-            sendto.append(authid)
-        r[authid] = 1
-
-        # now figure the nosy people who weren't recipients
-        nosy = self.get(nodeid, 'nosy')
-        for nosyid in nosy:
-            # Don't send nosy mail to the anonymous user (that user
-            # shouldn't appear in the nosy list, but just in case they
-            # do...)
-            if users.get(nosyid, 'username') == 'anonymous':
-                continue
-            # make sure they haven't seen the message already
-            if not r.has_key(nosyid):
-                # send it to them
-                sendto.append(nosyid)
-                recipients.append(nosyid)
-
-        # generate a change note
-        if oldvalues:
-            note = self.generateChangeNote(nodeid, oldvalues)
-        else:
-            note = self.generateCreateNote(nodeid)
-
-        # we have new recipients
-        if sendto:
-            # map userids to addresses
-            sendto = [users.get(i, 'address') for i in sendto]
-
-            # update the message's recipients list
-            messages.set(msgid, recipients=recipients)
-
-            # send the message
-            self.send_message(nodeid, msgid, note, sendto)
-
-    # XXX backwards compatibility - don't remove
-    sendmessage = nosymessage
-
-    def send_message(self, nodeid, msgid, note, sendto):
-        '''Actually send the nominated message from this node to the sendto
-           recipients, with the note appended.
-        '''
-        users = self.db.user
-        messages = self.db.msg
-        files = self.db.file
-
-        # determine the messageid and inreplyto of the message
-        inreplyto = messages.get(msgid, 'inreplyto')
-        messageid = messages.get(msgid, 'messageid')
-
-        # make up a messageid if there isn't one (web edit)
-        if not messageid:
-            # this is an old message that didn't get a messageid, so
-            # create one
-            messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
-                self.classname, nodeid, self.db.config.MAIL_DOMAIN)
-            messages.set(msgid, messageid=messageid)
-
-        # send an email to the people who missed out
-        cn = self.classname
-        title = self.get(nodeid, 'title') or '%s message copy'%cn
-        # figure author information
-        authid = messages.get(msgid, 'author')
-        authname = users.get(authid, 'realname')
-        if not authname:
-            authname = users.get(authid, 'username')
-        authaddr = users.get(authid, 'address')
-        if authaddr:
-            authaddr = ' <%s>'%authaddr
-        else:
-            authaddr = ''
-
-        # make the message body
-        m = ['']
-
-        # put in roundup's signature
-        if self.db.config.EMAIL_SIGNATURE_POSITION == 'top':
-            m.append(self.email_signature(nodeid, msgid))
-
-        # add author information
-        if len(self.get(nodeid,'messages')) == 1:
-            m.append("New submission from %s%s:"%(authname, authaddr))
-        else:
-            m.append("%s%s added the comment:"%(authname, authaddr))
-        m.append('')
-
-        # add the content
-        m.append(messages.get(msgid, 'content'))
-
-        # add the change note
-        if note:
-            m.append(note)
-
-        # put in roundup's signature
-        if self.db.config.EMAIL_SIGNATURE_POSITION == 'bottom':
-            m.append(self.email_signature(nodeid, msgid))
-
-        # encode the content as quoted-printable
-        content = cStringIO.StringIO('\n'.join(m))
-        content_encoded = cStringIO.StringIO()
-        quopri.encode(content, content_encoded, 0)
-        content_encoded = content_encoded.getvalue()
-
-        # get the files for this message
-        message_files = messages.get(msgid, 'files')
-
-        # make sure the To line is always the same (for testing mostly)
-        sendto.sort()
-
-        # create the message
-        message = cStringIO.StringIO()
-        writer = MimeWriter.MimeWriter(message)
-        writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title))
-        writer.addheader('To', ', '.join(sendto))
-        writer.addheader('From', '%s <%s>'%(authname,
-            self.db.config.ISSUE_TRACKER_EMAIL))
-        writer.addheader('Reply-To', '%s <%s>'%(self.db.config.INSTANCE_NAME,
-            self.db.config.ISSUE_TRACKER_EMAIL))
-        writer.addheader('MIME-Version', '1.0')
-        if messageid:
-            writer.addheader('Message-Id', messageid)
-        if inreplyto:
-            writer.addheader('In-Reply-To', inreplyto)
-
-        # add a uniquely Roundup header to help filtering
-        writer.addheader('X-Roundup-Name', self.db.config.INSTANCE_NAME)
-
-        # attach files
-        if message_files:
-            part = writer.startmultipartbody('mixed')
-            part = writer.nextpart()
-            part.addheader('Content-Transfer-Encoding', 'quoted-printable')
-            body = part.startbody('text/plain')
-            body.write(content_encoded)
-            for fileid in message_files:
-                name = files.get(fileid, 'name')
-                mime_type = files.get(fileid, 'type')
-                content = files.get(fileid, 'content')
-                part = writer.nextpart()
-                if mime_type == 'text/plain':
-                    part.addheader('Content-Disposition',
-                        'attachment;\n filename="%s"'%name)
-                    part.addheader('Content-Transfer-Encoding', '7bit')
-                    body = part.startbody('text/plain')
-                    body.write(content)
-                else:
-                    # some other type, so encode it
-                    if not mime_type:
-                        # this should have been done when the file was saved
-                        mime_type = mimetypes.guess_type(name)[0]
-                    if mime_type is None:
-                        mime_type = 'application/octet-stream'
-                    part.addheader('Content-Disposition',
-                        'attachment;\n filename="%s"'%name)
-                    part.addheader('Content-Transfer-Encoding', 'base64')
-                    body = part.startbody(mime_type)
-                    body.write(base64.encodestring(content))
-            writer.lastpart()
-        else:
-            writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
-            body = writer.startbody('text/plain')
-            body.write(content_encoded)
-
-        # now try to send the message
-        if SENDMAILDEBUG:
-            open(SENDMAILDEBUG, 'w').write('FROM: %s\nTO: %s\n%s\n'%(
-                self.db.config.ADMIN_EMAIL,
-                ', '.join(sendto),message.getvalue()))
-        else:
-            try:
-                # send the message as admin so bounces are sent there
-                # instead of to roundup
-                smtp = smtplib.SMTP(self.db.config.MAILHOST)
-                smtp.sendmail(self.db.config.ADMIN_EMAIL, sendto,
-                    message.getvalue())
-            except socket.error, value:
-                raise MessageSendError, \
-                    "Couldn't send confirmation email: mailhost %s"%value
-            except smtplib.SMTPException, value:
-                raise MessageSendError, \
-                    "Couldn't send confirmation email: %s"%value
-
-    def email_signature(self, nodeid, msgid):
-        ''' Add a signature to the e-mail with some useful information
-        '''
-        web = self.db.config.ISSUE_TRACKER_WEB + 'issue'+ nodeid
-        email = '"%s" <%s>'%(self.db.config.INSTANCE_NAME,
-            self.db.config.ISSUE_TRACKER_EMAIL)
-        line = '_' * max(len(web), len(email))
-        return '%s\n%s\n%s\n%s'%(line, email, web, line)
-
-    def generateCreateNote(self, nodeid):
-        """Generate a create note that lists initial property values
-        """
-        cn = self.classname
-        cl = self.db.classes[cn]
-        props = cl.getprops(protected=0)
-
-        # list the values
-        m = []
-        l = props.items()
-        l.sort()
-        for propname, prop in l:
-            value = cl.get(nodeid, propname, None)
-            # skip boring entries
-            if not value:
+CURVERSION = 1
+
+class Indexer(indexer.Indexer):
+    disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
+    def __init__(self, path, datadb):
+        self.db = metakit.storage(os.path.join(path, 'index.mk4'), 1)
+        self.datadb = datadb
+        self.reindex = 0
+        v = self.db.view('version')
+        if not v.structure():
+            v = self.db.getas('version[vers:I]')
+            self.db.commit()
+            v.append(vers=CURVERSION)
+            self.reindex = 1
+        elif v[0].vers != CURVERSION:
+            v[0].vers = CURVERSION
+            self.reindex = 1
+        if self.reindex:
+            self.db.getas('ids[tblid:I,nodeid:I,propid:I]')
+            self.db.getas('index[word:S,hits[pos:I]]')
+            self.db.commit()
+            self.reindex = 1
+        self.changed = 0
+        self.propcache = {}
+    def force_reindex(self):
+        v = self.db.view('ids')
+        v[:] = []
+        v = self.db.view('index')
+        v[:] = []
+        self.db.commit()
+        self.reindex = 1
+    def should_reindex(self):
+        return self.reindex
+    def _getprops(self, classname):
+        props = self.propcache.get(classname, None)
+        if props is None:
+            props = self.datadb.view(classname).structure()
+            props = [prop.name for prop in props]
+            self.propcache[classname] = props
+        return props
+    def _getpropid(self, classname, propname):
+        return self._getprops(classname).index(propname)
+    def _getpropname(self, classname, propid):
+        return self._getprops(classname)[propid]
+    def add_text(self, identifier, text, mime_type='text/plain'):
+        if mime_type != 'text/plain':
+            return
+        classname, nodeid, property = identifier
+        tbls = self.datadb.view('tables')
+        tblid = tbls.find(name=classname)
+        if tblid < 0:
+            raise KeyError, "unknown class %r"%classname
+        nodeid = int(nodeid)
+        propid = self._getpropid(classname, property)
+        pos = self.db.view('ids').append(tblid=tblid,nodeid=nodeid,propid=propid)
+        
+        wordlist = re.findall(r'\b\w{3,25}\b', text)
+        words = {}
+        for word in wordlist:
+           word = word.upper()
+           if not self.disallows.has_key(word):
+               words[word] = 1
+        words = words.keys()
+        
+        index = self.db.view('index').ordered(1)
+        for word in words:
+            ndx = index.find(word=word)
+            if ndx < 0:
+                ndx = index.append(word=word)
+            hits = index[ndx].hits
+            if len(hits)==0 or hits.find(pos=pos) < 0:
+                hits.append(pos=pos)
+                self.changed = 1
+    def find(self, wordlist):
+        hits = None
+        index = self.db.view('index').ordered(1)
+        for word in wordlist:
+            if not 2 < len(word) < 26:
                 continue
                 continue
-            if isinstance(prop, hyperdb.Link):
-                link = self.db.classes[prop.classname]
-                if value:
-                    key = link.labelprop(default_to_id=1)
-                    if key:
-                        value = link.get(value, key)
-                else:
-                    value = ''
-            elif isinstance(prop, hyperdb.Multilink):
-                if value is None: value = []
-                l = []
-                link = self.db.classes[prop.classname]
-                key = link.labelprop(default_to_id=1)
-                if key:
-                    value = [link.get(entry, key) for entry in value]
-                value.sort()
-                value = ', '.join(value)
-            m.append('%s: %s'%(propname, value))
-        m.insert(0, '----------')
-        m.insert(0, '')
-        return '\n'.join(m)
-
-    def generateChangeNote(self, nodeid, oldvalues):
-        """Generate a change note that lists property changes
-        """
-        cn = self.classname
-        cl = self.db.classes[cn]
-        changed = {}
-        props = cl.getprops(protected=0)
-
-        # determine what changed
-        for key in oldvalues.keys():
-            if key in ['files','messages']: continue
-            new_value = cl.get(nodeid, key)
-            # the old value might be non existent
-            try:
-                old_value = oldvalues[key]
-                if type(new_value) is type([]):
-                    new_value.sort()
-                    old_value.sort()
-                if new_value != old_value:
-                    changed[key] = old_value
-            except:
-                changed[key] = new_value
-
-        # list the changes
-        m = []
-        l = changed.items()
-        l.sort()
-        for propname, oldvalue in l:
-            prop = props[propname]
-            value = cl.get(nodeid, propname, None)
-            if isinstance(prop, hyperdb.Link):
-                link = self.db.classes[prop.classname]
-                key = link.labelprop(default_to_id=1)
-                if key:
-                    if value:
-                        value = link.get(value, key)
-                    else:
-                        value = ''
-                    if oldvalue:
-                        oldvalue = link.get(oldvalue, key)
-                    else:
-                        oldvalue = ''
-                change = '%s -> %s'%(oldvalue, value)
-            elif isinstance(prop, hyperdb.Multilink):
-                change = ''
-                if value is None: value = []
-                if oldvalue is None: oldvalue = []
-                l = []
-                link = self.db.classes[prop.classname]
-                key = link.labelprop(default_to_id=1)
-                # check for additions
-                for entry in value:
-                    if entry in oldvalue: continue
-                    if key:
-                        l.append(link.get(entry, key))
-                    else:
-                        l.append(entry)
-                if l:
-                    change = '+%s'%(', '.join(l))
-                    l = []
-                # check for removals
-                for entry in oldvalue:
-                    if entry in value: continue
-                    if key:
-                        l.append(link.get(entry, key))
-                    else:
-                        l.append(entry)
-                if l:
-                    change += ' -%s'%(', '.join(l))
+            ndx = index.find(word=word)
+            if ndx < 0:
+                return {}
+            if hits is None:
+                hits = index[ndx].hits
             else:
             else:
-                change = '%s -> %s'%(oldvalue, value)
-            m.append('%s: %s'%(propname, change))
-        if m:
-            m.insert(0, '----------')
-            m.insert(0, '')
-        return '\n'.join(m)
+                hits = hits.intersect(index[ndx].hits)
+            if len(hits) == 0:
+                return {}
+        if hits is None:
+            return {}
+        rslt = {}
+        ids = self.db.view('ids').remapwith(hits)
+        tbls = self.datadb.view('tables')
+        for i in range(len(ids)):
+            hit = ids[i]
+            classname = tbls[hit.tblid].name
+            nodeid = str(hit.nodeid)
+            property = self._getpropname(classname, hit.propid)
+            rslt[i] = (classname, nodeid, property)
+        return rslt
+    def save_index(self):
+        if self.changed:
+            self.db.commit()
+        self.changed = 0