Code

extended date syntax to make range searches even more useful
[roundup.git] / roundup / backends / back_metakit.py
1 # $Id: back_metakit.py,v 1.46 2003-04-20 11:58:45 kedder Exp $
2 '''
3    Metakit backend for Roundup, originally by Gordon McMillan.
5    Notes by Richard:
7    This backend has some behaviour specific to metakit:
9     - there's no concept of an explicit "unset" in metakit, so all types
10       have some "unset" value:
12       ========= ===== ====================================================
13       Type      Value Action when fetching from mk
14       ========= ===== ====================================================
15       Strings   ''    convert to None
16       Date      0     (seconds since 1970-01-01.00:00:00) convert to None
17       Interval  ''    convert to None
18       Number    0     ambiguious :( - do nothing
19       Boolean   0     ambiguious :( - do nothing
20       Link      0     convert to None
21       Multilink []    actually, mk can handle this one ;)
22       Passowrd  ''    convert to None
23       ========= ===== ====================================================
25       The get/set routines handle these values accordingly by converting
26       to/from None where they can. The Number/Boolean types are not able
27       to handle an "unset" at all, so they default the "unset" to 0.
29     - probably a bunch of stuff that I'm not aware of yet because I haven't
30       fully read through the source. One of these days....
31 '''
32 from roundup import hyperdb, date, password, roundupdb, security
33 import metakit
34 from sessions import Sessions, OneTimeKeys
35 import re, marshal, os, sys, weakref, time, calendar
36 from roundup import indexer
37 import locking
38 from roundup.date import Range
40 _dbs = {}
42 def Database(config, journaltag=None):
43     ''' Only have a single instance of the Database class for each instance
44     '''
45     db = _dbs.get(config.DATABASE, None)
46     if db is None or db._db is None:
47         db = _Database(config, journaltag)
48         _dbs[config.DATABASE] = db
49     else:
50         db.journaltag = journaltag
51         try:
52             delattr(db, 'curuserid')
53         except AttributeError:
54             pass
55     return db
57 class _Database(hyperdb.Database, roundupdb.Database):
58     def __init__(self, config, journaltag=None):
59         self.config = config
60         self.journaltag = journaltag
61         self.classes = {}
62         self.dirty = 0
63         self.lockfile = None
64         self._db = self.__open()
65         self.indexer = Indexer(self.config.DATABASE, self._db)
66         self.sessions = Sessions(self.config)
67         self.otks = OneTimeKeys(self.config)
68         self.security = security.Security(self)
70         os.umask(0002)
72     def post_init(self):
73         if self.indexer.should_reindex():
74             self.reindex()
76     def reindex(self):
77         for klass in self.classes.values():
78             for nodeid in klass.list():
79                 klass.index(nodeid)
80         self.indexer.save_index()
82     # --- defined in ping's spec
83     def __getattr__(self, classname):
84         if classname == 'curuserid':
85             if self.journaltag is None:
86                 return None
88             # try to set the curuserid from the journaltag
89             try:
90                 x = int(self.classes['user'].lookup(self.journaltag))
91                 self.curuserid = x
92             except KeyError:
93                 if self.journaltag == 'admin':
94                     self.curuserid = x = 1
95                 else:
96                     x = 0
97             return x
98         elif classname == 'transactions':
99             return self.dirty
100         # fall back on the classes
101         return self.getclass(classname)
102     def getclass(self, classname):
103         try:
104             return self.classes[classname]
105         except KeyError:
106             raise KeyError, 'There is no class called "%s"'%classname
107     def getclasses(self):
108         return self.classes.keys()
109     # --- end of ping's spec 
111     # --- exposed methods
112     def commit(self):
113         if self.dirty:
114             self._db.commit()
115             for cl in self.classes.values():
116                 cl._commit()
117             self.indexer.save_index()
118         self.dirty = 0
119     def rollback(self):
120         if self.dirty:
121             for cl in self.classes.values():
122                 cl._rollback()
123             self._db.rollback()
124             self._db = None
125             self._db = metakit.storage(self.dbnm, 1)
126             self.hist = self._db.view('history')
127             self.tables = self._db.view('tables')
128             self.indexer.rollback()
129             self.indexer.datadb = self._db
130         self.dirty = 0
131     def clearCache(self):
132         for cl in self.classes.values():
133             cl._commit()
134     def clear(self):
135         for cl in self.classes.values():
136             cl._clear()
137     def hasnode(self, classname, nodeid):
138         return self.getclass(classname).hasnode(nodeid)
139     def pack(self, pack_before):
140         mindate = int(calendar.timegm(pack_before.get_tuple()))
141         i = 0
142         while i < len(self.hist):
143             if self.hist[i].date < mindate and self.hist[i].action != _CREATE:
144                 self.hist.delete(i)
145             else:
146                 i = i + 1
147     def addclass(self, cl):
148         self.classes[cl.classname] = cl
149         if self.tables.find(name=cl.classname) < 0:
150             self.tables.append(name=cl.classname)
151     def addjournal(self, tablenm, nodeid, action, params, creator=None,
152                 creation=None):
153         tblid = self.tables.find(name=tablenm)
154         if tblid == -1:
155             tblid = self.tables.append(name=tablenm)
156         if creator is None:
157             creator = self.curuserid
158         else:
159             try:
160                 creator = int(creator)
161             except TypeError:
162                 creator = int(self.getclass('user').lookup(creator))
163         if creation is None:
164             creation = int(time.time())
165         elif isinstance(creation, date.Date):
166             creation = int(calendar.timegm(creation.get_tuple()))
167         # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
168         self.hist.append(tableid=tblid,
169                          nodeid=int(nodeid),
170                          date=creation,
171                          action=action,
172                          user = creator,
173                          params = marshal.dumps(params))
174     def getjournal(self, tablenm, nodeid):
175         rslt = []
176         tblid = self.tables.find(name=tablenm)
177         if tblid == -1:
178             return rslt
179         q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
180         if len(q) == 0:
181             raise IndexError, "no history for id %s in %s" % (nodeid, tablenm)
182         i = 0
183         #userclass = self.getclass('user')
184         for row in q:
185             try:
186                 params = marshal.loads(row.params)
187             except ValueError:
188                 print "history couldn't unmarshal %r" % row.params
189                 params = {}
190             #usernm = userclass.get(str(row.user), 'username')
191             dt = date.Date(time.gmtime(row.date))
192             #rslt.append((nodeid, dt, usernm, _actionnames[row.action], params))
193             rslt.append((nodeid, dt, str(row.user), _actionnames[row.action],
194                 params))
195         return rslt
197     def destroyjournal(self, tablenm, nodeid):
198         nodeid = int(nodeid)
199         tblid = self.tables.find(name=tablenm)
200         if tblid == -1:
201             return 
202         i = 0
203         hist = self.hist
204         while i < len(hist):
205             if hist[i].tableid == tblid and hist[i].nodeid == nodeid:
206                 hist.delete(i)
207             else:
208                 i = i + 1
209         self.dirty = 1
210         
211     def close(self):
212         for cl in self.classes.values():
213             cl.db = None
214         self._db = None
215         if self.lockfile is not None:
216             locking.release_lock(self.lockfile)
217         if _dbs.has_key(self.config.DATABASE):
218             del _dbs[self.config.DATABASE]
219         if self.lockfile is not None:
220             self.lockfile.close()
221             self.lockfile = None
222         self.classes = {}
223         self.indexer = None
225     # --- internal
226     def __open(self):
227         ''' Open the metakit database
228         '''
229         # make the database dir if it doesn't exist
230         if not os.path.exists(self.config.DATABASE):
231             os.makedirs(self.config.DATABASE)
233         # figure the file names
234         self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
235         lockfilenm = db[:-3]+'lck'
237         # get the database lock
238         self.lockfile = locking.acquire_lock(lockfilenm)
239         self.lockfile.write(str(os.getpid()))
240         self.lockfile.flush()
242         # see if the schema has changed since last db access
243         self.fastopen = 0
244         if os.path.exists(db):
245             dbtm = os.path.getmtime(db)
246             pkgnm = self.config.__name__.split('.')[0]
247             schemamod = sys.modules.get(pkgnm+'.dbinit', None)
248             if schemamod:
249                 if os.path.exists(schemamod.__file__):
250                     schematm = os.path.getmtime(schemamod.__file__)
251                     if schematm < dbtm:
252                         # found schema mod - it's older than the db
253                         self.fastopen = 1
254                 else:
255                      # can't find schemamod - must be frozen
256                     self.fastopen = 1
258         # open the db
259         db = metakit.storage(db, 1)
260         hist = db.view('history')
261         tables = db.view('tables')
262         if not self.fastopen:
263             # create the database if it's brand new
264             if not hist.structure():
265                 hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
266             if not tables.structure():
267                 tables = db.getas('tables[name:S]')
268             db.commit()
270         # we now have an open, initialised database
271         self.tables = tables
272         self.hist = hist
273         return db
275     def setid(self, classname, maxid):
276         ''' No-op in metakit
277         '''
278         pass
279         
280 _STRINGTYPE = type('')
281 _LISTTYPE = type([])
282 _CREATE, _SET, _RETIRE, _LINK, _UNLINK, _RESTORE = range(6)
284 _actionnames = {
285     _CREATE : 'create',
286     _SET : 'set',
287     _RETIRE : 'retire',
288     _RESTORE : 'restore',
289     _LINK : 'link',
290     _UNLINK : 'unlink',
293 _marker = []
295 _ALLOWSETTINGPRIVATEPROPS = 0
297 class Class:    
298     privateprops = None
299     def __init__(self, db, classname, **properties):
300         #self.db = weakref.proxy(db)
301         self.db = db
302         self.classname = classname
303         self.keyname = None
304         self.ruprops = properties
305         self.privateprops = { 'id' : hyperdb.String(),
306                               'activity' : hyperdb.Date(),
307                               'creation' : hyperdb.Date(),
308                               'creator'  : hyperdb.Link('user') }
310         # event -> list of callables
311         self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
312         self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
314         view = self.__getview()
315         self.maxid = 1
316         if view:
317             self.maxid = view[-1].id + 1
318         self.uncommitted = {}
319         self.rbactions = []
321         # people reach inside!!
322         self.properties = self.ruprops
323         self.db.addclass(self)
324         self.idcache = {}
326         # default is to journal changes
327         self.do_journal = 1
329     def enableJournalling(self):
330         '''Turn journalling on for this class
331         '''
332         self.do_journal = 1
334     def disableJournalling(self):
335         '''Turn journalling off for this class
336         '''
337         self.do_journal = 0
338         
339     # --- the roundup.Class methods
340     def audit(self, event, detector):
341         l = self.auditors[event]
342         if detector not in l:
343             self.auditors[event].append(detector)
344     def fireAuditors(self, action, nodeid, newvalues):
345         for audit in self.auditors[action]:
346             audit(self.db, self, nodeid, newvalues)
347     def fireReactors(self, action, nodeid, oldvalues):
348         for react in self.reactors[action]:
349             react(self.db, self, nodeid, oldvalues)
350     def react(self, event, detector):
351         l = self.reactors[event]
352         if detector not in l:
353             self.reactors[event].append(detector)
355     # --- the hyperdb.Class methods
356     def create(self, **propvalues):
357         self.fireAuditors('create', None, propvalues)
358         newid = self.create_inner(**propvalues)
359         # self.set() (called in self.create_inner()) does reactors)
360         return newid
362     def create_inner(self, **propvalues):
363         rowdict = {}
364         rowdict['id'] = newid = self.maxid
365         self.maxid += 1
366         ndx = self.getview(1).append(rowdict)
367         propvalues['#ISNEW'] = 1
368         try:
369             self.set(str(newid), **propvalues)
370         except Exception:
371             self.maxid -= 1
372             raise
373         return str(newid)
374     
375     def get(self, nodeid, propname, default=_marker, cache=1):
376         # default and cache aren't in the spec
377         # cache=0 means "original value"
379         view = self.getview()        
380         id = int(nodeid)
381         if cache == 0:
382             oldnode = self.uncommitted.get(id, None)
383             if oldnode and oldnode.has_key(propname):
384                 return oldnode[propname]
385         ndx = self.idcache.get(id, None)
386         if ndx is None:
387             ndx = view.find(id=id)
388             if ndx < 0:
389                 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
390             self.idcache[id] = ndx
391         try:
392             raw = getattr(view[ndx], propname)
393         except AttributeError:
394             raise KeyError, propname
395         rutyp = self.ruprops.get(propname, None)
396         if rutyp is None:
397             rutyp = self.privateprops[propname]
398         converter = _converters.get(rutyp.__class__, None)
399         if converter:
400             raw = converter(raw)
401         return raw
402         
403     def set(self, nodeid, **propvalues):
404         isnew = 0
405         if propvalues.has_key('#ISNEW'):
406             isnew = 1
407             del propvalues['#ISNEW']
408         if not isnew:
409             self.fireAuditors('set', nodeid, propvalues)
410         if not propvalues:
411             return propvalues
412         if propvalues.has_key('id'):
413             raise KeyError, '"id" is reserved'
414         if self.db.journaltag is None:
415             raise hyperdb.DatabaseError, 'Database open read-only'
416         view = self.getview(1)
418         # node must exist & not be retired
419         id = int(nodeid)
420         ndx = view.find(id=id)
421         if ndx < 0:
422             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
423         row = view[ndx]
424         if row._isdel:
425             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
426         oldnode = self.uncommitted.setdefault(id, {})
427         changes = {}
429         for key, value in propvalues.items():
430             # this will raise the KeyError if the property isn't valid
431             # ... we don't use getprops() here because we only care about
432             # the writeable properties.
433             if _ALLOWSETTINGPRIVATEPROPS:
434                 prop = self.ruprops.get(key, None)
435                 if not prop:
436                     prop = self.privateprops[key]
437             else:
438                 prop = self.ruprops[key]
439             converter = _converters.get(prop.__class__, lambda v: v)
440             # if the value's the same as the existing value, no sense in
441             # doing anything
442             oldvalue = converter(getattr(row, key))
443             if  value == oldvalue:
444                 del propvalues[key]
445                 continue
446             
447             # check to make sure we're not duplicating an existing key
448             if key == self.keyname:
449                 iv = self.getindexview(1)
450                 ndx = iv.find(k=value)
451                 if ndx == -1:
452                     iv.append(k=value, i=row.id)
453                     if not isnew:
454                         ndx = iv.find(k=oldvalue)
455                         if ndx > -1:
456                             iv.delete(ndx)
457                 else:
458                     raise ValueError, 'node with key "%s" exists'%value
460             # do stuff based on the prop type
461             if isinstance(prop, hyperdb.Link):
462                 link_class = prop.classname
463                 # must be a string or None
464                 if value is not None and not isinstance(value, type('')):
465                     raise ValueError, 'property "%s" link value be a string'%(
466                         key)
467                 # Roundup sets to "unselected" by passing None
468                 if value is None:
469                     value = 0   
470                 # if it isn't a number, it's a key
471                 try:
472                     int(value)
473                 except ValueError:
474                     try:
475                         value = self.db.getclass(link_class).lookup(value)
476                     except (TypeError, KeyError):
477                         raise IndexError, 'new property "%s": %s not a %s'%(
478                             key, value, prop.classname)
480                 if (value is not None and
481                         not self.db.getclass(link_class).hasnode(value)):
482                     raise IndexError, '%s has no node %s'%(link_class, value)
484                 setattr(row, key, int(value))
485                 changes[key] = oldvalue
486                 
487                 if self.do_journal and prop.do_journal:
488                     # register the unlink with the old linked node
489                     if oldvalue:
490                         self.db.addjournal(link_class, oldvalue, _UNLINK,
491                             (self.classname, str(row.id), key))
493                     # register the link with the newly linked node
494                     if value:
495                         self.db.addjournal(link_class, value, _LINK,
496                             (self.classname, str(row.id), key))
498             elif isinstance(prop, hyperdb.Multilink):
499                 if value is not None and type(value) != _LISTTYPE:
500                     raise TypeError, 'new property "%s" not a list of ids'%key
501                 link_class = prop.classname
502                 l = []
503                 if value is None:
504                     value = []
505                 for entry in value:
506                     if type(entry) != _STRINGTYPE:
507                         raise ValueError, 'new property "%s" link value ' \
508                             'must be a string'%key
509                     # if it isn't a number, it's a key
510                     try:
511                         int(entry)
512                     except ValueError:
513                         try:
514                             entry = self.db.getclass(link_class).lookup(entry)
515                         except (TypeError, KeyError):
516                             raise IndexError, 'new property "%s": %s not a %s'%(
517                                 key, entry, prop.classname)
518                     l.append(entry)
519                 propvalues[key] = value = l
521                 # handle removals
522                 rmvd = []
523                 for id in oldvalue:
524                     if id not in value:
525                         rmvd.append(id)
526                         # register the unlink with the old linked node
527                         if self.do_journal and prop.do_journal:
528                             self.db.addjournal(link_class, id, _UNLINK,
529                                 (self.classname, str(row.id), key))
531                 # handle additions
532                 adds = []
533                 for id in value:
534                     if id not in oldvalue:
535                         if not self.db.getclass(link_class).hasnode(id):
536                             raise IndexError, '%s has no node %s'%(
537                                 link_class, id)
538                         adds.append(id)
539                         # register the link with the newly linked node
540                         if self.do_journal and prop.do_journal:
541                             self.db.addjournal(link_class, id, _LINK,
542                                 (self.classname, str(row.id), key))
544                 # perform the modifications on the actual property value
545                 sv = getattr(row, key)
546                 i = 0
547                 while i < len(sv):
548                     if str(sv[i].fid) in rmvd:
549                         sv.delete(i)
550                     else:
551                         i += 1
552                 for id in adds:
553                     sv.append(fid=int(id))
555                 # figure the journal entry
556                 l = []
557                 if adds:
558                     l.append(('+', adds))
559                 if rmvd:
560                     l.append(('-', rmvd))
561                 if l:
562                     changes[key] = tuple(l)
563                 #changes[key] = oldvalue
565                 if not rmvd and not adds:
566                     del propvalues[key]
568             elif isinstance(prop, hyperdb.String):
569                 if value is not None and type(value) != _STRINGTYPE:
570                     raise TypeError, 'new property "%s" not a string'%key
571                 if value is None:
572                     value = ''
573                 setattr(row, key, value)
574                 changes[key] = oldvalue
575                 if hasattr(prop, 'isfilename') and prop.isfilename:
576                     propvalues[key] = os.path.basename(value)
577                 if prop.indexme:
578                     self.db.indexer.add_text((self.classname, nodeid, key),
579                         value, 'text/plain')
581             elif isinstance(prop, hyperdb.Password):
582                 if value is not None and not isinstance(value, password.Password):
583                     raise TypeError, 'new property "%s" not a Password'% key
584                 if value is None:
585                     value = ''
586                 setattr(row, key, str(value))
587                 changes[key] = str(oldvalue)
588                 propvalues[key] = str(value)
590             elif isinstance(prop, hyperdb.Date):
591                 if value is not None and not isinstance(value, date.Date):
592                     raise TypeError, 'new property "%s" not a Date'% key
593                 if value is None:
594                     setattr(row, key, 0)
595                 else:
596                     setattr(row, key, int(calendar.timegm(value.get_tuple())))
597                 changes[key] = str(oldvalue)
598                 propvalues[key] = str(value)
600             elif isinstance(prop, hyperdb.Interval):
601                 if value is not None and not isinstance(value, date.Interval):
602                     raise TypeError, 'new property "%s" not an Interval'% key
603                 if value is None:
604                     setattr(row, key, '')
605                 else:
606                     # kedder: we should store interval values serialized
607                     setattr(row, key, value.serialise())
608                 changes[key] = str(oldvalue)
609                 propvalues[key] = str(value)
610  
611             elif isinstance(prop, hyperdb.Number):
612                 if value is None:
613                     value = 0
614                 try:
615                     v = int(value)
616                 except ValueError:
617                     raise TypeError, "%s (%s) is not numeric"%(key, repr(value))
618                 setattr(row, key, v)
619                 changes[key] = oldvalue
620                 propvalues[key] = value
622             elif isinstance(prop, hyperdb.Boolean):
623                 if value is None:
624                     bv = 0
625                 elif value not in (0,1):
626                     raise TypeError, "%s (%s) is not boolean"%(key, repr(value))
627                 else:
628                     bv = value 
629                 setattr(row, key, bv)
630                 changes[key] = oldvalue
631                 propvalues[key] = value
633             oldnode[key] = oldvalue
635         # nothing to do?
636         if not propvalues:
637             return propvalues
638         if not propvalues.has_key('activity'):
639             row.activity = int(time.time())
640         if isnew:
641             if not row.creation:
642                 row.creation = int(time.time())
643             if not row.creator:
644                 row.creator = self.db.curuserid
646         self.db.dirty = 1
647         if self.do_journal:
648             if isnew:
649                 self.db.addjournal(self.classname, nodeid, _CREATE, {})
650                 self.fireReactors('create', nodeid, None)
651             else:
652                 self.db.addjournal(self.classname, nodeid, _SET, changes)
653                 self.fireReactors('set', nodeid, oldnode)
655         return propvalues
656     
657     def retire(self, nodeid):
658         if self.db.journaltag is None:
659             raise hyperdb.DatabaseError, 'Database open read-only'
660         self.fireAuditors('retire', nodeid, None)
661         view = self.getview(1)
662         ndx = view.find(id=int(nodeid))
663         if ndx < 0:
664             raise KeyError, "nodeid %s not found" % nodeid
666         row = view[ndx]
667         oldvalues = self.uncommitted.setdefault(row.id, {})
668         oldval = oldvalues['_isdel'] = row._isdel
669         row._isdel = 1
671         if self.do_journal:
672             self.db.addjournal(self.classname, nodeid, _RETIRE, {})
673         if self.keyname:
674             iv = self.getindexview(1)
675             ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
676             if ndx > -1:
677                 iv.delete(ndx)
678         self.db.dirty = 1
679         self.fireReactors('retire', nodeid, None)
681     def restore(self, nodeid):
682         '''Restpre a retired node.
684         Make node available for all operations like it was before retirement.
685         '''
686         if self.db.journaltag is None:
687             raise hyperdb.DatabaseError, 'Database open read-only'
689         # check if key property was overrided
690         key = self.getkey()
691         keyvalue = self.get(nodeid, key)
692         try:
693             id = self.lookup(keyvalue)
694         except KeyError:
695             pass
696         else:
697             raise KeyError, "Key property (%s) of retired node clashes with \
698                 existing one (%s)" % (key, keyvalue)
699         # Now we can safely restore node
700         self.fireAuditors('restore', nodeid, None)
701         view = self.getview(1)
702         ndx = view.find(id=int(nodeid))
703         if ndx < 0:
704             raise KeyError, "nodeid %s not found" % nodeid
706         row = view[ndx]
707         oldvalues = self.uncommitted.setdefault(row.id, {})
708         oldval = oldvalues['_isdel'] = row._isdel
709         row._isdel = 0
711         if self.do_journal:
712             self.db.addjournal(self.classname, nodeid, _RESTORE, {})
713         if self.keyname:
714             iv = self.getindexview(1)
715             ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
716             if ndx > -1:
717                 iv.delete(ndx)
718         self.db.dirty = 1
719         self.fireReactors('restore', nodeid, None)
721     def is_retired(self, nodeid):
722         view = self.getview(1)
723         # node must exist & not be retired
724         id = int(nodeid)
725         ndx = view.find(id=id)
726         if ndx < 0:
727             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
728         row = view[ndx]
729         return row._isdel
731     def history(self, nodeid):
732         if not self.do_journal:
733             raise ValueError, 'Journalling is disabled for this class'
734         return self.db.getjournal(self.classname, nodeid)
736     def setkey(self, propname):
737         if self.keyname:
738             if propname == self.keyname:
739                 return
740             raise ValueError, "%s already indexed on %s"%(self.classname,
741                 self.keyname)
742         prop = self.properties.get(propname, None)
743         if prop is None:
744             prop = self.privateprops.get(propname, None)
745         if prop is None:
746             raise KeyError, "no property %s" % propname
747         if not isinstance(prop, hyperdb.String):
748             raise TypeError, "%s is not a String" % propname
750         # first setkey for this run
751         self.keyname = propname
752         iv = self.db._db.view('_%s' % self.classname)
753         if self.db.fastopen and iv.structure():
754             return
756         # very first setkey ever
757         self.db.dirty = 1
758         iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
759         iv = iv.ordered(1)
760         for row in self.getview():
761             iv.append(k=getattr(row, propname), i=row.id)
762         self.db.commit()
764     def getkey(self):
765         return self.keyname
767     def lookup(self, keyvalue):
768         if type(keyvalue) is not _STRINGTYPE:
769             raise TypeError, "%r is not a string" % keyvalue
770         iv = self.getindexview()
771         if iv:
772             ndx = iv.find(k=keyvalue)
773             if ndx > -1:
774                 return str(iv[ndx].i)
775         else:
776             view = self.getview()
777             ndx = view.find({self.keyname:keyvalue, '_isdel':0})
778             if ndx > -1:
779                 return str(view[ndx].id)
780         raise KeyError, keyvalue
782     def destroy(self, id):
783         view = self.getview(1)
784         ndx = view.find(id=int(id))
785         if ndx > -1:
786             if self.keyname:
787                 keyvalue = getattr(view[ndx], self.keyname)
788                 iv = self.getindexview(1)
789                 if iv:
790                     ivndx = iv.find(k=keyvalue)
791                     if ivndx > -1:
792                         iv.delete(ivndx)
793             view.delete(ndx)
794             self.db.destroyjournal(self.classname, id)
795             self.db.dirty = 1
796         
797     def find(self, **propspec):
798         """Get the ids of nodes in this class which link to the given nodes.
800         'propspec' consists of keyword args propname={nodeid:1,}   
801         'propname' must be the name of a property in this class, or a
802                    KeyError is raised.  That property must be a Link or
803                    Multilink property, or a TypeError is raised.
805         Any node in this class whose propname property links to any of the
806         nodeids will be returned. Used by the full text indexing, which knows
807         that "foo" occurs in msg1, msg3 and file7; so we have hits on these
808         issues:
810             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
812         """
813         propspec = propspec.items()
814         for propname, nodeid in propspec:
815             # check the prop is OK
816             prop = self.ruprops[propname]
817             if (not isinstance(prop, hyperdb.Link) and
818                     not isinstance(prop, hyperdb.Multilink)):
819                 raise TypeError, "'%s' not a Link/Multilink property"%propname
821         vws = []
822         for propname, ids in propspec:
823             if type(ids) is _STRINGTYPE:
824                 ids = {int(ids):1}
825             elif ids is None:
826                 ids = {0:1}
827             else:
828                 d = {}
829                 for id in ids.keys():
830                     d[int(id)] = 1
831                 ids = d
832             prop = self.ruprops[propname]
833             view = self.getview()
834             if isinstance(prop, hyperdb.Multilink):
835                 def ff(row, nm=propname, ids=ids):
836                     sv = getattr(row, nm)
837                     for sr in sv:
838                         if ids.has_key(sr.fid):
839                             return 1
840                     return 0
841             else:
842                 def ff(row, nm=propname, ids=ids):
843                     return ids.has_key(getattr(row, nm))
844             ndxview = view.filter(ff)
845             vws.append(ndxview.unique())
847         # handle the empty match case
848         if not vws:
849             return []
851         ndxview = vws[0]
852         for v in vws[1:]:
853             ndxview = ndxview.union(v)
854         view = self.getview().remapwith(ndxview)
855         rslt = []
856         for row in view:
857             rslt.append(str(row.id))
858         return rslt
859             
861     def list(self):
862         l = []
863         for row in self.getview().select(_isdel=0):
864             l.append(str(row.id))
865         return l
867     def getnodeids(self):
868         l = []
869         for row in self.getview():
870             l.append(str(row.id))
871         return l
873     def count(self):
874         return len(self.getview())
876     def getprops(self, protected=1):
877         # protected is not in ping's spec
878         allprops = self.ruprops.copy()
879         if protected and self.privateprops is not None:
880             allprops.update(self.privateprops)
881         return allprops
883     def addprop(self, **properties):
884         for key in properties.keys():
885             if self.ruprops.has_key(key):
886                 raise ValueError, "%s is already a property of %s"%(key,
887                     self.classname)
888         self.ruprops.update(properties)
889         # Class structure has changed
890         self.db.fastopen = 0
891         view = self.__getview()
892         self.db.commit()
893     # ---- end of ping's spec
895     def filter(self, search_matches, filterspec, sort=(None,None),
896             group=(None,None)):
897         # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
898         # filterspec is a dict {propname:value}
899         # sort and group are (dir, prop) where dir is '+', '-' or None
900         #                    and prop is a prop name or None
902         timezone = self.db.getUserTimezone()
904         where = {'_isdel':0}
905         wherehigh = {}
906         mlcriteria = {}
907         regexes = {}
908         orcriteria = {}
909         for propname, value in filterspec.items():
910             prop = self.ruprops.get(propname, None)
911             if prop is None:
912                 prop = self.privateprops[propname]
913             if isinstance(prop, hyperdb.Multilink):
914                 if value in ('-1', ['-1']):
915                     value = []
916                 elif type(value) is not _LISTTYPE:
917                     value = [value]
918                 # transform keys to ids
919                 u = []
920                 for item in value:
921                     try:
922                         item = int(item)
923                     except (TypeError, ValueError):
924                         item = int(self.db.getclass(prop.classname).lookup(item))
925                     if item == -1:
926                         item = 0
927                     u.append(item)
928                 mlcriteria[propname] = u
929             elif isinstance(prop, hyperdb.Link):
930                 if type(value) is not _LISTTYPE:
931                     value = [value]
932                 # transform keys to ids
933                 u = []
934                 for item in value:
935                     try:
936                         item = int(item)
937                     except (TypeError, ValueError):
938                         item = int(self.db.getclass(prop.classname).lookup(item))
939                     if item == -1:
940                         item = 0
941                     u.append(item)
942                 if len(u) == 1:
943                     where[propname] = u[0]
944                 else:
945                     orcriteria[propname] = u
946             elif isinstance(prop, hyperdb.String):
947                 # simple glob searching
948                 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', value)
949                 v = v.replace('?', '.')
950                 v = v.replace('*', '.*?')
951                 regexes[propname] = re.compile(v, re.I)
952             elif propname == 'id':
953                 where[propname] = int(value)
954             elif isinstance(prop, hyperdb.Boolean):
955                 if type(value) is _STRINGTYPE:
956                     bv = value.lower() in ('yes', 'true', 'on', '1')
957                 else:
958                     bv = value
959                 where[propname] = bv
960             elif isinstance(prop, hyperdb.Date):
961                 try:
962                     # Try to filter on range of dates
963                     date_rng = Range(value, date.Date, offset=timezone)
964                     if date_rng.from_value:
965                         t = date_rng.from_value.get_tuple()
966                         where[propname] = int(calendar.timegm(t))
967                     else:
968                         # use minimum possible value to exclude items without
969                         # 'prop' property
970                         where[propname] = 0
971                     if date_rng.to_value:
972                         t = date_rng.to_value.get_tuple()
973                         wherehigh[propname] = int(calendar.timegm(t))
974                     else:
975                         wherehigh[propname] = None
976                 except ValueError:
977                     # If range creation fails - ignore that search parameter
978                     pass                        
979             elif isinstance(prop, hyperdb.Interval):
980                 try:
981                     # Try to filter on range of intervals
982                     date_rng = Range(value, date.Interval)
983                     if date_rng.from_value:
984                         #t = date_rng.from_value.get_tuple()
985                         where[propname] = date_rng.from_value.serialise()
986                     else:
987                         # use minimum possible value to exclude items without
988                         # 'prop' property
989                         where[propname] = '-99999999999999'
990                     if date_rng.to_value:
991                         #t = date_rng.to_value.get_tuple()
992                         wherehigh[propname] = date_rng.to_value.serialise()
993                     else:
994                         wherehigh[propname] = None
995                 except ValueError:
996                     # If range creation fails - ignore that search parameter
997                     pass                        
998             elif isinstance(prop, hyperdb.Number):
999                 where[propname] = int(value)
1000             else:
1001                 where[propname] = str(value)
1002         v = self.getview()
1003         #print "filter start at  %s" % time.time() 
1004         if where:
1005             where_higherbound = where.copy()
1006             where_higherbound.update(wherehigh)
1007             v = v.select(where, where_higherbound)
1008         #print "filter where at  %s" % time.time() 
1010         if mlcriteria:
1011             # multilink - if any of the nodeids required by the
1012             # filterspec aren't in this node's property, then skip it
1013             def ff(row, ml=mlcriteria):
1014                 for propname, values in ml.items():
1015                     sv = getattr(row, propname)
1016                     if not values and sv:
1017                         return 0
1018                     for id in values:
1019                         if sv.find(fid=id) == -1:
1020                             return 0
1021                 return 1
1022             iv = v.filter(ff)
1023             v = v.remapwith(iv)
1025         #print "filter mlcrit at %s" % time.time() 
1026         
1027         if orcriteria:
1028             def ff(row, crit=orcriteria):
1029                 for propname, allowed in crit.items():
1030                     val = getattr(row, propname)
1031                     if val not in allowed:
1032                         return 0
1033                 return 1
1034             
1035             iv = v.filter(ff)
1036             v = v.remapwith(iv)
1037         
1038         #print "filter orcrit at %s" % time.time() 
1039         if regexes:
1040             def ff(row, r=regexes):
1041                 for propname, regex in r.items():
1042                     val = str(getattr(row, propname))
1043                     if not regex.search(val):
1044                         return 0
1045                 return 1
1046             
1047             iv = v.filter(ff)
1048             v = v.remapwith(iv)
1049         #print "filter regexs at %s" % time.time() 
1050         
1051         if sort or group:
1052             sortspec = []
1053             rev = []
1054             for dir, propname in group, sort:
1055                 if propname is None: continue
1056                 isreversed = 0
1057                 if dir == '-':
1058                     isreversed = 1
1059                 try:
1060                     prop = getattr(v, propname)
1061                 except AttributeError:
1062                     print "MK has no property %s" % propname
1063                     continue
1064                 propclass = self.ruprops.get(propname, None)
1065                 if propclass is None:
1066                     propclass = self.privateprops.get(propname, None)
1067                     if propclass is None:
1068                         print "Schema has no property %s" % propname
1069                         continue
1070                 if isinstance(propclass, hyperdb.Link):
1071                     linkclass = self.db.getclass(propclass.classname)
1072                     lv = linkclass.getview()
1073                     lv = lv.rename('id', propname)
1074                     v = v.join(lv, prop, 1)
1075                     if linkclass.getprops().has_key('order'):
1076                         propname = 'order'
1077                     else:
1078                         propname = linkclass.labelprop()
1079                     prop = getattr(v, propname)
1080                 if isreversed:
1081                     rev.append(prop)
1082                 sortspec.append(prop)
1083             v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
1084         #print "filter sort   at %s" % time.time() 
1085             
1086         rslt = []
1087         for row in v:
1088             id = str(row.id)
1089             if search_matches is not None:
1090                 if search_matches.has_key(id):
1091                     rslt.append(id)
1092             else:
1093                 rslt.append(id)
1094         return rslt
1095     
1096     def hasnode(self, nodeid):
1097         return int(nodeid) < self.maxid
1098     
1099     def labelprop(self, default_to_id=0):
1100         ''' Return the property name for a label for the given node.
1102         This method attempts to generate a consistent label for the node.
1103         It tries the following in order:
1104             1. key property
1105             2. "name" property
1106             3. "title" property
1107             4. first property from the sorted property name list
1108         '''
1109         k = self.getkey()
1110         if  k:
1111             return k
1112         props = self.getprops()
1113         if props.has_key('name'):
1114             return 'name'
1115         elif props.has_key('title'):
1116             return 'title'
1117         if default_to_id:
1118             return 'id'
1119         props = props.keys()
1120         props.sort()
1121         return props[0]
1123     def stringFind(self, **requirements):
1124         """Locate a particular node by matching a set of its String
1125         properties in a caseless search.
1127         If the property is not a String property, a TypeError is raised.
1128         
1129         The return is a list of the id of all nodes that match.
1130         """
1131         for propname in requirements.keys():
1132             prop = self.properties[propname]
1133             if isinstance(not prop, hyperdb.String):
1134                 raise TypeError, "'%s' not a String property"%propname
1135             requirements[propname] = requirements[propname].lower()
1136         requirements['_isdel'] = 0
1137         
1138         l = []
1139         for row in self.getview().select(requirements):
1140             l.append(str(row.id))
1141         return l
1143     def addjournal(self, nodeid, action, params):
1144         self.db.addjournal(self.classname, nodeid, action, params)
1146     def index(self, nodeid):
1147         ''' Add (or refresh) the node to search indexes '''
1148         # find all the String properties that have indexme
1149         for prop, propclass in self.getprops().items():
1150             if isinstance(propclass, hyperdb.String) and propclass.indexme:
1151                 # index them under (classname, nodeid, property)
1152                 self.db.indexer.add_text((self.classname, nodeid, prop),
1153                                 str(self.get(nodeid, prop)))
1155     def export_list(self, propnames, nodeid):
1156         ''' Export a node - generate a list of CSV-able data in the order
1157             specified by propnames for the given node.
1158         '''
1159         properties = self.getprops()
1160         l = []
1161         for prop in propnames:
1162             proptype = properties[prop]
1163             value = self.get(nodeid, prop)
1164             # "marshal" data where needed
1165             if value is None:
1166                 pass
1167             elif isinstance(proptype, hyperdb.Date):
1168                 value = value.get_tuple()
1169             elif isinstance(proptype, hyperdb.Interval):
1170                 value = value.get_tuple()
1171             elif isinstance(proptype, hyperdb.Password):
1172                 value = str(value)
1173             l.append(repr(value))
1175         # append retired flag
1176         l.append(self.is_retired(nodeid))
1178         return l
1179         
1180     def import_list(self, propnames, proplist):
1181         ''' Import a node - all information including "id" is present and
1182             should not be sanity checked. Triggers are not triggered. The
1183             journal should be initialised using the "creator" and "creation"
1184             information.
1186             Return the nodeid of the node imported.
1187         '''
1188         if self.db.journaltag is None:
1189             raise hyperdb.DatabaseError, 'Database open read-only'
1190         properties = self.getprops()
1192         d = {}
1193         view = self.getview(1)
1194         for i in range(len(propnames)):
1195             value = eval(proplist[i])
1196             if not value:
1197                 continue
1199             propname = propnames[i]
1200             if propname == 'id':
1201                 newid = value = int(value)
1202             elif propname == 'is retired':
1203                 # is the item retired?
1204                 if int(value):
1205                     d['_isdel'] = 1
1206                 continue
1208             prop = properties[propname]
1209             if isinstance(prop, hyperdb.Date):
1210                 value = int(calendar.timegm(value))
1211             elif isinstance(prop, hyperdb.Interval):
1212                 value = date.Interval(value).serialise()
1213             elif isinstance(prop, hyperdb.Number):
1214                 value = int(value)
1215             elif isinstance(prop, hyperdb.Boolean):
1216                 value = int(value)
1217             elif isinstance(prop, hyperdb.Link) and value:
1218                 value = int(value)
1219             elif isinstance(prop, hyperdb.Multilink):
1220                 # we handle multilinks separately
1221                 continue
1222             d[propname] = value
1224         # possibly make a new node
1225         if not d.has_key('id'):
1226             d['id'] = newid = self.maxid
1227             self.maxid += 1
1229         # save off the node
1230         view.append(d)
1232         # fix up multilinks
1233         ndx = view.find(id=newid)
1234         row = view[ndx]
1235         for i in range(len(propnames)):
1236             value = eval(proplist[i])
1237             propname = propnames[i]
1238             if propname == 'is retired':
1239                 continue
1240             prop = properties[propname]
1241             if not isinstance(prop, hyperdb.Multilink):
1242                 continue
1243             sv = getattr(row, propname)
1244             for entry in value:
1245                 sv.append(int(entry))
1247         self.db.dirty = 1
1248         creator = d.get('creator', 0)
1249         creation = d.get('creation', 0)
1250         self.db.addjournal(self.classname, str(newid), _CREATE, {}, creator,
1251             creation)
1252         return newid
1254     # --- used by Database
1255     def _commit(self):
1256         """ called post commit of the DB.
1257             interested subclasses may override """
1258         self.uncommitted = {}
1259         self.rbactions = []
1260         self.idcache = {}
1261     def _rollback(self):  
1262         """ called pre rollback of the DB.
1263             interested subclasses may override """
1264         for action in self.rbactions:
1265             action()
1266         self.rbactions = []
1267         self.uncommitted = {}
1268         self.idcache = {}
1269     def _clear(self):
1270         view = self.getview(1)
1271         if len(view):
1272             view[:] = []
1273             self.db.dirty = 1
1274         iv = self.getindexview(1)
1275         if iv:
1276             iv[:] = []
1277     def rollbackaction(self, action):
1278         """ call this to register a callback called on rollback
1279             callback is removed on end of transaction """
1280         self.rbactions.append(action)
1281     # --- internal
1282     def __getview(self):
1283         ''' Find the interface for a specific Class in the hyperdb.
1285             This method checks to see whether the schema has changed and
1286             re-works the underlying metakit structure if it has.
1287         '''
1288         db = self.db._db
1289         view = db.view(self.classname)
1290         mkprops = view.structure()
1292         # if we have structure in the database, and the structure hasn't
1293         # changed
1294         if mkprops and self.db.fastopen:
1295             return view.ordered(1)
1297         # is the definition the same?
1298         for nm, rutyp in self.ruprops.items():
1299             for mkprop in mkprops:
1300                 if mkprop.name == nm:
1301                     break
1302             else:
1303                 mkprop = None
1304             if mkprop is None:
1305                 break
1306             if _typmap[rutyp.__class__] != mkprop.type:
1307                 break
1308         else:
1309             return view.ordered(1)
1310         # need to create or restructure the mk view
1311         # id comes first, so MK will order it for us
1312         self.db.dirty = 1
1313         s = ["%s[id:I" % self.classname]
1314         for nm, rutyp in self.ruprops.items():
1315             mktyp = _typmap[rutyp.__class__]
1316             s.append('%s:%s' % (nm, mktyp))
1317             if mktyp == 'V':
1318                 s[-1] += ('[fid:I]')
1319         s.append('_isdel:I,activity:I,creation:I,creator:I]')
1320         v = self.db._db.getas(','.join(s))
1321         self.db.commit()
1322         return v.ordered(1)
1323     def getview(self, RW=0):
1324         return self.db._db.view(self.classname).ordered(1)
1325     def getindexview(self, RW=0):
1326         return self.db._db.view("_%s" % self.classname).ordered(1)
1328 def _fetchML(sv):
1329     l = []
1330     for row in sv:
1331         if row.fid:
1332             l.append(str(row.fid))
1333     return l
1335 def _fetchPW(s):
1336     ''' Convert to a password.Password unless the password is '' which is
1337         our sentinel for "unset".
1338     '''
1339     if s == '':
1340         return None
1341     p = password.Password()
1342     p.unpack(s)
1343     return p
1345 def _fetchLink(n):
1346     ''' Return None if the link is 0 - otherwise strify it.
1347     '''
1348     return n and str(n) or None
1350 def _fetchDate(n):
1351     ''' Convert the timestamp to a date.Date instance - unless it's 0 which
1352         is our sentinel for "unset".
1353     '''
1354     if n == 0:
1355         return None
1356     return date.Date(time.gmtime(n))
1358 def _fetchInterval(n):
1359     ''' Convert to a date.Interval unless the interval is '' which is our
1360         sentinel for "unset".
1361     '''
1362     if n == '':
1363         return None
1364     return date.Interval(n)
1366 _converters = {
1367     hyperdb.Date   : _fetchDate,
1368     hyperdb.Link   : _fetchLink,
1369     hyperdb.Multilink : _fetchML,
1370     hyperdb.Interval  : _fetchInterval,
1371     hyperdb.Password  : _fetchPW,
1372     hyperdb.Boolean   : lambda n: n,
1373     hyperdb.Number    : lambda n: n,
1374     hyperdb.String    : lambda s: s and str(s) or None,
1375 }                
1377 class FileName(hyperdb.String):
1378     isfilename = 1            
1380 _typmap = {
1381     FileName : 'S',
1382     hyperdb.String : 'S',
1383     hyperdb.Date   : 'I',
1384     hyperdb.Link   : 'I',
1385     hyperdb.Multilink : 'V',
1386     hyperdb.Interval  : 'S',
1387     hyperdb.Password  : 'S',
1388     hyperdb.Boolean   : 'I',
1389     hyperdb.Number    : 'I',
1391 class FileClass(Class, hyperdb.FileClass):
1392     ''' like Class but with a content property
1393     '''
1394     default_mime_type = 'text/plain'
1395     def __init__(self, db, classname, **properties):
1396         properties['content'] = FileName()
1397         if not properties.has_key('type'):
1398             properties['type'] = hyperdb.String()
1399         Class.__init__(self, db, classname, **properties)
1401     def get(self, nodeid, propname, default=_marker, cache=1):
1402         x = Class.get(self, nodeid, propname, default, cache)
1403         poss_msg = 'Possibly an access right configuration problem.'
1404         if propname == 'content':
1405             if x.startswith('file:'):
1406                 fnm = x[5:]
1407                 try:
1408                     x = open(fnm, 'rb').read()
1409                 except IOError, (strerror):
1410                     # XXX by catching this we donot see an error in the log.
1411                     return 'ERROR reading file: %s%s\n%s\n%s'%(
1412                             self.classname, nodeid, poss_msg, strerror)
1413         return x
1415     def create(self, **propvalues):
1416         self.fireAuditors('create', None, propvalues)
1417         content = propvalues['content']
1418         del propvalues['content']
1419         newid = Class.create_inner(self, **propvalues)
1420         if not content:
1421             return newid
1422         nm = bnm = '%s%s' % (self.classname, newid)
1423         sd = str(int(int(newid) / 1000))
1424         d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
1425         if not os.path.exists(d):
1426             os.makedirs(d)
1427         nm = os.path.join(d, nm)
1428         open(nm, 'wb').write(content)
1429         self.set(newid, content = 'file:'+nm)
1430         mimetype = propvalues.get('type', self.default_mime_type)
1431         self.db.indexer.add_text((self.classname, newid, 'content'), content,
1432             mimetype)
1433         def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
1434             action1(fnm)
1435         self.rollbackaction(undo)
1436         return newid
1438     def index(self, nodeid):
1439         Class.index(self, nodeid)
1440         mimetype = self.get(nodeid, 'type')
1441         if not mimetype:
1442             mimetype = self.default_mime_type
1443         self.db.indexer.add_text((self.classname, nodeid, 'content'),
1444                     self.get(nodeid, 'content'), mimetype)
1445  
1446 class IssueClass(Class, roundupdb.IssueClass):
1447     ''' The newly-created class automatically includes the "messages",
1448         "files", "nosy", and "superseder" properties.  If the 'properties'
1449         dictionary attempts to specify any of these properties or a
1450         "creation" or "activity" property, a ValueError is raised.
1451     '''
1452     def __init__(self, db, classname, **properties):
1453         if not properties.has_key('title'):
1454             properties['title'] = hyperdb.String(indexme='yes')
1455         if not properties.has_key('messages'):
1456             properties['messages'] = hyperdb.Multilink("msg")
1457         if not properties.has_key('files'):
1458             properties['files'] = hyperdb.Multilink("file")
1459         if not properties.has_key('nosy'):
1460             # note: journalling is turned off as it really just wastes
1461             # space. this behaviour may be overridden in an instance
1462             properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
1463         if not properties.has_key('superseder'):
1464             properties['superseder'] = hyperdb.Multilink(classname)
1465         Class.__init__(self, db, classname, **properties)
1466         
1467 CURVERSION = 2
1469 class Indexer(indexer.Indexer):
1470     disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1471     def __init__(self, path, datadb):
1472         self.path = os.path.join(path, 'index.mk4')
1473         self.db = metakit.storage(self.path, 1)
1474         self.datadb = datadb
1475         self.reindex = 0
1476         v = self.db.view('version')
1477         if not v.structure():
1478             v = self.db.getas('version[vers:I]')
1479             self.db.commit()
1480             v.append(vers=CURVERSION)
1481             self.reindex = 1
1482         elif v[0].vers != CURVERSION:
1483             v[0].vers = CURVERSION
1484             self.reindex = 1
1485         if self.reindex:
1486             self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
1487             self.db.getas('index[word:S,hits[pos:I]]')
1488             self.db.commit()
1489             self.reindex = 1
1490         self.changed = 0
1491         self.propcache = {}
1493     def force_reindex(self):
1494         v = self.db.view('ids')
1495         v[:] = []
1496         v = self.db.view('index')
1497         v[:] = []
1498         self.db.commit()
1499         self.reindex = 1
1501     def should_reindex(self):
1502         return self.reindex
1504     def _getprops(self, classname):
1505         props = self.propcache.get(classname, None)
1506         if props is None:
1507             props = self.datadb.view(classname).structure()
1508             props = [prop.name for prop in props]
1509             self.propcache[classname] = props
1510         return props
1512     def _getpropid(self, classname, propname):
1513         return self._getprops(classname).index(propname)
1515     def _getpropname(self, classname, propid):
1516         return self._getprops(classname)[propid]
1518     def add_text(self, identifier, text, mime_type='text/plain'):
1519         if mime_type != 'text/plain':
1520             return
1521         classname, nodeid, property = identifier
1522         tbls = self.datadb.view('tables')
1523         tblid = tbls.find(name=classname)
1524         if tblid < 0:
1525             raise KeyError, "unknown class %r"%classname
1526         nodeid = int(nodeid)
1527         propid = self._getpropid(classname, property)
1528         ids = self.db.view('ids')
1529         oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
1530         if oldpos > -1:
1531             ids[oldpos].ignore = 1
1532             self.changed = 1
1533         pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
1534         
1535         wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
1536         words = {}
1537         for word in wordlist:
1538             if not self.disallows.has_key(word):
1539                 words[word] = 1
1540         words = words.keys()
1541         
1542         index = self.db.view('index').ordered(1)
1543         for word in words:
1544             ndx = index.find(word=word)
1545             if ndx < 0:
1546                 index.append(word=word)
1547                 ndx = index.find(word=word)
1548             index[ndx].hits.append(pos=pos)
1549             self.changed = 1
1551     def find(self, wordlist):
1552         hits = None
1553         index = self.db.view('index').ordered(1)
1554         for word in wordlist:
1555             word = word.upper()
1556             if not 2 < len(word) < 26:
1557                 continue
1558             ndx = index.find(word=word)
1559             if ndx < 0:
1560                 return {}
1561             if hits is None:
1562                 hits = index[ndx].hits
1563             else:
1564                 hits = hits.intersect(index[ndx].hits)
1565             if len(hits) == 0:
1566                 return {}
1567         if hits is None:
1568             return {}
1569         rslt = {}
1570         ids = self.db.view('ids').remapwith(hits)
1571         tbls = self.datadb.view('tables')
1572         for i in range(len(ids)):
1573             hit = ids[i]
1574             if not hit.ignore:
1575                 classname = tbls[hit.tblid].name
1576                 nodeid = str(hit.nodeid)
1577                 property = self._getpropname(classname, hit.propid)
1578                 rslt[i] = (classname, nodeid, property)
1579         return rslt
1581     def save_index(self):
1582         if self.changed:
1583             self.db.commit()
1584         self.changed = 0
1586     def rollback(self):
1587         if self.changed:
1588             self.db.rollback()
1589             self.db = metakit.storage(self.path, 1)
1590         self.changed = 0