1515# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
1616# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
1717#
18- #$Id: back_anydbm.py,v 1.51 2002-07-18 23:07:08 richard Exp $
18+ #$Id: back_anydbm.py,v 1.52 2002-07-19 03:36:34 richard Exp $
1919'''
2020This module defines a backend that saves the hyperdatabase in a database
2121chosen by anydbm. It is guaranteed to always be available in python
@@ -63,6 +63,7 @@ def __init__(self, config, journaltag=None):
6363 self .cache = {} # cache of nodes loaded or created
6464 self .dirtynodes = {} # keep track of the dirty nodes by class
6565 self .newnodes = {} # keep track of the new nodes by class
66+ self .destroyednodes = {}# keep track of the destroyed nodes by class
6667 self .transactions = []
6768 self .indexer = Indexer (self .dir )
6869 # ensure files are group readable and writable
@@ -141,7 +142,7 @@ def getclassdb(self, classname, mode='r'):
141142 '''
142143 if __debug__ :
143144 print >> hyperdb .DEBUG , 'getclassdb' , (self , classname , mode )
144- return self ._opendb ('nodes.%s' % classname , mode )
145+ return self .opendb ('nodes.%s' % classname , mode )
145146
146147 def determine_db_type (self , path ):
147148 ''' determine which DB wrote the class file
@@ -157,12 +158,12 @@ def determine_db_type(self, path):
157158 db_type = 'dbm'
158159 return db_type
159160
160- def _opendb (self , name , mode ):
161+ def opendb (self , name , mode ):
161162 '''Low-level database opener that gets around anydbm/dbm
162163 eccentricities.
163164 '''
164165 if __debug__ :
165- print >> hyperdb .DEBUG , '_opendb ' , (self , name , mode )
166+ print >> hyperdb .DEBUG , 'opendb ' , (self , name , mode )
166167
167168 # figure the class db type
168169 path = os .path .join (os .getcwd (), self .dir , name )
@@ -171,7 +172,7 @@ def _opendb(self, name, mode):
171172 # new database? let anydbm pick the best dbm
172173 if not db_type :
173174 if __debug__ :
174- print >> hyperdb .DEBUG , "_opendb anydbm.open(%r, 'n')" % path
175+ print >> hyperdb .DEBUG , "opendb anydbm.open(%r, 'n')" % path
175176 return anydbm .open (path , 'n' )
176177
177178 # open the database with the correct module
@@ -182,11 +183,11 @@ def _opendb(self, name, mode):
182183 "Couldn't open database - the required module '%s'" \
183184 " is not available" % db_type
184185 if __debug__ :
185- print >> hyperdb .DEBUG , "_opendb %r.open(%r, %r)" % (db_type , path ,
186+ print >> hyperdb .DEBUG , "opendb %r.open(%r, %r)" % (db_type , path ,
186187 mode )
187188 return dbm .open (path , mode )
188189
189- def _lockdb (self , name ):
190+ def lockdb (self , name ):
190191 ''' Lock a database file
191192 '''
192193 path = os .path .join (os .getcwd (), self .dir , '%s.lock' % name )
@@ -199,8 +200,8 @@ def newid(self, classname):
199200 ''' Generate a new id for the given class
200201 '''
201202 # open the ids DB - create if if doesn't exist
202- lock = self ._lockdb ('_ids' )
203- db = self ._opendb ('_ids' , 'c' )
203+ lock = self .lockdb ('_ids' )
204+ db = self .opendb ('_ids' , 'c' )
204205 if db .has_key (classname ):
205206 newid = db [classname ] = str (int (db [classname ]) + 1 )
206207 else :
@@ -239,7 +240,7 @@ def savenode(self, classname, nodeid, node):
239240 '''
240241 if __debug__ :
241242 print >> hyperdb .DEBUG , 'savenode' , (self , classname , nodeid , node )
242- self .transactions .append ((self ._doSaveNode , (classname , nodeid , node )))
243+ self .transactions .append ((self .doSaveNode , (classname , nodeid , node )))
243244
244245 def getnode (self , classname , nodeid , db = None , cache = 1 ):
245246 ''' get a node from the database
@@ -264,6 +265,11 @@ def getnode(self, classname, nodeid, db=None, cache=1):
264265 if not db .has_key (nodeid ):
265266 raise IndexError , "no such %s %s" % (classname , nodeid )
266267
268+ # check the uncommitted, destroyed nodes
269+ if (self .destroyednodes .has_key (classname ) and
270+ self .destroyednodes [classname ].has_key (nodeid )):
271+ raise IndexError , "no such %s %s" % (classname , nodeid )
272+
267273 # decode
268274 res = marshal .loads (db [nodeid ])
269275
@@ -276,6 +282,32 @@ def getnode(self, classname, nodeid, db=None, cache=1):
276282
277283 return res
278284
285+ def destroynode (self , classname , nodeid ):
286+ '''Remove a node from the database. Called exclusively by the
287+ destroy() method on Class.
288+ '''
289+ if __debug__ :
290+ print >> hyperdb .DEBUG , 'destroynode' , (self , classname , nodeid )
291+
292+ # remove from cache and newnodes if it's there
293+ if (self .cache .has_key (classname ) and
294+ self .cache [classname ].has_key (nodeid )):
295+ del self .cache [classname ][nodeid ]
296+ if (self .newnodes .has_key (classname ) and
297+ self .newnodes [classname ].has_key (nodeid )):
298+ del self .newnodes [classname ][nodeid ]
299+
300+ # see if there's any obvious commit actions that we should get rid of
301+ for entry in self .transactions [:]:
302+ if entry [1 ][:2 ] == (classname , nodeid ):
303+ self .transactions .remove (entry )
304+
305+ # add to the destroyednodes map
306+ self .destroyednodes .setdefault (classname , {})[nodeid ] = 1
307+
308+ # add the destroy commit action
309+ self .transactions .append ((self .doDestroyNode , (classname , nodeid )))
310+
279311 def serialise (self , classname , node ):
280312 '''Copy the node contents, converting non-marshallable data into
281313 marshallable data.
@@ -357,8 +389,14 @@ def hasnode(self, classname, nodeid, db=None):
357389 def countnodes (self , classname , db = None ):
358390 if __debug__ :
359391 print >> hyperdb .DEBUG , 'countnodes' , (self , classname , db )
360- # include the new nodes not saved to the DB yet
361- count = len (self .newnodes .get (classname , {}))
392+
393+ count = 0
394+
395+ # include the uncommitted nodes
396+ if self .newnodes .has_key (classname ):
397+ count += len (self .newnodes [classname ])
398+ if self .destroyednodes .has_key (classname ):
399+ count -= len (self .destroyednodes [classname ])
362400
363401 # and count those in the DB
364402 if db is None :
@@ -369,12 +407,23 @@ def countnodes(self, classname, db=None):
369407 def getnodeids (self , classname , db = None ):
370408 if __debug__ :
371409 print >> hyperdb .DEBUG , 'getnodeids' , (self , classname , db )
410+
411+ res = []
412+
372413 # start off with the new nodes
373- res = self .newnodes .get (classname , {}).keys ()
414+ if self .newnodes .has_key (classname ):
415+ res += self .newnodes [classname ].keys ()
374416
375417 if db is None :
376418 db = self .getclassdb (classname )
377419 res = res + db .keys ()
420+
421+ # remove the uncommitted, destroyed nodes
422+ if self .destroyednodes .has_key (classname ):
423+ for nodeid in self .destroyednodes [classname ].keys ():
424+ if db .has_key (nodeid ):
425+ res .remove (nodeid )
426+
378427 return res
379428
380429
@@ -396,33 +445,36 @@ def addjournal(self, classname, nodeid, action, params):
396445 if __debug__ :
397446 print >> hyperdb .DEBUG , 'addjournal' , (self , classname , nodeid ,
398447 action , params )
399- self .transactions .append ((self ._doSaveJournal , (classname , nodeid ,
448+ self .transactions .append ((self .doSaveJournal , (classname , nodeid ,
400449 action , params )))
401450
402451 def getjournal (self , classname , nodeid ):
403452 ''' get the journal for id
453+
454+ Raise IndexError if the node doesn't exist (as per history()'s
455+ API)
404456 '''
405457 if __debug__ :
406458 print >> hyperdb .DEBUG , 'getjournal' , (self , classname , nodeid )
407459 # attempt to open the journal - in some rare cases, the journal may
408460 # not exist
409461 try :
410- db = self ._opendb ('journals.%s' % classname , 'r' )
462+ db = self .opendb ('journals.%s' % classname , 'r' )
411463 except anydbm .error , error :
412- if str (error ) == "need 'c' or 'n' flag to open new db" : return []
413- elif error .args [0 ] != 2 : raise
414- return []
464+ if str (error ) == "need 'c' or 'n' flag to open new db" :
465+ raise IndexError , 'no such %s %s' % (classname , nodeid )
466+ elif error .args [0 ] != 2 :
467+ raise
468+ raise IndexError , 'no such %s %s' % (classname , nodeid )
415469 try :
416470 journal = marshal .loads (db [nodeid ])
417471 except KeyError :
418472 db .close ()
419- raise KeyError , 'no such %s %s' % (classname , nodeid )
473+ raise IndexError , 'no such %s %s' % (classname , nodeid )
420474 db .close ()
421475 res = []
422- for entry in journal :
423- (nodeid , date_stamp , user , action , params ) = entry
424- date_obj = date .Date (date_stamp )
425- res .append ((nodeid , date_obj , user , action , params ))
476+ for nodeid , date_stamp , user , action , params in journal :
477+ res .append ((nodeid , date .Date (date_stamp ), user , action , params ))
426478 return res
427479
428480 def pack (self , pack_before ):
@@ -440,7 +492,7 @@ def pack(self, pack_before):
440492 db_name = 'journals.%s' % classname
441493 path = os .path .join (os .getcwd (), self .dir , classname )
442494 db_type = self .determine_db_type (path )
443- db = self ._opendb (db_name , 'w' )
495+ db = self .opendb (db_name , 'w' )
444496
445497 for key in db .keys ():
446498 journal = marshal .loads (db [key ])
@@ -503,27 +555,41 @@ def commit(self):
503555 self .cache = {}
504556 self .dirtynodes = {}
505557 self .newnodes = {}
558+ self .destroyednodes = {}
506559 self .transactions = []
507560
508- def _doSaveNode (self , classname , nodeid , node ):
561+ def getCachedClassDB (self , classname ):
562+ ''' get the class db, looking in our cache of databases for commit
563+ '''
564+ # get the database handle
565+ db_name = 'nodes.%s' % classname
566+ if not self .databases .has_key (db_name ):
567+ self .databases [db_name ] = self .getclassdb (classname , 'c' )
568+ return self .databases [db_name ]
569+
570+ def doSaveNode (self , classname , nodeid , node ):
509571 if __debug__ :
510- print >> hyperdb .DEBUG , '_doSaveNode ' , (self , classname , nodeid ,
572+ print >> hyperdb .DEBUG , 'doSaveNode ' , (self , classname , nodeid ,
511573 node )
512574
513- # get the database handle
514- db_name = 'nodes.%s' % classname
515- if self .databases .has_key (db_name ):
516- db = self .databases [db_name ]
517- else :
518- db = self .databases [db_name ] = self .getclassdb (classname , 'c' )
575+ db = self .getCachedClassDB (classname )
519576
520577 # now save the marshalled data
521578 db [nodeid ] = marshal .dumps (self .serialise (classname , node ))
522579
523580 # return the classname, nodeid so we reindex this content
524581 return (classname , nodeid )
525582
526- def _doSaveJournal (self , classname , nodeid , action , params ):
583+ def getCachedJournalDB (self , classname ):
584+ ''' get the journal db, looking in our cache of databases for commit
585+ '''
586+ # get the database handle
587+ db_name = 'journals.%s' % classname
588+ if not self .databases .has_key (db_name ):
589+ self .databases [db_name ] = self .opendb (db_name , 'c' )
590+ return self .databases [db_name ]
591+
592+ def doSaveJournal (self , classname , nodeid , action , params ):
527593 # serialise first
528594 if action in ('set' , 'create' ):
529595 params = self .serialise (classname , params )
@@ -533,14 +599,9 @@ def _doSaveJournal(self, classname, nodeid, action, params):
533599 params )
534600
535601 if __debug__ :
536- print >> hyperdb .DEBUG , '_doSaveJournal ' , entry
602+ print >> hyperdb .DEBUG , 'doSaveJournal ' , entry
537603
538- # get the database handle
539- db_name = 'journals.%s' % classname
540- if self .databases .has_key (db_name ):
541- db = self .databases [db_name ]
542- else :
543- db = self .databases [db_name ] = self ._opendb (db_name , 'c' )
604+ db = self .getCachedJournalDB (classname )
544605
545606 # now insert the journal entry
546607 if db .has_key (nodeid ):
@@ -553,18 +614,36 @@ def _doSaveJournal(self, classname, nodeid, action, params):
553614
554615 db [nodeid ] = marshal .dumps (l )
555616
617+ def doDestroyNode (self , classname , nodeid ):
618+ if __debug__ :
619+ print >> hyperdb .DEBUG , 'doDestroyNode' , (self , classname , nodeid )
620+
621+ # delete from the class database
622+ db = self .getCachedClassDB (classname )
623+ if db .has_key (nodeid ):
624+ del db [nodeid ]
625+
626+ # delete from the database
627+ db = self .getCachedJournalDB (classname )
628+ if db .has_key (nodeid ):
629+ del db [nodeid ]
630+
631+ # return the classname, nodeid so we reindex this content
632+ return (classname , nodeid )
633+
556634 def rollback (self ):
557635 ''' Reverse all actions from the current transaction.
558636 '''
559637 if __debug__ :
560638 print >> hyperdb .DEBUG , 'rollback' , (self , )
561639 for method , args in self .transactions :
562640 # delete temporary files
563- if method == self ._doStoreFile :
564- self ._rollbackStoreFile (* args )
641+ if method == self .doStoreFile :
642+ self .rollbackStoreFile (* args )
565643 self .cache = {}
566644 self .dirtynodes = {}
567645 self .newnodes = {}
646+ self .destroyednodes = {}
568647 self .transactions = []
569648
570649_marker = []
@@ -1075,6 +1154,23 @@ def retire(self, nodeid):
10751154
10761155 self .fireReactors ('retire' , nodeid , None )
10771156
1157+ def destroy (self , nodeid ):
1158+ """Destroy a node.
1159+
1160+ WARNING: this method should never be used except in extremely rare
1161+ situations where there could never be links to the node being
1162+ deleted
1163+ WARNING: use retire() instead
1164+ WARNING: the properties of this node will not be available ever again
1165+ WARNING: really, use retire() instead
1166+
1167+ Well, I think that's enough warnings. This method exists mostly to
1168+ support the session storage of the cgi interface.
1169+ """
1170+ if self .db .journaltag is None :
1171+ raise DatabaseError , 'Database open read-only'
1172+ self .db .destroynode (self .classname , nodeid )
1173+
10781174 def history (self , nodeid ):
10791175 """Retrieve the journal of edits on a particular node.
10801176
@@ -1550,9 +1646,15 @@ def index(self, nodeid):
15501646 # find all the String properties that have indexme
15511647 for prop , propclass in self .getprops ().items ():
15521648 if isinstance (propclass , String ) and propclass .indexme :
1553- # and index them under (classname, nodeid, property)
1554- self .db .indexer .add_text ((self .classname , nodeid , prop ),
1555- str (self .get (nodeid , prop )))
1649+ try :
1650+ value = str (self .get (nodeid , prop ))
1651+ except IndexError :
1652+ # node no longer exists - entry should be removed
1653+ self .db .indexer .purge_entry ((self .classname , nodeid , prop ))
1654+ else :
1655+ # and index them under (classname, nodeid, property)
1656+ self .db .indexer .add_text ((self .classname , nodeid , prop ),
1657+ value )
15561658
15571659 #
15581660 # Detector interface
@@ -1676,6 +1778,9 @@ def __init__(self, db, classname, **properties):
16761778
16771779#
16781780#$Log: not supported by cvs2svn $
1781+ #Revision 1.51 2002/07/18 23:07:08 richard
1782+ #Unit tests and a few fixes.
1783+ #
16791784#Revision 1.50 2002/07/18 11:50:58 richard
16801785#added tests for number type too
16811786#
0 commit comments