Code

- Fixed retirement of items in rdbms imports (sf bug 841355)
[roundup.git] / roundup / backends / back_metakit.py
1 # $Id: back_metakit.py,v 1.53 2003-11-14 00:11:18 richard 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     return db
53 class _Database(hyperdb.Database, roundupdb.Database):
54     def __init__(self, config, journaltag=None):
55         self.config = config
56         self.journaltag = journaltag
57         self.classes = {}
58         self.dirty = 0
59         self.lockfile = None
60         self._db = self.__open()
61         self.indexer = Indexer(self.config.DATABASE, self._db)
62         self.sessions = Sessions(self.config)
63         self.otks = OneTimeKeys(self.config)
64         self.security = security.Security(self)
66         os.umask(0002)
68     def post_init(self):
69         if self.indexer.should_reindex():
70             self.reindex()
72     def refresh_database(self):
73         # XXX handle refresh
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 == 'transactions':
85             return self.dirty
86         # fall back on the classes
87         return self.getclass(classname)
88     def getclass(self, classname):
89         try:
90             return self.classes[classname]
91         except KeyError:
92             raise KeyError, 'There is no class called "%s"'%classname
93     def getclasses(self):
94         return self.classes.keys()
95     # --- end of ping's spec 
97     # --- exposed methods
98     def commit(self):
99         if self.dirty:
100             self._db.commit()
101             for cl in self.classes.values():
102                 cl._commit()
103             self.indexer.save_index()
104         self.dirty = 0
105     def rollback(self):
106         if self.dirty:
107             for cl in self.classes.values():
108                 cl._rollback()
109             self._db.rollback()
110             self._db = None
111             self._db = metakit.storage(self.dbnm, 1)
112             self.hist = self._db.view('history')
113             self.tables = self._db.view('tables')
114             self.indexer.rollback()
115             self.indexer.datadb = self._db
116         self.dirty = 0
117     def clearCache(self):
118         for cl in self.classes.values():
119             cl._commit()
120     def clear(self):
121         for cl in self.classes.values():
122             cl._clear()
123     def hasnode(self, classname, nodeid):
124         return self.getclass(classname).hasnode(nodeid)
125     def pack(self, pack_before):
126         mindate = int(calendar.timegm(pack_before.get_tuple()))
127         i = 0
128         while i < len(self.hist):
129             if self.hist[i].date < mindate and self.hist[i].action != _CREATE:
130                 self.hist.delete(i)
131             else:
132                 i = i + 1
133     def addclass(self, cl):
134         self.classes[cl.classname] = cl
135         if self.tables.find(name=cl.classname) < 0:
136             self.tables.append(name=cl.classname)
137     def addjournal(self, tablenm, nodeid, action, params, creator=None,
138                 creation=None):
139         tblid = self.tables.find(name=tablenm)
140         if tblid == -1:
141             tblid = self.tables.append(name=tablenm)
142         if creator is None:
143             creator = int(self.getuid())
144         else:
145             try:
146                 creator = int(creator)
147             except TypeError:
148                 creator = int(self.getclass('user').lookup(creator))
149         if creation is None:
150             creation = int(time.time())
151         elif isinstance(creation, date.Date):
152             creation = int(calendar.timegm(creation.get_tuple()))
153         # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
154         self.hist.append(tableid=tblid,
155                          nodeid=int(nodeid),
156                          date=creation,
157                          action=action,
158                          user = creator,
159                          params = marshal.dumps(params))
160     def getjournal(self, tablenm, nodeid):
161         rslt = []
162         tblid = self.tables.find(name=tablenm)
163         if tblid == -1:
164             return rslt
165         q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
166         if len(q) == 0:
167             raise IndexError, "no history for id %s in %s" % (nodeid, tablenm)
168         i = 0
169         #userclass = self.getclass('user')
170         for row in q:
171             try:
172                 params = marshal.loads(row.params)
173             except ValueError:
174                 print "history couldn't unmarshal %r" % row.params
175                 params = {}
176             #usernm = userclass.get(str(row.user), 'username')
177             dt = date.Date(time.gmtime(row.date))
178             #rslt.append((nodeid, dt, usernm, _actionnames[row.action], params))
179             rslt.append((nodeid, dt, str(row.user), _actionnames[row.action],
180                 params))
181         return rslt
183     def destroyjournal(self, tablenm, nodeid):
184         nodeid = int(nodeid)
185         tblid = self.tables.find(name=tablenm)
186         if tblid == -1:
187             return 
188         i = 0
189         hist = self.hist
190         while i < len(hist):
191             if hist[i].tableid == tblid and hist[i].nodeid == nodeid:
192                 hist.delete(i)
193             else:
194                 i = i + 1
195         self.dirty = 1
196         
197     def close(self):
198         for cl in self.classes.values():
199             cl.db = None
200         self._db = None
201         if self.lockfile is not None:
202             locking.release_lock(self.lockfile)
203         if _dbs.has_key(self.config.DATABASE):
204             del _dbs[self.config.DATABASE]
205         if self.lockfile is not None:
206             self.lockfile.close()
207             self.lockfile = None
208         self.classes = {}
209         self.indexer = None
211     # --- internal
212     def __open(self):
213         ''' Open the metakit database
214         '''
215         # make the database dir if it doesn't exist
216         if not os.path.exists(self.config.DATABASE):
217             os.makedirs(self.config.DATABASE)
219         # figure the file names
220         self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
221         lockfilenm = db[:-3]+'lck'
223         # get the database lock
224         self.lockfile = locking.acquire_lock(lockfilenm)
225         self.lockfile.write(str(os.getpid()))
226         self.lockfile.flush()
228         # see if the schema has changed since last db access
229         self.fastopen = 0
230         if os.path.exists(db):
231             dbtm = os.path.getmtime(db)
232             pkgnm = self.config.__name__.split('.')[0]
233             schemamod = sys.modules.get(pkgnm+'.dbinit', None)
234             if schemamod:
235                 if os.path.exists(schemamod.__file__):
236                     schematm = os.path.getmtime(schemamod.__file__)
237                     if schematm < dbtm:
238                         # found schema mod - it's older than the db
239                         self.fastopen = 1
240                 else:
241                      # can't find schemamod - must be frozen
242                     self.fastopen = 1
244         # open the db
245         db = metakit.storage(db, 1)
246         hist = db.view('history')
247         tables = db.view('tables')
248         if not self.fastopen:
249             # create the database if it's brand new
250             if not hist.structure():
251                 hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
252             if not tables.structure():
253                 tables = db.getas('tables[name:S]')
254             db.commit()
256         # we now have an open, initialised database
257         self.tables = tables
258         self.hist = hist
259         return db
261     def setid(self, classname, maxid):
262         ''' No-op in metakit
263         '''
264         pass
265         
266 _STRINGTYPE = type('')
267 _LISTTYPE = type([])
268 _CREATE, _SET, _RETIRE, _LINK, _UNLINK, _RESTORE = range(6)
270 _actionnames = {
271     _CREATE : 'create',
272     _SET : 'set',
273     _RETIRE : 'retire',
274     _RESTORE : 'restore',
275     _LINK : 'link',
276     _UNLINK : 'unlink',
279 _marker = []
281 _ALLOWSETTINGPRIVATEPROPS = 0
283 class Class:    
284     privateprops = None
285     def __init__(self, db, classname, **properties):
286         #self.db = weakref.proxy(db)
287         self.db = db
288         self.classname = classname
289         self.keyname = None
290         self.ruprops = properties
291         self.privateprops = { 'id' : hyperdb.String(),
292                               'activity' : hyperdb.Date(),
293                               'creation' : hyperdb.Date(),
294                               'creator'  : hyperdb.Link('user') }
296         # event -> list of callables
297         self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
298         self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
300         view = self.__getview()
301         self.maxid = 1
302         if view:
303             self.maxid = view[-1].id + 1
304         self.uncommitted = {}
305         self.rbactions = []
307         # people reach inside!!
308         self.properties = self.ruprops
309         self.db.addclass(self)
310         self.idcache = {}
312         # default is to journal changes
313         self.do_journal = 1
315     def enableJournalling(self):
316         '''Turn journalling on for this class
317         '''
318         self.do_journal = 1
320     def disableJournalling(self):
321         '''Turn journalling off for this class
322         '''
323         self.do_journal = 0
324         
325     # --- the roundup.Class methods
326     def audit(self, event, detector):
327         l = self.auditors[event]
328         if detector not in l:
329             self.auditors[event].append(detector)
330     def fireAuditors(self, action, nodeid, newvalues):
331         for audit in self.auditors[action]:
332             audit(self.db, self, nodeid, newvalues)
333     def fireReactors(self, action, nodeid, oldvalues):
334         for react in self.reactors[action]:
335             react(self.db, self, nodeid, oldvalues)
336     def react(self, event, detector):
337         l = self.reactors[event]
338         if detector not in l:
339             self.reactors[event].append(detector)
341     # --- the hyperdb.Class methods
342     def create(self, **propvalues):
343         self.fireAuditors('create', None, propvalues)
344         newid = self.create_inner(**propvalues)
345         # self.set() (called in self.create_inner()) does reactors)
346         return newid
348     def create_inner(self, **propvalues):
349         rowdict = {}
350         rowdict['id'] = newid = self.maxid
351         self.maxid += 1
352         ndx = self.getview(1).append(rowdict)
353         propvalues['#ISNEW'] = 1
354         try:
355             self.set(str(newid), **propvalues)
356         except Exception:
357             self.maxid -= 1
358             raise
359         return str(newid)
360     
361     def get(self, nodeid, propname, default=_marker, cache=1):
362         '''
363             'cache' exists for backwards compatibility, and is not used.
364         '''
366         view = self.getview()        
367         id = int(nodeid)
368         if cache == 0:
369             oldnode = self.uncommitted.get(id, None)
370             if oldnode and oldnode.has_key(propname):
371                 return oldnode[propname]
372         ndx = self.idcache.get(id, None)
373         if ndx is None:
374             ndx = view.find(id=id)
375             if ndx < 0:
376                 raise IndexError, "%s has no node %s" % (self.classname, nodeid)
377             self.idcache[id] = ndx
378         try:
379             raw = getattr(view[ndx], propname)
380         except AttributeError:
381             raise KeyError, propname
382         rutyp = self.ruprops.get(propname, None)
383         if rutyp is None:
384             rutyp = self.privateprops[propname]
385         converter = _converters.get(rutyp.__class__, None)
386         if converter:
387             raw = converter(raw)
388         return raw
389         
390     def set(self, nodeid, **propvalues):
391         isnew = 0
392         if propvalues.has_key('#ISNEW'):
393             isnew = 1
394             del propvalues['#ISNEW']
395         if not isnew:
396             self.fireAuditors('set', nodeid, propvalues)
397         if not propvalues:
398             return propvalues
399         if propvalues.has_key('id'):
400             raise KeyError, '"id" is reserved'
401         if self.db.journaltag is None:
402             raise hyperdb.DatabaseError, 'Database open read-only'
403         view = self.getview(1)
405         # node must exist & not be retired
406         id = int(nodeid)
407         ndx = view.find(id=id)
408         if ndx < 0:
409             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
410         row = view[ndx]
411         if row._isdel:
412             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
413         oldnode = self.uncommitted.setdefault(id, {})
414         changes = {}
416         for key, value in propvalues.items():
417             # this will raise the KeyError if the property isn't valid
418             # ... we don't use getprops() here because we only care about
419             # the writeable properties.
420             if _ALLOWSETTINGPRIVATEPROPS:
421                 prop = self.ruprops.get(key, None)
422                 if not prop:
423                     prop = self.privateprops[key]
424             else:
425                 prop = self.ruprops[key]
426             converter = _converters.get(prop.__class__, lambda v: v)
427             # if the value's the same as the existing value, no sense in
428             # doing anything
429             oldvalue = converter(getattr(row, key))
430             if  value == oldvalue:
431                 del propvalues[key]
432                 continue
433             
434             # check to make sure we're not duplicating an existing key
435             if key == self.keyname:
436                 iv = self.getindexview(1)
437                 ndx = iv.find(k=value)
438                 if ndx == -1:
439                     iv.append(k=value, i=row.id)
440                     if not isnew:
441                         ndx = iv.find(k=oldvalue)
442                         if ndx > -1:
443                             iv.delete(ndx)
444                 else:
445                     raise ValueError, 'node with key "%s" exists'%value
447             # do stuff based on the prop type
448             if isinstance(prop, hyperdb.Link):
449                 link_class = prop.classname
450                 # must be a string or None
451                 if value is not None and not isinstance(value, type('')):
452                     raise ValueError, 'property "%s" link value be a string'%(
453                         key)
454                 # Roundup sets to "unselected" by passing None
455                 if value is None:
456                     value = 0   
457                 # if it isn't a number, it's a key
458                 try:
459                     int(value)
460                 except ValueError:
461                     try:
462                         value = self.db.getclass(link_class).lookup(value)
463                     except (TypeError, KeyError):
464                         raise IndexError, 'new property "%s": %s not a %s'%(
465                             key, value, prop.classname)
467                 if (value is not None and
468                         not self.db.getclass(link_class).hasnode(value)):
469                     raise IndexError, '%s has no node %s'%(link_class, value)
471                 setattr(row, key, int(value))
472                 changes[key] = oldvalue
473                 
474                 if self.do_journal and prop.do_journal:
475                     # register the unlink with the old linked node
476                     if oldvalue:
477                         self.db.addjournal(link_class, oldvalue, _UNLINK,
478                             (self.classname, str(row.id), key))
480                     # register the link with the newly linked node
481                     if value:
482                         self.db.addjournal(link_class, value, _LINK,
483                             (self.classname, str(row.id), key))
485             elif isinstance(prop, hyperdb.Multilink):
486                 if value is not None and type(value) != _LISTTYPE:
487                     raise TypeError, 'new property "%s" not a list of ids'%key
488                 link_class = prop.classname
489                 l = []
490                 if value is None:
491                     value = []
492                 for entry in value:
493                     if type(entry) != _STRINGTYPE:
494                         raise ValueError, 'new property "%s" link value ' \
495                             'must be a string'%key
496                     # if it isn't a number, it's a key
497                     try:
498                         int(entry)
499                     except ValueError:
500                         try:
501                             entry = self.db.getclass(link_class).lookup(entry)
502                         except (TypeError, KeyError):
503                             raise IndexError, 'new property "%s": %s not a %s'%(
504                                 key, entry, prop.classname)
505                     l.append(entry)
506                 propvalues[key] = value = l
508                 # handle removals
509                 rmvd = []
510                 for id in oldvalue:
511                     if id not in value:
512                         rmvd.append(id)
513                         # register the unlink with the old linked node
514                         if self.do_journal and prop.do_journal:
515                             self.db.addjournal(link_class, id, _UNLINK,
516                                 (self.classname, str(row.id), key))
518                 # handle additions
519                 adds = []
520                 for id in value:
521                     if id not in oldvalue:
522                         if not self.db.getclass(link_class).hasnode(id):
523                             raise IndexError, '%s has no node %s'%(
524                                 link_class, id)
525                         adds.append(id)
526                         # register the link with the newly linked node
527                         if self.do_journal and prop.do_journal:
528                             self.db.addjournal(link_class, id, _LINK,
529                                 (self.classname, str(row.id), key))
531                 # perform the modifications on the actual property value
532                 sv = getattr(row, key)
533                 i = 0
534                 while i < len(sv):
535                     if str(sv[i].fid) in rmvd:
536                         sv.delete(i)
537                     else:
538                         i += 1
539                 for id in adds:
540                     sv.append(fid=int(id))
542                 # figure the journal entry
543                 l = []
544                 if adds:
545                     l.append(('+', adds))
546                 if rmvd:
547                     l.append(('-', rmvd))
548                 if l:
549                     changes[key] = tuple(l)
550                 #changes[key] = oldvalue
552                 if not rmvd and not adds:
553                     del propvalues[key]
555             elif isinstance(prop, hyperdb.String):
556                 if value is not None and type(value) != _STRINGTYPE:
557                     raise TypeError, 'new property "%s" not a string'%key
558                 if value is None:
559                     value = ''
560                 setattr(row, key, value)
561                 changes[key] = oldvalue
562                 if hasattr(prop, 'isfilename') and prop.isfilename:
563                     propvalues[key] = os.path.basename(value)
564                 if prop.indexme:
565                     self.db.indexer.add_text((self.classname, nodeid, key),
566                         value, 'text/plain')
568             elif isinstance(prop, hyperdb.Password):
569                 if value is not None and not isinstance(value, password.Password):
570                     raise TypeError, 'new property "%s" not a Password'% key
571                 if value is None:
572                     value = ''
573                 setattr(row, key, str(value))
574                 changes[key] = str(oldvalue)
575                 propvalues[key] = str(value)
577             elif isinstance(prop, hyperdb.Date):
578                 if value is not None and not isinstance(value, date.Date):
579                     raise TypeError, 'new property "%s" not a Date'% key
580                 if value is None:
581                     setattr(row, key, 0)
582                 else:
583                     setattr(row, key, int(calendar.timegm(value.get_tuple())))
584                 changes[key] = str(oldvalue)
585                 propvalues[key] = str(value)
587             elif isinstance(prop, hyperdb.Interval):
588                 if value is not None and not isinstance(value, date.Interval):
589                     raise TypeError, 'new property "%s" not an Interval'% key
590                 if value is None:
591                     setattr(row, key, '')
592                 else:
593                     # kedder: we should store interval values serialized
594                     setattr(row, key, value.serialise())
595                 changes[key] = str(oldvalue)
596                 propvalues[key] = str(value)
597  
598             elif isinstance(prop, hyperdb.Number):
599                 if value is None:
600                     value = 0
601                 try:
602                     v = int(value)
603                 except ValueError:
604                     raise TypeError, "%s (%s) is not numeric"%(key, repr(value))
605                 setattr(row, key, v)
606                 changes[key] = oldvalue
607                 propvalues[key] = value
609             elif isinstance(prop, hyperdb.Boolean):
610                 if value is None:
611                     bv = 0
612                 elif value not in (0,1):
613                     raise TypeError, "%s (%s) is not boolean"%(key, repr(value))
614                 else:
615                     bv = value 
616                 setattr(row, key, bv)
617                 changes[key] = oldvalue
618                 propvalues[key] = value
620             oldnode[key] = oldvalue
622         # nothing to do?
623         if not propvalues:
624             return propvalues
625         if not propvalues.has_key('activity'):
626             row.activity = int(time.time())
627         if isnew:
628             if not row.creation:
629                 row.creation = int(time.time())
630             if not row.creator:
631                 row.creator = int(self.db.getuid())
633         self.db.dirty = 1
634         if self.do_journal:
635             if isnew:
636                 self.db.addjournal(self.classname, nodeid, _CREATE, {})
637                 self.fireReactors('create', nodeid, None)
638             else:
639                 self.db.addjournal(self.classname, nodeid, _SET, changes)
640                 self.fireReactors('set', nodeid, oldnode)
642         return propvalues
643     
644     def retire(self, nodeid):
645         if self.db.journaltag is None:
646             raise hyperdb.DatabaseError, 'Database open read-only'
647         self.fireAuditors('retire', nodeid, None)
648         view = self.getview(1)
649         ndx = view.find(id=int(nodeid))
650         if ndx < 0:
651             raise KeyError, "nodeid %s not found" % nodeid
653         row = view[ndx]
654         oldvalues = self.uncommitted.setdefault(row.id, {})
655         oldval = oldvalues['_isdel'] = row._isdel
656         row._isdel = 1
658         if self.do_journal:
659             self.db.addjournal(self.classname, nodeid, _RETIRE, {})
660         if self.keyname:
661             iv = self.getindexview(1)
662             ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
663             if ndx > -1:
664                 iv.delete(ndx)
665         self.db.dirty = 1
666         self.fireReactors('retire', nodeid, None)
668     def restore(self, nodeid):
669         '''Restpre a retired node.
671         Make node available for all operations like it was before retirement.
672         '''
673         if self.db.journaltag is None:
674             raise hyperdb.DatabaseError, 'Database open read-only'
676         # check if key property was overrided
677         key = self.getkey()
678         keyvalue = self.get(nodeid, key)
679         try:
680             id = self.lookup(keyvalue)
681         except KeyError:
682             pass
683         else:
684             raise KeyError, "Key property (%s) of retired node clashes with \
685                 existing one (%s)" % (key, keyvalue)
686         # Now we can safely restore node
687         self.fireAuditors('restore', nodeid, None)
688         view = self.getview(1)
689         ndx = view.find(id=int(nodeid))
690         if ndx < 0:
691             raise KeyError, "nodeid %s not found" % nodeid
693         row = view[ndx]
694         oldvalues = self.uncommitted.setdefault(row.id, {})
695         oldval = oldvalues['_isdel'] = row._isdel
696         row._isdel = 0
698         if self.do_journal:
699             self.db.addjournal(self.classname, nodeid, _RESTORE, {})
700         if self.keyname:
701             iv = self.getindexview(1)
702             ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
703             if ndx > -1:
704                 iv.delete(ndx)
705         self.db.dirty = 1
706         self.fireReactors('restore', nodeid, None)
708     def is_retired(self, nodeid):
709         view = self.getview(1)
710         # node must exist & not be retired
711         id = int(nodeid)
712         ndx = view.find(id=id)
713         if ndx < 0:
714             raise IndexError, "%s has no node %s" % (self.classname, nodeid)
715         row = view[ndx]
716         return row._isdel
718     def history(self, nodeid):
719         if not self.do_journal:
720             raise ValueError, 'Journalling is disabled for this class'
721         return self.db.getjournal(self.classname, nodeid)
723     def setkey(self, propname):
724         if self.keyname:
725             if propname == self.keyname:
726                 return
727             raise ValueError, "%s already indexed on %s"%(self.classname,
728                 self.keyname)
729         prop = self.properties.get(propname, None)
730         if prop is None:
731             prop = self.privateprops.get(propname, None)
732         if prop is None:
733             raise KeyError, "no property %s" % propname
734         if not isinstance(prop, hyperdb.String):
735             raise TypeError, "%s is not a String" % propname
737         # TODO: metakit needs to be able to cope with the key property
738         # *changing*, which it can't do at present. At the moment, it
739         # creates the key prop index once, with no record of the name of
740         # the property for the index.
742         # first setkey for this run
743         self.keyname = propname
744         iv = self.db._db.view('_%s' % self.classname)
745         if self.db.fastopen and iv.structure():
746             return
748         # very first setkey ever
749         self.db.dirty = 1
750         iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
751         iv = iv.ordered(1)
752         for row in self.getview():
753             iv.append(k=getattr(row, propname), i=row.id)
754         self.db.commit()
756     def getkey(self):
757         return self.keyname
759     def lookup(self, keyvalue):
760         if type(keyvalue) is not _STRINGTYPE:
761             raise TypeError, "%r is not a string" % keyvalue
762         iv = self.getindexview()
763         if iv:
764             ndx = iv.find(k=keyvalue)
765             if ndx > -1:
766                 return str(iv[ndx].i)
767         else:
768             view = self.getview()
769             ndx = view.find({self.keyname:keyvalue, '_isdel':0})
770             if ndx > -1:
771                 return str(view[ndx].id)
772         raise KeyError, keyvalue
774     def destroy(self, id):
775         view = self.getview(1)
776         ndx = view.find(id=int(id))
777         if ndx > -1:
778             if self.keyname:
779                 keyvalue = getattr(view[ndx], self.keyname)
780                 iv = self.getindexview(1)
781                 if iv:
782                     ivndx = iv.find(k=keyvalue)
783                     if ivndx > -1:
784                         iv.delete(ivndx)
785             view.delete(ndx)
786             self.db.destroyjournal(self.classname, id)
787             self.db.dirty = 1
788         
789     def find(self, **propspec):
790         """Get the ids of nodes in this class which link to the given nodes.
792         'propspec' consists of keyword args propname={nodeid:1,}   
793         'propname' must be the name of a property in this class, or a
794                    KeyError is raised.  That property must be a Link or
795                    Multilink property, or a TypeError is raised.
797         Any node in this class whose propname property links to any of the
798         nodeids will be returned. Used by the full text indexing, which knows
799         that "foo" occurs in msg1, msg3 and file7; so we have hits on these
800         issues:
802             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
804         """
805         propspec = propspec.items()
806         for propname, nodeid in propspec:
807             # check the prop is OK
808             prop = self.ruprops[propname]
809             if (not isinstance(prop, hyperdb.Link) and
810                     not isinstance(prop, hyperdb.Multilink)):
811                 raise TypeError, "'%s' not a Link/Multilink property"%propname
813         vws = []
814         for propname, ids in propspec:
815             if type(ids) is _STRINGTYPE:
816                 ids = {int(ids):1}
817             elif ids is None:
818                 ids = {0:1}
819             else:
820                 d = {}
821                 for id in ids.keys():
822                     if id is None:
823                         d[0] = 1
824                     else:
825                         d[int(id)] = 1
826                 ids = d
827             prop = self.ruprops[propname]
828             view = self.getview()
829             if isinstance(prop, hyperdb.Multilink):
830                 def ff(row, nm=propname, ids=ids):
831                     sv = getattr(row, nm)
832                     for sr in sv:
833                         if ids.has_key(sr.fid):
834                             return 1
835                     return 0
836             else:
837                 def ff(row, nm=propname, ids=ids):
838                     return ids.has_key(getattr(row, nm))
839             ndxview = view.filter(ff)
840             vws.append(ndxview.unique())
842         # handle the empty match case
843         if not vws:
844             return []
846         ndxview = vws[0]
847         for v in vws[1:]:
848             ndxview = ndxview.union(v)
849         view = self.getview().remapwith(ndxview)
850         rslt = []
851         for row in view:
852             rslt.append(str(row.id))
853         return rslt
854             
856     def list(self):
857         l = []
858         for row in self.getview().select(_isdel=0):
859             l.append(str(row.id))
860         return l
862     def getnodeids(self):
863         l = []
864         for row in self.getview():
865             l.append(str(row.id))
866         return l
868     def count(self):
869         return len(self.getview())
871     def getprops(self, protected=1):
872         # protected is not in ping's spec
873         allprops = self.ruprops.copy()
874         if protected and self.privateprops is not None:
875             allprops.update(self.privateprops)
876         return allprops
878     def addprop(self, **properties):
879         for key in properties.keys():
880             if self.ruprops.has_key(key):
881                 raise ValueError, "%s is already a property of %s"%(key,
882                     self.classname)
883         self.ruprops.update(properties)
884         # Class structure has changed
885         self.db.fastopen = 0
886         view = self.__getview()
887         self.db.commit()
888     # ---- end of ping's spec
890     def filter(self, search_matches, filterspec, sort=(None,None),
891             group=(None,None)):
892         # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
893         # filterspec is a dict {propname:value}
894         # sort and group are (dir, prop) where dir is '+', '-' or None
895         #                    and prop is a prop name or None
897         timezone = self.db.getUserTimezone()
899         where = {'_isdel':0}
900         wherehigh = {}
901         mlcriteria = {}
902         regexes = {}
903         orcriteria = {}
904         for propname, value in filterspec.items():
905             prop = self.ruprops.get(propname, None)
906             if prop is None:
907                 prop = self.privateprops[propname]
908             if isinstance(prop, hyperdb.Multilink):
909                 if value in ('-1', ['-1']):
910                     value = []
911                 elif type(value) is not _LISTTYPE:
912                     value = [value]
913                 # transform keys to ids
914                 u = []
915                 for item in value:
916                     try:
917                         item = int(item)
918                     except (TypeError, ValueError):
919                         item = int(self.db.getclass(prop.classname).lookup(item))
920                     if item == -1:
921                         item = 0
922                     u.append(item)
923                 mlcriteria[propname] = u
924             elif isinstance(prop, hyperdb.Link):
925                 if type(value) is not _LISTTYPE:
926                     value = [value]
927                 # transform keys to ids
928                 u = []
929                 for item in value:
930                     try:
931                         item = int(item)
932                     except (TypeError, ValueError):
933                         item = int(self.db.getclass(prop.classname).lookup(item))
934                     if item == -1:
935                         item = 0
936                     u.append(item)
937                 if len(u) == 1:
938                     where[propname] = u[0]
939                 else:
940                     orcriteria[propname] = u
941             elif isinstance(prop, hyperdb.String):
942                 if type(value) is not type([]):
943                     value = [value]
944                 m = []
945                 for v in value:
946                     # simple glob searching
947                     v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
948                     v = v.replace('?', '.')
949                     v = v.replace('*', '.*?')
950                     m.append(v)
951                 regexes[propname] = re.compile('(%s)'%('|'.join(m)), 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(repr(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
1207             elif value is None:
1208                 d[propname] = None
1209                 continue
1211             prop = properties[propname]
1212             if isinstance(prop, hyperdb.Date):
1213                 value = int(calendar.timegm(value))
1214             elif isinstance(prop, hyperdb.Interval):
1215                 value = date.Interval(value).serialise()
1216             elif isinstance(prop, hyperdb.Number):
1217                 value = int(value)
1218             elif isinstance(prop, hyperdb.Boolean):
1219                 value = int(value)
1220             elif isinstance(prop, hyperdb.Link) and value:
1221                 value = int(value)
1222             elif isinstance(prop, hyperdb.Multilink):
1223                 # we handle multilinks separately
1224                 continue
1225             d[propname] = value
1227         # possibly make a new node
1228         if not d.has_key('id'):
1229             d['id'] = newid = self.maxid
1230             self.maxid += 1
1232         # save off the node
1233         view.append(d)
1235         # fix up multilinks
1236         ndx = view.find(id=newid)
1237         row = view[ndx]
1238         for i in range(len(propnames)):
1239             value = eval(proplist[i])
1240             propname = propnames[i]
1241             if propname == 'is retired':
1242                 continue
1243             prop = properties[propname]
1244             if not isinstance(prop, hyperdb.Multilink):
1245                 continue
1246             sv = getattr(row, propname)
1247             for entry in value:
1248                 sv.append(int(entry))
1250         self.db.dirty = 1
1251         creator = d.get('creator', 0)
1252         creation = d.get('creation', 0)
1253         self.db.addjournal(self.classname, str(newid), _CREATE, {}, creator,
1254             creation)
1255         return newid
1257     # --- used by Database
1258     def _commit(self):
1259         """ called post commit of the DB.
1260             interested subclasses may override """
1261         self.uncommitted = {}
1262         self.rbactions = []
1263         self.idcache = {}
1264     def _rollback(self):  
1265         """ called pre rollback of the DB.
1266             interested subclasses may override """
1267         for action in self.rbactions:
1268             action()
1269         self.rbactions = []
1270         self.uncommitted = {}
1271         self.idcache = {}
1272     def _clear(self):
1273         view = self.getview(1)
1274         if len(view):
1275             view[:] = []
1276             self.db.dirty = 1
1277         iv = self.getindexview(1)
1278         if iv:
1279             iv[:] = []
1280     def rollbackaction(self, action):
1281         """ call this to register a callback called on rollback
1282             callback is removed on end of transaction """
1283         self.rbactions.append(action)
1284     # --- internal
1285     def __getview(self):
1286         ''' Find the interface for a specific Class in the hyperdb.
1288             This method checks to see whether the schema has changed and
1289             re-works the underlying metakit structure if it has.
1290         '''
1291         db = self.db._db
1292         view = db.view(self.classname)
1293         mkprops = view.structure()
1295         # if we have structure in the database, and the structure hasn't
1296         # changed
1297         if mkprops and self.db.fastopen:
1298             return view.ordered(1)
1300         # is the definition the same?
1301         for nm, rutyp in self.ruprops.items():
1302             for mkprop in mkprops:
1303                 if mkprop.name == nm:
1304                     break
1305             else:
1306                 mkprop = None
1307             if mkprop is None:
1308                 break
1309             if _typmap[rutyp.__class__] != mkprop.type:
1310                 break
1311         else:
1312             return view.ordered(1)
1313         # need to create or restructure the mk view
1314         # id comes first, so MK will order it for us
1315         self.db.dirty = 1
1316         s = ["%s[id:I" % self.classname]
1317         for nm, rutyp in self.ruprops.items():
1318             mktyp = _typmap[rutyp.__class__]
1319             s.append('%s:%s' % (nm, mktyp))
1320             if mktyp == 'V':
1321                 s[-1] += ('[fid:I]')
1322         s.append('_isdel:I,activity:I,creation:I,creator:I]')
1323         v = self.db._db.getas(','.join(s))
1324         self.db.commit()
1325         return v.ordered(1)
1326     def getview(self, RW=0):
1327         return self.db._db.view(self.classname).ordered(1)
1328     def getindexview(self, RW=0):
1329         return self.db._db.view("_%s" % self.classname).ordered(1)
1331 def _fetchML(sv):
1332     l = []
1333     for row in sv:
1334         if row.fid:
1335             l.append(str(row.fid))
1336     return l
1338 def _fetchPW(s):
1339     ''' Convert to a password.Password unless the password is '' which is
1340         our sentinel for "unset".
1341     '''
1342     if s == '':
1343         return None
1344     p = password.Password()
1345     p.unpack(s)
1346     return p
1348 def _fetchLink(n):
1349     ''' Return None if the link is 0 - otherwise strify it.
1350     '''
1351     return n and str(n) or None
1353 def _fetchDate(n):
1354     ''' Convert the timestamp to a date.Date instance - unless it's 0 which
1355         is our sentinel for "unset".
1356     '''
1357     if n == 0:
1358         return None
1359     return date.Date(time.gmtime(n))
1361 def _fetchInterval(n):
1362     ''' Convert to a date.Interval unless the interval is '' which is our
1363         sentinel for "unset".
1364     '''
1365     if n == '':
1366         return None
1367     return date.Interval(n)
1369 _converters = {
1370     hyperdb.Date   : _fetchDate,
1371     hyperdb.Link   : _fetchLink,
1372     hyperdb.Multilink : _fetchML,
1373     hyperdb.Interval  : _fetchInterval,
1374     hyperdb.Password  : _fetchPW,
1375     hyperdb.Boolean   : lambda n: n,
1376     hyperdb.Number    : lambda n: n,
1377     hyperdb.String    : lambda s: s and str(s) or None,
1378 }                
1380 class FileName(hyperdb.String):
1381     isfilename = 1            
1383 _typmap = {
1384     FileName : 'S',
1385     hyperdb.String : 'S',
1386     hyperdb.Date   : 'I',
1387     hyperdb.Link   : 'I',
1388     hyperdb.Multilink : 'V',
1389     hyperdb.Interval  : 'S',
1390     hyperdb.Password  : 'S',
1391     hyperdb.Boolean   : 'I',
1392     hyperdb.Number    : 'I',
1394 class FileClass(Class, hyperdb.FileClass):
1395     ''' like Class but with a content property
1396     '''
1397     default_mime_type = 'text/plain'
1398     def __init__(self, db, classname, **properties):
1399         properties['content'] = FileName()
1400         if not properties.has_key('type'):
1401             properties['type'] = hyperdb.String()
1402         Class.__init__(self, db, classname, **properties)
1404     def get(self, nodeid, propname, default=_marker, cache=1):
1405         x = Class.get(self, nodeid, propname, default)
1406         poss_msg = 'Possibly an access right configuration problem.'
1407         if propname == 'content':
1408             if x.startswith('file:'):
1409                 fnm = x[5:]
1410                 try:
1411                     x = open(fnm, 'rb').read()
1412                 except IOError, (strerror):
1413                     # XXX by catching this we donot see an error in the log.
1414                     return 'ERROR reading file: %s%s\n%s\n%s'%(
1415                             self.classname, nodeid, poss_msg, strerror)
1416         return x
1418     def create(self, **propvalues):
1419         self.fireAuditors('create', None, propvalues)
1420         content = propvalues['content']
1421         del propvalues['content']
1422         newid = Class.create_inner(self, **propvalues)
1423         if not content:
1424             return newid
1425         nm = bnm = '%s%s' % (self.classname, newid)
1426         sd = str(int(int(newid) / 1000))
1427         d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
1428         if not os.path.exists(d):
1429             os.makedirs(d)
1430         nm = os.path.join(d, nm)
1431         open(nm, 'wb').write(content)
1432         self.set(newid, content = 'file:'+nm)
1433         mimetype = propvalues.get('type', self.default_mime_type)
1434         self.db.indexer.add_text((self.classname, newid, 'content'), content,
1435             mimetype)
1436         def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
1437             action1(fnm)
1438         self.rollbackaction(undo)
1439         return newid
1441     def index(self, nodeid):
1442         Class.index(self, nodeid)
1443         mimetype = self.get(nodeid, 'type')
1444         if not mimetype:
1445             mimetype = self.default_mime_type
1446         self.db.indexer.add_text((self.classname, nodeid, 'content'),
1447                     self.get(nodeid, 'content'), mimetype)
1448  
1449 class IssueClass(Class, roundupdb.IssueClass):
1450     ''' The newly-created class automatically includes the "messages",
1451         "files", "nosy", and "superseder" properties.  If the 'properties'
1452         dictionary attempts to specify any of these properties or a
1453         "creation" or "activity" property, a ValueError is raised.
1454     '''
1455     def __init__(self, db, classname, **properties):
1456         if not properties.has_key('title'):
1457             properties['title'] = hyperdb.String(indexme='yes')
1458         if not properties.has_key('messages'):
1459             properties['messages'] = hyperdb.Multilink("msg")
1460         if not properties.has_key('files'):
1461             properties['files'] = hyperdb.Multilink("file")
1462         if not properties.has_key('nosy'):
1463             # note: journalling is turned off as it really just wastes
1464             # space. this behaviour may be overridden in an instance
1465             properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
1466         if not properties.has_key('superseder'):
1467             properties['superseder'] = hyperdb.Multilink(classname)
1468         Class.__init__(self, db, classname, **properties)
1469         
1470 CURVERSION = 2
1472 class Indexer(indexer.Indexer):
1473     disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
1474     def __init__(self, path, datadb):
1475         self.path = os.path.join(path, 'index.mk4')
1476         self.db = metakit.storage(self.path, 1)
1477         self.datadb = datadb
1478         self.reindex = 0
1479         v = self.db.view('version')
1480         if not v.structure():
1481             v = self.db.getas('version[vers:I]')
1482             self.db.commit()
1483             v.append(vers=CURVERSION)
1484             self.reindex = 1
1485         elif v[0].vers != CURVERSION:
1486             v[0].vers = CURVERSION
1487             self.reindex = 1
1488         if self.reindex:
1489             self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
1490             self.db.getas('index[word:S,hits[pos:I]]')
1491             self.db.commit()
1492             self.reindex = 1
1493         self.changed = 0
1494         self.propcache = {}
1496     def force_reindex(self):
1497         v = self.db.view('ids')
1498         v[:] = []
1499         v = self.db.view('index')
1500         v[:] = []
1501         self.db.commit()
1502         self.reindex = 1
1504     def should_reindex(self):
1505         return self.reindex
1507     def _getprops(self, classname):
1508         props = self.propcache.get(classname, None)
1509         if props is None:
1510             props = self.datadb.view(classname).structure()
1511             props = [prop.name for prop in props]
1512             self.propcache[classname] = props
1513         return props
1515     def _getpropid(self, classname, propname):
1516         return self._getprops(classname).index(propname)
1518     def _getpropname(self, classname, propid):
1519         return self._getprops(classname)[propid]
1521     def add_text(self, identifier, text, mime_type='text/plain'):
1522         if mime_type != 'text/plain':
1523             return
1524         classname, nodeid, property = identifier
1525         tbls = self.datadb.view('tables')
1526         tblid = tbls.find(name=classname)
1527         if tblid < 0:
1528             raise KeyError, "unknown class %r"%classname
1529         nodeid = int(nodeid)
1530         propid = self._getpropid(classname, property)
1531         ids = self.db.view('ids')
1532         oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
1533         if oldpos > -1:
1534             ids[oldpos].ignore = 1
1535             self.changed = 1
1536         pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
1537         
1538         wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
1539         words = {}
1540         for word in wordlist:
1541             if not self.disallows.has_key(word):
1542                 words[word] = 1
1543         words = words.keys()
1544         
1545         index = self.db.view('index').ordered(1)
1546         for word in words:
1547             ndx = index.find(word=word)
1548             if ndx < 0:
1549                 index.append(word=word)
1550                 ndx = index.find(word=word)
1551             index[ndx].hits.append(pos=pos)
1552             self.changed = 1
1554     def find(self, wordlist):
1555         hits = None
1556         index = self.db.view('index').ordered(1)
1557         for word in wordlist:
1558             word = word.upper()
1559             if not 2 < len(word) < 26:
1560                 continue
1561             ndx = index.find(word=word)
1562             if ndx < 0:
1563                 return {}
1564             if hits is None:
1565                 hits = index[ndx].hits
1566             else:
1567                 hits = hits.intersect(index[ndx].hits)
1568             if len(hits) == 0:
1569                 return {}
1570         if hits is None:
1571             return {}
1572         rslt = {}
1573         ids = self.db.view('ids').remapwith(hits)
1574         tbls = self.datadb.view('tables')
1575         for i in range(len(ids)):
1576             hit = ids[i]
1577             if not hit.ignore:
1578                 classname = tbls[hit.tblid].name
1579                 nodeid = str(hit.nodeid)
1580                 property = self._getpropname(classname, hit.propid)
1581                 rslt[i] = (classname, nodeid, property)
1582         return rslt
1584     def save_index(self):
1585         if self.changed:
1586             self.db.commit()
1587         self.changed = 0
1589     def rollback(self):
1590         if self.changed:
1591             self.db.rollback()
1592             self.db = metakit.storage(self.path, 1)
1593         self.changed = 0