1 # $Id: rdbms_common.py,v 1.66 2003-10-25 22:53:26 richard Exp $
2 ''' Relational database (SQL) backend common code.
4 Basics:
6 - map roundup classes to relational tables
7 - automatically detect schema changes and modify the table schemas
8 appropriately (we store the "database version" of the schema in the
9 database itself as the only row of the "schema" table)
10 - multilinks (which represent a many-to-many relationship) are handled through
11 intermediate tables
12 - journals are stored adjunct to the per-class tables
13 - table names and columns have "_" prepended so the names can't clash with
14 restricted names (like "order")
15 - retirement is determined by the __retired__ column being true
17 Database-specific changes may generally be pushed out to the overridable
18 sql_* methods, since everything else should be fairly generic. There's
19 probably a bit of work to be done if a database is used that actually
20 honors column typing, since the initial databases don't (sqlite stores
21 everything as a string.)
22 '''
24 # standard python modules
25 import sys, os, time, re, errno, weakref, copy
27 # roundup modules
28 from roundup import hyperdb, date, password, roundupdb, security
29 from roundup.hyperdb import String, Password, Date, Interval, Link, \
30 Multilink, DatabaseError, Boolean, Number, Node
31 from roundup.backends import locking
33 # support
34 from blobfiles import FileStorage
35 from roundup.indexer import Indexer
36 from sessions import Sessions, OneTimeKeys
37 from roundup.date import Range
39 # number of rows to keep in memory
40 ROW_CACHE_SIZE = 100
42 class Database(FileStorage, hyperdb.Database, roundupdb.Database):
43 ''' Wrapper around an SQL database that presents a hyperdb interface.
45 - some functionality is specific to the actual SQL database, hence
46 the sql_* methods that are NotImplemented
47 - we keep a cache of the latest ROW_CACHE_SIZE row fetches.
48 '''
49 def __init__(self, config, journaltag=None):
50 ''' Open the database and load the schema from it.
51 '''
52 self.config, self.journaltag = config, journaltag
53 self.dir = config.DATABASE
54 self.classes = {}
55 self.indexer = Indexer(self.dir)
56 self.sessions = Sessions(self.config)
57 self.otks = OneTimeKeys(self.config)
58 self.security = security.Security(self)
60 # additional transaction support for external files and the like
61 self.transactions = []
63 # keep a cache of the N most recently retrieved rows of any kind
64 # (classname, nodeid) = row
65 self.cache = {}
66 self.cache_lru = []
68 # database lock
69 self.lockfile = None
71 # open a connection to the database, creating the "conn" attribute
72 self.open_connection()
74 def clearCache(self):
75 self.cache = {}
76 self.cache_lru = []
78 def open_connection(self):
79 ''' Open a connection to the database, creating it if necessary
80 '''
81 raise NotImplemented
83 def sql(self, sql, args=None):
84 ''' Execute the sql with the optional args.
85 '''
86 if __debug__:
87 print >>hyperdb.DEBUG, (self, sql, args)
88 if args:
89 self.cursor.execute(sql, args)
90 else:
91 self.cursor.execute(sql)
93 def sql_fetchone(self):
94 ''' Fetch a single row. If there's nothing to fetch, return None.
95 '''
96 raise NotImplemented
98 def sql_stringquote(self, value):
99 ''' Quote the string so it's safe to put in the 'sql quotes'
100 '''
101 return re.sub("'", "''", str(value))
103 def save_dbschema(self, schema):
104 ''' Save the schema definition that the database currently implements
105 '''
106 raise NotImplemented
108 def load_dbschema(self):
109 ''' Load the schema definition that the database currently implements
110 '''
111 raise NotImplemented
113 def post_init(self):
114 ''' Called once the schema initialisation has finished.
116 We should now confirm that the schema defined by our "classes"
117 attribute actually matches the schema in the database.
118 '''
119 # now detect changes in the schema
120 save = 0
121 for classname, spec in self.classes.items():
122 if self.database_schema.has_key(classname):
123 dbspec = self.database_schema[classname]
124 if self.update_class(spec, dbspec):
125 self.database_schema[classname] = spec.schema()
126 save = 1
127 else:
128 self.create_class(spec)
129 self.database_schema[classname] = spec.schema()
130 save = 1
132 for classname, spec in self.database_schema.items():
133 if not self.classes.has_key(classname):
134 self.drop_class(classname, spec)
135 del self.database_schema[classname]
136 save = 1
138 # update the database version of the schema
139 if save:
140 self.sql('delete from schema')
141 self.save_dbschema(self.database_schema)
143 # reindex the db if necessary
144 if self.indexer.should_reindex():
145 self.reindex()
147 # commit
148 self.conn.commit()
150 def refresh_database(self):
151 # now detect changes in the schema
152 for classname, spec in self.classes.items():
153 dbspec = self.database_schema[classname]
154 self.update_class(spec, dbspec, force=1)
155 self.database_schema[classname] = spec.schema()
156 # update the database version of the schema
157 self.sql('delete from schema')
158 self.save_dbschema(self.database_schema)
159 # reindex the db
160 self.reindex()
161 # commit
162 self.conn.commit()
165 def reindex(self):
166 for klass in self.classes.values():
167 for nodeid in klass.list():
168 klass.index(nodeid)
169 self.indexer.save_index()
171 def determine_columns(self, properties):
172 ''' Figure the column names and multilink properties from the spec
174 "properties" is a list of (name, prop) where prop may be an
175 instance of a hyperdb "type" _or_ a string repr of that type.
176 '''
177 cols = ['_activity', '_creator', '_creation']
178 mls = []
179 # add the multilinks separately
180 for col, prop in properties:
181 if isinstance(prop, Multilink):
182 mls.append(col)
183 elif isinstance(prop, type('')) and prop.find('Multilink') != -1:
184 mls.append(col)
185 else:
186 cols.append('_'+col)
187 cols.sort()
188 return cols, mls
190 def update_class(self, spec, old_spec, force=0):
191 ''' Determine the differences between the current spec and the
192 database version of the spec, and update where necessary.
193 If 'force' is true, update the database anyway.
194 '''
195 new_has = spec.properties.has_key
196 new_spec = spec.schema()
197 new_spec[1].sort()
198 old_spec[1].sort()
199 if not force and new_spec == old_spec:
200 # no changes
201 return 0
203 if __debug__:
204 print >>hyperdb.DEBUG, 'update_class FIRING'
206 # detect multilinks that have been removed, and drop their table
207 old_has = {}
208 for name,prop in old_spec[1]:
209 old_has[name] = 1
210 if (force or not new_has(name)) and isinstance(prop, Multilink):
211 # it's a multilink, and it's been removed - drop the old
212 # table. First drop indexes.
213 index_sqls = [ 'drop index %s_%s_l_idx'%(spec.classname, ml),
214 'drop index %s_%s_n_idx'%(spec.classname, ml) ]
215 for index_sql in index_sqls:
216 if __debug__:
217 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
218 try:
219 self.cursor.execute(index_sql)
220 except:
221 # The database may not actually have any indexes.
222 # assume the worst.
223 pass
224 sql = 'drop table %s_%s'%(spec.classname, prop)
225 if __debug__:
226 print >>hyperdb.DEBUG, 'update_class', (self, sql)
227 self.cursor.execute(sql)
228 continue
229 old_has = old_has.has_key
231 # now figure how we populate the new table
232 fetch = ['_activity', '_creation', '_creator']
233 properties = spec.getprops()
234 for propname,x in new_spec[1]:
235 prop = properties[propname]
236 if isinstance(prop, Multilink):
237 if force or not old_has(propname):
238 # we need to create the new table
239 self.create_multilink_table(spec, propname)
240 elif old_has(propname):
241 # we copy this col over from the old table
242 fetch.append('_'+propname)
244 # select the data out of the old table
245 fetch.append('id')
246 fetch.append('__retired__')
247 fetchcols = ','.join(fetch)
248 cn = spec.classname
249 sql = 'select %s from _%s'%(fetchcols, cn)
250 if __debug__:
251 print >>hyperdb.DEBUG, 'update_class', (self, sql)
252 self.cursor.execute(sql)
253 olddata = self.cursor.fetchall()
255 # drop the old table indexes first
256 index_sqls = [ 'drop index _%s_id_idx'%cn,
257 'drop index _%s_retired_idx'%cn ]
258 if old_spec[0]:
259 index_sqls.append('drop index _%s_%s_idx'%(cn, old_spec[0]))
260 for index_sql in index_sqls:
261 if __debug__:
262 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
263 try:
264 self.cursor.execute(index_sql)
265 except:
266 # The database may not actually have any indexes.
267 # assume the worst.
268 pass
270 # drop the old table
271 self.cursor.execute('drop table _%s'%cn)
273 # create the new table
274 self.create_class_table(spec)
276 if olddata:
277 # do the insert
278 args = ','.join([self.arg for x in fetch])
279 sql = 'insert into _%s (%s) values (%s)'%(cn, fetchcols, args)
280 if __debug__:
281 print >>hyperdb.DEBUG, 'update_class', (self, sql, olddata[0])
282 for entry in olddata:
283 self.cursor.execute(sql, tuple(entry))
285 return 1
287 def create_class_table(self, spec):
288 ''' create the class table for the given spec
289 '''
290 cols, mls = self.determine_columns(spec.properties.items())
292 # add on our special columns
293 cols.append('id')
294 cols.append('__retired__')
296 # create the base table
297 scols = ','.join(['%s varchar'%x for x in cols])
298 sql = 'create table _%s (%s)'%(spec.classname, scols)
299 if __debug__:
300 print >>hyperdb.DEBUG, 'create_class', (self, sql)
301 self.cursor.execute(sql)
303 # create id index
304 index_sql1 = 'create index _%s_id_idx on _%s(id)'%(
305 spec.classname, spec.classname)
306 if __debug__:
307 print >>hyperdb.DEBUG, 'create_index', (self, index_sql1)
308 self.cursor.execute(index_sql1)
310 # create __retired__ index
311 index_sql2 = 'create index _%s_retired_idx on _%s(__retired__)'%(
312 spec.classname, spec.classname)
313 if __debug__:
314 print >>hyperdb.DEBUG, 'create_index', (self, index_sql2)
315 self.cursor.execute(index_sql2)
317 # create index for key property
318 if spec.key:
319 if __debug__:
320 print >>hyperdb.DEBUG, 'update_class setting keyprop %r'% \
321 spec.key
322 index_sql3 = 'create index _%s_%s_idx on _%s(_%s)'%(
323 spec.classname, spec.key,
324 spec.classname, spec.key)
325 if __debug__:
326 print >>hyperdb.DEBUG, 'create_index', (self, index_sql3)
327 self.cursor.execute(index_sql3)
329 return cols, mls
331 def create_journal_table(self, spec):
332 ''' create the journal table for a class given the spec and
333 already-determined cols
334 '''
335 # journal table
336 cols = ','.join(['%s varchar'%x
337 for x in 'nodeid date tag action params'.split()])
338 sql = 'create table %s__journal (%s)'%(spec.classname, cols)
339 if __debug__:
340 print >>hyperdb.DEBUG, 'create_class', (self, sql)
341 self.cursor.execute(sql)
343 # index on nodeid
344 index_sql = 'create index %s_journ_idx on %s__journal(nodeid)'%(
345 spec.classname, spec.classname)
346 if __debug__:
347 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
348 self.cursor.execute(index_sql)
350 def create_multilink_table(self, spec, ml):
351 ''' Create a multilink table for the "ml" property of the class
352 given by the spec
353 '''
354 # create the table
355 sql = 'create table %s_%s (linkid varchar, nodeid varchar)'%(
356 spec.classname, ml)
357 if __debug__:
358 print >>hyperdb.DEBUG, 'create_class', (self, sql)
359 self.cursor.execute(sql)
361 # create index on linkid
362 index_sql = 'create index %s_%s_l_idx on %s_%s(linkid)'%(
363 spec.classname, ml, spec.classname, ml)
364 if __debug__:
365 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
366 self.cursor.execute(index_sql)
368 # create index on nodeid
369 index_sql = 'create index %s_%s_n_idx on %s_%s(nodeid)'%(
370 spec.classname, ml, spec.classname, ml)
371 if __debug__:
372 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
373 self.cursor.execute(index_sql)
375 def create_class(self, spec):
376 ''' Create a database table according to the given spec.
377 '''
378 cols, mls = self.create_class_table(spec)
379 self.create_journal_table(spec)
381 # now create the multilink tables
382 for ml in mls:
383 self.create_multilink_table(spec, ml)
385 # ID counter
386 sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
387 vals = (spec.classname, 1)
388 if __debug__:
389 print >>hyperdb.DEBUG, 'create_class', (self, sql, vals)
390 self.cursor.execute(sql, vals)
392 def drop_class(self, cn, spec):
393 ''' Drop the given table from the database.
395 Drop the journal and multilink tables too.
396 '''
397 properties = spec[1]
398 # figure the multilinks
399 mls = []
400 for propanme, prop in properties:
401 if isinstance(prop, Multilink):
402 mls.append(propname)
404 index_sqls = [ 'drop index _%s_id_idx'%cn,
405 'drop index _%s_retired_idx'%cn,
406 'drop index %s_journ_idx'%cn ]
407 if spec[0]:
408 index_sqls.append('drop index _%s_%s_idx'%(cn, spec[0]))
409 for index_sql in index_sqls:
410 if __debug__:
411 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
412 try:
413 self.cursor.execute(index_sql)
414 except:
415 # The database may not actually have any indexes.
416 # assume the worst.
417 pass
419 sql = 'drop table _%s'%cn
420 if __debug__:
421 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
422 self.cursor.execute(sql)
424 sql = 'drop table %s__journal'%cn
425 if __debug__:
426 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
427 self.cursor.execute(sql)
429 for ml in mls:
430 index_sqls = [
431 'drop index %s_%s_n_idx'%(cn, ml),
432 'drop index %s_%s_l_idx'%(cn, ml),
433 ]
434 for index_sql in index_sqls:
435 if __debug__:
436 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
437 try:
438 self.cursor.execute(index_sql)
439 except:
440 # The database may not actually have any indexes.
441 # assume the worst.
442 pass
443 sql = 'drop table %s_%s'%(spec.classname, ml)
444 if __debug__:
445 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
446 self.cursor.execute(sql)
448 #
449 # Classes
450 #
451 def __getattr__(self, classname):
452 ''' A convenient way of calling self.getclass(classname).
453 '''
454 if self.classes.has_key(classname):
455 if __debug__:
456 print >>hyperdb.DEBUG, '__getattr__', (self, classname)
457 return self.classes[classname]
458 raise AttributeError, classname
460 def addclass(self, cl):
461 ''' Add a Class to the hyperdatabase.
462 '''
463 if __debug__:
464 print >>hyperdb.DEBUG, 'addclass', (self, cl)
465 cn = cl.classname
466 if self.classes.has_key(cn):
467 raise ValueError, cn
468 self.classes[cn] = cl
470 def getclasses(self):
471 ''' Return a list of the names of all existing classes.
472 '''
473 if __debug__:
474 print >>hyperdb.DEBUG, 'getclasses', (self,)
475 l = self.classes.keys()
476 l.sort()
477 return l
479 def getclass(self, classname):
480 '''Get the Class object representing a particular class.
482 If 'classname' is not a valid class name, a KeyError is raised.
483 '''
484 if __debug__:
485 print >>hyperdb.DEBUG, 'getclass', (self, classname)
486 try:
487 return self.classes[classname]
488 except KeyError:
489 raise KeyError, 'There is no class called "%s"'%classname
491 def clear(self):
492 ''' Delete all database contents.
494 Note: I don't commit here, which is different behaviour to the
495 "nuke from orbit" behaviour in the *dbms.
496 '''
497 if __debug__:
498 print >>hyperdb.DEBUG, 'clear', (self,)
499 for cn in self.classes.keys():
500 sql = 'delete from _%s'%cn
501 if __debug__:
502 print >>hyperdb.DEBUG, 'clear', (self, sql)
503 self.cursor.execute(sql)
505 #
506 # Node IDs
507 #
508 def newid(self, classname):
509 ''' Generate a new id for the given class
510 '''
511 # get the next ID
512 sql = 'select num from ids where name=%s'%self.arg
513 if __debug__:
514 print >>hyperdb.DEBUG, 'newid', (self, sql, classname)
515 self.cursor.execute(sql, (classname, ))
516 newid = self.cursor.fetchone()[0]
518 # update the counter
519 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
520 vals = (int(newid)+1, classname)
521 if __debug__:
522 print >>hyperdb.DEBUG, 'newid', (self, sql, vals)
523 self.cursor.execute(sql, vals)
525 # return as string
526 return str(newid)
528 def setid(self, classname, setid):
529 ''' Set the id counter: used during import of database
530 '''
531 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
532 vals = (setid, classname)
533 if __debug__:
534 print >>hyperdb.DEBUG, 'setid', (self, sql, vals)
535 self.cursor.execute(sql, vals)
537 #
538 # Nodes
539 #
540 def addnode(self, classname, nodeid, node):
541 ''' Add the specified node to its class's db.
542 '''
543 if __debug__:
544 print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
546 # determine the column definitions and multilink tables
547 cl = self.classes[classname]
548 cols, mls = self.determine_columns(cl.properties.items())
550 # we'll be supplied these props if we're doing an import
551 if not node.has_key('creator'):
552 # add in the "calculated" properties (dupe so we don't affect
553 # calling code's node assumptions)
554 node = node.copy()
555 node['creation'] = node['activity'] = date.Date()
556 node['creator'] = self.getuid()
558 # default the non-multilink columns
559 for col, prop in cl.properties.items():
560 if not node.has_key(col):
561 if isinstance(prop, Multilink):
562 node[col] = []
563 else:
564 node[col] = None
566 # clear this node out of the cache if it's in there
567 key = (classname, nodeid)
568 if self.cache.has_key(key):
569 del self.cache[key]
570 self.cache_lru.remove(key)
572 # make the node data safe for the DB
573 node = self.serialise(classname, node)
575 # make sure the ordering is correct for column name -> column value
576 vals = tuple([node[col[1:]] for col in cols]) + (nodeid, 0)
577 s = ','.join([self.arg for x in cols]) + ',%s,%s'%(self.arg, self.arg)
578 cols = ','.join(cols) + ',id,__retired__'
580 # perform the inserts
581 sql = 'insert into _%s (%s) values (%s)'%(classname, cols, s)
582 if __debug__:
583 print >>hyperdb.DEBUG, 'addnode', (self, sql, vals)
584 self.cursor.execute(sql, vals)
586 # insert the multilink rows
587 for col in mls:
588 t = '%s_%s'%(classname, col)
589 for entry in node[col]:
590 sql = 'insert into %s (linkid, nodeid) values (%s,%s)'%(t,
591 self.arg, self.arg)
592 self.sql(sql, (entry, nodeid))
594 # make sure we do the commit-time extra stuff for this node
595 self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
597 def setnode(self, classname, nodeid, values, multilink_changes):
598 ''' Change the specified node.
599 '''
600 if __debug__:
601 print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, values)
603 # clear this node out of the cache if it's in there
604 key = (classname, nodeid)
605 if self.cache.has_key(key):
606 del self.cache[key]
607 self.cache_lru.remove(key)
609 # add the special props
610 values = values.copy()
611 values['activity'] = date.Date()
613 # make db-friendly
614 values = self.serialise(classname, values)
616 cl = self.classes[classname]
617 cols = []
618 mls = []
619 # add the multilinks separately
620 props = cl.getprops()
621 for col in values.keys():
622 prop = props[col]
623 if isinstance(prop, Multilink):
624 mls.append(col)
625 else:
626 cols.append('_'+col)
627 cols.sort()
629 # if there's any updates to regular columns, do them
630 if cols:
631 # make sure the ordering is correct for column name -> column value
632 sqlvals = tuple([values[col[1:]] for col in cols]) + (nodeid,)
633 s = ','.join(['%s=%s'%(x, self.arg) for x in cols])
634 cols = ','.join(cols)
636 # perform the update
637 sql = 'update _%s set %s where id=%s'%(classname, s, self.arg)
638 if __debug__:
639 print >>hyperdb.DEBUG, 'setnode', (self, sql, sqlvals)
640 self.cursor.execute(sql, sqlvals)
642 # now the fun bit, updating the multilinks ;)
643 for col, (add, remove) in multilink_changes.items():
644 tn = '%s_%s'%(classname, col)
645 if add:
646 sql = 'insert into %s (nodeid, linkid) values (%s,%s)'%(tn,
647 self.arg, self.arg)
648 for addid in add:
649 self.sql(sql, (nodeid, addid))
650 if remove:
651 sql = 'delete from %s where nodeid=%s and linkid=%s'%(tn,
652 self.arg, self.arg)
653 for removeid in remove:
654 self.sql(sql, (nodeid, removeid))
656 # make sure we do the commit-time extra stuff for this node
657 self.transactions.append((self.doSaveNode, (classname, nodeid, values)))
659 def getnode(self, classname, nodeid):
660 ''' Get a node from the database.
661 '''
662 if __debug__:
663 print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid)
665 # see if we have this node cached
666 key = (classname, nodeid)
667 if self.cache.has_key(key):
668 # push us back to the top of the LRU
669 self.cache_lru.remove(key)
670 self.cache_lru.insert(0, key)
671 # return the cached information
672 return self.cache[key]
674 # figure the columns we're fetching
675 cl = self.classes[classname]
676 cols, mls = self.determine_columns(cl.properties.items())
677 scols = ','.join(cols)
679 # perform the basic property fetch
680 sql = 'select %s from _%s where id=%s'%(scols, classname, self.arg)
681 self.sql(sql, (nodeid,))
683 values = self.sql_fetchone()
684 if values is None:
685 raise IndexError, 'no such %s node %s'%(classname, nodeid)
687 # make up the node
688 node = {}
689 for col in range(len(cols)):
690 node[cols[col][1:]] = values[col]
692 # now the multilinks
693 for col in mls:
694 # get the link ids
695 sql = 'select linkid from %s_%s where nodeid=%s'%(classname, col,
696 self.arg)
697 self.cursor.execute(sql, (nodeid,))
698 # extract the first column from the result
699 node[col] = [x[0] for x in self.cursor.fetchall()]
701 # un-dbificate the node data
702 node = self.unserialise(classname, node)
704 # save off in the cache
705 key = (classname, nodeid)
706 self.cache[key] = node
707 # update the LRU
708 self.cache_lru.insert(0, key)
709 if len(self.cache_lru) > ROW_CACHE_SIZE:
710 del self.cache[self.cache_lru.pop()]
712 return node
714 def destroynode(self, classname, nodeid):
715 '''Remove a node from the database. Called exclusively by the
716 destroy() method on Class.
717 '''
718 if __debug__:
719 print >>hyperdb.DEBUG, 'destroynode', (self, classname, nodeid)
721 # make sure the node exists
722 if not self.hasnode(classname, nodeid):
723 raise IndexError, '%s has no node %s'%(classname, nodeid)
725 # see if we have this node cached
726 if self.cache.has_key((classname, nodeid)):
727 del self.cache[(classname, nodeid)]
729 # see if there's any obvious commit actions that we should get rid of
730 for entry in self.transactions[:]:
731 if entry[1][:2] == (classname, nodeid):
732 self.transactions.remove(entry)
734 # now do the SQL
735 sql = 'delete from _%s where id=%s'%(classname, self.arg)
736 self.sql(sql, (nodeid,))
738 # remove from multilnks
739 cl = self.getclass(classname)
740 x, mls = self.determine_columns(cl.properties.items())
741 for col in mls:
742 # get the link ids
743 sql = 'delete from %s_%s where nodeid=%s'%(classname, col, self.arg)
744 self.cursor.execute(sql, (nodeid,))
746 # remove journal entries
747 sql = 'delete from %s__journal where nodeid=%s'%(classname, self.arg)
748 self.sql(sql, (nodeid,))
750 def serialise(self, classname, node):
751 '''Copy the node contents, converting non-marshallable data into
752 marshallable data.
753 '''
754 if __debug__:
755 print >>hyperdb.DEBUG, 'serialise', classname, node
756 properties = self.getclass(classname).getprops()
757 d = {}
758 for k, v in node.items():
759 # if the property doesn't exist, or is the "retired" flag then
760 # it won't be in the properties dict
761 if not properties.has_key(k):
762 d[k] = v
763 continue
765 # get the property spec
766 prop = properties[k]
768 if isinstance(prop, Password) and v is not None:
769 d[k] = str(v)
770 elif isinstance(prop, Date) and v is not None:
771 d[k] = v.serialise()
772 elif isinstance(prop, Interval) and v is not None:
773 d[k] = v.serialise()
774 else:
775 d[k] = v
776 return d
778 def unserialise(self, classname, node):
779 '''Decode the marshalled node data
780 '''
781 if __debug__:
782 print >>hyperdb.DEBUG, 'unserialise', classname, node
783 properties = self.getclass(classname).getprops()
784 d = {}
785 for k, v in node.items():
786 # if the property doesn't exist, or is the "retired" flag then
787 # it won't be in the properties dict
788 if not properties.has_key(k):
789 d[k] = v
790 continue
792 # get the property spec
793 prop = properties[k]
795 if isinstance(prop, Date) and v is not None:
796 d[k] = date.Date(v)
797 elif isinstance(prop, Interval) and v is not None:
798 d[k] = date.Interval(v)
799 elif isinstance(prop, Password) and v is not None:
800 p = password.Password()
801 p.unpack(v)
802 d[k] = p
803 elif (isinstance(prop, Boolean) or isinstance(prop, Number)) and v is not None:
804 d[k]=float(v)
805 else:
806 d[k] = v
807 return d
809 def hasnode(self, classname, nodeid):
810 ''' Determine if the database has a given node.
811 '''
812 sql = 'select count(*) from _%s where id=%s'%(classname, self.arg)
813 if __debug__:
814 print >>hyperdb.DEBUG, 'hasnode', (self, sql, nodeid)
815 self.cursor.execute(sql, (nodeid,))
816 return int(self.cursor.fetchone()[0])
818 def countnodes(self, classname):
819 ''' Count the number of nodes that exist for a particular Class.
820 '''
821 sql = 'select count(*) from _%s'%classname
822 if __debug__:
823 print >>hyperdb.DEBUG, 'countnodes', (self, sql)
824 self.cursor.execute(sql)
825 return self.cursor.fetchone()[0]
827 def addjournal(self, classname, nodeid, action, params, creator=None,
828 creation=None):
829 ''' Journal the Action
830 'action' may be:
832 'create' or 'set' -- 'params' is a dictionary of property values
833 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
834 'retire' -- 'params' is None
835 '''
836 # serialise the parameters now if necessary
837 if isinstance(params, type({})):
838 if action in ('set', 'create'):
839 params = self.serialise(classname, params)
841 # handle supply of the special journalling parameters (usually
842 # supplied on importing an existing database)
843 if creator:
844 journaltag = creator
845 else:
846 journaltag = self.getuid()
847 if creation:
848 journaldate = creation.serialise()
849 else:
850 journaldate = date.Date().serialise()
852 # create the journal entry
853 cols = ','.join('nodeid date tag action params'.split())
855 if __debug__:
856 print >>hyperdb.DEBUG, 'addjournal', (nodeid, journaldate,
857 journaltag, action, params)
859 self.save_journal(classname, cols, nodeid, journaldate,
860 journaltag, action, params)
862 def save_journal(self, classname, cols, nodeid, journaldate,
863 journaltag, action, params):
864 ''' Save the journal entry to the database
865 '''
866 raise NotImplemented
868 def getjournal(self, classname, nodeid):
869 ''' get the journal for id
870 '''
871 # make sure the node exists
872 if not self.hasnode(classname, nodeid):
873 raise IndexError, '%s has no node %s'%(classname, nodeid)
875 cols = ','.join('nodeid date tag action params'.split())
876 return self.load_journal(classname, cols, nodeid)
878 def load_journal(self, classname, cols, nodeid):
879 ''' Load the journal from the database
880 '''
881 raise NotImplemented
883 def pack(self, pack_before):
884 ''' Delete all journal entries except "create" before 'pack_before'.
885 '''
886 # get a 'yyyymmddhhmmss' version of the date
887 date_stamp = pack_before.serialise()
889 # do the delete
890 for classname in self.classes.keys():
891 sql = "delete from %s__journal where date<%s and "\
892 "action<>'create'"%(classname, self.arg)
893 if __debug__:
894 print >>hyperdb.DEBUG, 'pack', (self, sql, date_stamp)
895 self.cursor.execute(sql, (date_stamp,))
897 def sql_commit(self):
898 ''' Actually commit to the database.
899 '''
900 self.conn.commit()
902 def commit(self):
903 ''' Commit the current transactions.
905 Save all data changed since the database was opened or since the
906 last commit() or rollback().
907 '''
908 if __debug__:
909 print >>hyperdb.DEBUG, 'commit', (self,)
911 # commit the database
912 self.sql_commit()
914 # now, do all the other transaction stuff
915 reindex = {}
916 for method, args in self.transactions:
917 reindex[method(*args)] = 1
919 # reindex the nodes that request it
920 for classname, nodeid in filter(None, reindex.keys()):
921 print >>hyperdb.DEBUG, 'commit.reindex', (classname, nodeid)
922 self.getclass(classname).index(nodeid)
924 # save the indexer state
925 self.indexer.save_index()
927 # clear out the transactions
928 self.transactions = []
930 def rollback(self):
931 ''' Reverse all actions from the current transaction.
933 Undo all the changes made since the database was opened or the last
934 commit() or rollback() was performed.
935 '''
936 if __debug__:
937 print >>hyperdb.DEBUG, 'rollback', (self,)
939 # roll back
940 self.conn.rollback()
942 # roll back "other" transaction stuff
943 for method, args in self.transactions:
944 # delete temporary files
945 if method == self.doStoreFile:
946 self.rollbackStoreFile(*args)
947 self.transactions = []
949 # clear the cache
950 self.clearCache()
952 def doSaveNode(self, classname, nodeid, node):
953 ''' dummy that just generates a reindex event
954 '''
955 # return the classname, nodeid so we reindex this content
956 return (classname, nodeid)
958 def close(self):
959 ''' Close off the connection.
960 '''
961 self.conn.close()
962 if self.lockfile is not None:
963 locking.release_lock(self.lockfile)
964 if self.lockfile is not None:
965 self.lockfile.close()
966 self.lockfile = None
968 #
969 # The base Class class
970 #
971 class Class(hyperdb.Class):
972 ''' The handle to a particular class of nodes in a hyperdatabase.
974 All methods except __repr__ and getnode must be implemented by a
975 concrete backend Class.
976 '''
978 def __init__(self, db, classname, **properties):
979 '''Create a new class with a given name and property specification.
981 'classname' must not collide with the name of an existing class,
982 or a ValueError is raised. The keyword arguments in 'properties'
983 must map names to property objects, or a TypeError is raised.
984 '''
985 if (properties.has_key('creation') or properties.has_key('activity')
986 or properties.has_key('creator')):
987 raise ValueError, '"creation", "activity" and "creator" are '\
988 'reserved'
990 self.classname = classname
991 self.properties = properties
992 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
993 self.key = ''
995 # should we journal changes (default yes)
996 self.do_journal = 1
998 # do the db-related init stuff
999 db.addclass(self)
1001 self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
1002 self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
1004 def schema(self):
1005 ''' A dumpable version of the schema that we can store in the
1006 database
1007 '''
1008 return (self.key, [(x, repr(y)) for x,y in self.properties.items()])
1010 def enableJournalling(self):
1011 '''Turn journalling on for this class
1012 '''
1013 self.do_journal = 1
1015 def disableJournalling(self):
1016 '''Turn journalling off for this class
1017 '''
1018 self.do_journal = 0
1020 # Editing nodes:
1021 def create(self, **propvalues):
1022 ''' Create a new node of this class and return its id.
1024 The keyword arguments in 'propvalues' map property names to values.
1026 The values of arguments must be acceptable for the types of their
1027 corresponding properties or a TypeError is raised.
1029 If this class has a key property, it must be present and its value
1030 must not collide with other key strings or a ValueError is raised.
1032 Any other properties on this class that are missing from the
1033 'propvalues' dictionary are set to None.
1035 If an id in a link or multilink property does not refer to a valid
1036 node, an IndexError is raised.
1037 '''
1038 self.fireAuditors('create', None, propvalues)
1039 newid = self.create_inner(**propvalues)
1040 self.fireReactors('create', newid, None)
1041 return newid
1043 def create_inner(self, **propvalues):
1044 ''' Called by create, in-between the audit and react calls.
1045 '''
1046 if propvalues.has_key('id'):
1047 raise KeyError, '"id" is reserved'
1049 if self.db.journaltag is None:
1050 raise DatabaseError, 'Database open read-only'
1052 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1053 raise KeyError, '"creation" and "activity" are reserved'
1055 # new node's id
1056 newid = self.db.newid(self.classname)
1058 # validate propvalues
1059 num_re = re.compile('^\d+$')
1060 for key, value in propvalues.items():
1061 if key == self.key:
1062 try:
1063 self.lookup(value)
1064 except KeyError:
1065 pass
1066 else:
1067 raise ValueError, 'node with key "%s" exists'%value
1069 # try to handle this property
1070 try:
1071 prop = self.properties[key]
1072 except KeyError:
1073 raise KeyError, '"%s" has no property "%s"'%(self.classname,
1074 key)
1076 if value is not None and isinstance(prop, Link):
1077 if type(value) != type(''):
1078 raise ValueError, 'link value must be String'
1079 link_class = self.properties[key].classname
1080 # if it isn't a number, it's a key
1081 if not num_re.match(value):
1082 try:
1083 value = self.db.classes[link_class].lookup(value)
1084 except (TypeError, KeyError):
1085 raise IndexError, 'new property "%s": %s not a %s'%(
1086 key, value, link_class)
1087 elif not self.db.getclass(link_class).hasnode(value):
1088 raise IndexError, '%s has no node %s'%(link_class, value)
1090 # save off the value
1091 propvalues[key] = value
1093 # register the link with the newly linked node
1094 if self.do_journal and self.properties[key].do_journal:
1095 self.db.addjournal(link_class, value, 'link',
1096 (self.classname, newid, key))
1098 elif isinstance(prop, Multilink):
1099 if type(value) != type([]):
1100 raise TypeError, 'new property "%s" not a list of ids'%key
1102 # clean up and validate the list of links
1103 link_class = self.properties[key].classname
1104 l = []
1105 for entry in value:
1106 if type(entry) != type(''):
1107 raise ValueError, '"%s" multilink value (%r) '\
1108 'must contain Strings'%(key, value)
1109 # if it isn't a number, it's a key
1110 if not num_re.match(entry):
1111 try:
1112 entry = self.db.classes[link_class].lookup(entry)
1113 except (TypeError, KeyError):
1114 raise IndexError, 'new property "%s": %s not a %s'%(
1115 key, entry, self.properties[key].classname)
1116 l.append(entry)
1117 value = l
1118 propvalues[key] = value
1120 # handle additions
1121 for nodeid in value:
1122 if not self.db.getclass(link_class).hasnode(nodeid):
1123 raise IndexError, '%s has no node %s'%(link_class,
1124 nodeid)
1125 # register the link with the newly linked node
1126 if self.do_journal and self.properties[key].do_journal:
1127 self.db.addjournal(link_class, nodeid, 'link',
1128 (self.classname, newid, key))
1130 elif isinstance(prop, String):
1131 if type(value) != type('') and type(value) != type(u''):
1132 raise TypeError, 'new property "%s" not a string'%key
1134 elif isinstance(prop, Password):
1135 if not isinstance(value, password.Password):
1136 raise TypeError, 'new property "%s" not a Password'%key
1138 elif isinstance(prop, Date):
1139 if value is not None and not isinstance(value, date.Date):
1140 raise TypeError, 'new property "%s" not a Date'%key
1142 elif isinstance(prop, Interval):
1143 if value is not None and not isinstance(value, date.Interval):
1144 raise TypeError, 'new property "%s" not an Interval'%key
1146 elif value is not None and isinstance(prop, Number):
1147 try:
1148 float(value)
1149 except ValueError:
1150 raise TypeError, 'new property "%s" not numeric'%key
1152 elif value is not None and isinstance(prop, Boolean):
1153 try:
1154 int(value)
1155 except ValueError:
1156 raise TypeError, 'new property "%s" not boolean'%key
1158 # make sure there's data where there needs to be
1159 for key, prop in self.properties.items():
1160 if propvalues.has_key(key):
1161 continue
1162 if key == self.key:
1163 raise ValueError, 'key property "%s" is required'%key
1164 if isinstance(prop, Multilink):
1165 propvalues[key] = []
1166 else:
1167 propvalues[key] = None
1169 # done
1170 self.db.addnode(self.classname, newid, propvalues)
1171 if self.do_journal:
1172 self.db.addjournal(self.classname, newid, 'create', {})
1174 return newid
1176 def export_list(self, propnames, nodeid):
1177 ''' Export a node - generate a list of CSV-able data in the order
1178 specified by propnames for the given node.
1179 '''
1180 properties = self.getprops()
1181 l = []
1182 for prop in propnames:
1183 proptype = properties[prop]
1184 value = self.get(nodeid, prop)
1185 # "marshal" data where needed
1186 if value is None:
1187 pass
1188 elif isinstance(proptype, hyperdb.Date):
1189 value = value.get_tuple()
1190 elif isinstance(proptype, hyperdb.Interval):
1191 value = value.get_tuple()
1192 elif isinstance(proptype, hyperdb.Password):
1193 value = str(value)
1194 l.append(repr(value))
1195 l.append(self.is_retired(nodeid))
1196 return l
1198 def import_list(self, propnames, proplist):
1199 ''' Import a node - all information including "id" is present and
1200 should not be sanity checked. Triggers are not triggered. The
1201 journal should be initialised using the "creator" and "created"
1202 information.
1204 Return the nodeid of the node imported.
1205 '''
1206 if self.db.journaltag is None:
1207 raise DatabaseError, 'Database open read-only'
1208 properties = self.getprops()
1210 # make the new node's property map
1211 d = {}
1212 retire = 0
1213 newid = None
1214 for i in range(len(propnames)):
1215 # Use eval to reverse the repr() used to output the CSV
1216 value = eval(proplist[i])
1218 # Figure the property for this column
1219 propname = propnames[i]
1221 # "unmarshal" where necessary
1222 if propname == 'id':
1223 newid = value
1224 continue
1225 elif propname == 'is retired':
1226 # is the item retired?
1227 if int(value):
1228 retire = 1
1229 continue
1230 elif value is None:
1231 d[propname] = None
1232 continue
1234 prop = properties[propname]
1235 if value is None:
1236 # don't set Nones
1237 continue
1238 elif isinstance(prop, hyperdb.Date):
1239 value = date.Date(value)
1240 elif isinstance(prop, hyperdb.Interval):
1241 value = date.Interval(value)
1242 elif isinstance(prop, hyperdb.Password):
1243 pwd = password.Password()
1244 pwd.unpack(value)
1245 value = pwd
1246 d[propname] = value
1248 # get a new id if necessary
1249 if newid is None:
1250 newid = self.db.newid(self.classname)
1252 # retire?
1253 if retire:
1254 # use the arg for __retired__ to cope with any odd database type
1255 # conversion (hello, sqlite)
1256 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1257 self.db.arg, self.db.arg)
1258 if __debug__:
1259 print >>hyperdb.DEBUG, 'retire', (self, sql, newid)
1260 self.db.cursor.execute(sql, (1, newid))
1262 # add the node and journal
1263 self.db.addnode(self.classname, newid, d)
1265 # extract the extraneous journalling gumpf and nuke it
1266 if d.has_key('creator'):
1267 creator = d['creator']
1268 del d['creator']
1269 else:
1270 creator = None
1271 if d.has_key('creation'):
1272 creation = d['creation']
1273 del d['creation']
1274 else:
1275 creation = None
1276 if d.has_key('activity'):
1277 del d['activity']
1278 self.db.addjournal(self.classname, newid, 'create', {}, creator,
1279 creation)
1280 return newid
1282 _marker = []
1283 def get(self, nodeid, propname, default=_marker, cache=1):
1284 '''Get the value of a property on an existing node of this class.
1286 'nodeid' must be the id of an existing node of this class or an
1287 IndexError is raised. 'propname' must be the name of a property
1288 of this class or a KeyError is raised.
1290 'cache' exists for backwards compatibility, and is not used.
1291 '''
1292 if propname == 'id':
1293 return nodeid
1295 # get the node's dict
1296 d = self.db.getnode(self.classname, nodeid)
1298 if propname == 'creation':
1299 if d.has_key('creation'):
1300 return d['creation']
1301 else:
1302 return date.Date()
1303 if propname == 'activity':
1304 if d.has_key('activity'):
1305 return d['activity']
1306 else:
1307 return date.Date()
1308 if propname == 'creator':
1309 if d.has_key('creator'):
1310 return d['creator']
1311 else:
1312 return self.db.getuid()
1314 # get the property (raises KeyErorr if invalid)
1315 prop = self.properties[propname]
1317 if not d.has_key(propname):
1318 if default is self._marker:
1319 if isinstance(prop, Multilink):
1320 return []
1321 else:
1322 return None
1323 else:
1324 return default
1326 # don't pass our list to other code
1327 if isinstance(prop, Multilink):
1328 return d[propname][:]
1330 return d[propname]
1332 def getnode(self, nodeid, cache=1):
1333 ''' Return a convenience wrapper for the node.
1335 'nodeid' must be the id of an existing node of this class or an
1336 IndexError is raised.
1338 'cache' exists for backwards compatibility, and is not used.
1339 '''
1340 return Node(self, nodeid)
1342 def set(self, nodeid, **propvalues):
1343 '''Modify a property on an existing node of this class.
1345 'nodeid' must be the id of an existing node of this class or an
1346 IndexError is raised.
1348 Each key in 'propvalues' must be the name of a property of this
1349 class or a KeyError is raised.
1351 All values in 'propvalues' must be acceptable types for their
1352 corresponding properties or a TypeError is raised.
1354 If the value of the key property is set, it must not collide with
1355 other key strings or a ValueError is raised.
1357 If the value of a Link or Multilink property contains an invalid
1358 node id, a ValueError is raised.
1359 '''
1360 if not propvalues:
1361 return propvalues
1363 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1364 raise KeyError, '"creation" and "activity" are reserved'
1366 if propvalues.has_key('id'):
1367 raise KeyError, '"id" is reserved'
1369 if self.db.journaltag is None:
1370 raise DatabaseError, 'Database open read-only'
1372 self.fireAuditors('set', nodeid, propvalues)
1373 # Take a copy of the node dict so that the subsequent set
1374 # operation doesn't modify the oldvalues structure.
1375 # XXX used to try the cache here first
1376 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
1378 node = self.db.getnode(self.classname, nodeid)
1379 if self.is_retired(nodeid):
1380 raise IndexError, 'Requested item is retired'
1381 num_re = re.compile('^\d+$')
1383 # if the journal value is to be different, store it in here
1384 journalvalues = {}
1386 # remember the add/remove stuff for multilinks, making it easier
1387 # for the Database layer to do its stuff
1388 multilink_changes = {}
1390 for propname, value in propvalues.items():
1391 # check to make sure we're not duplicating an existing key
1392 if propname == self.key and node[propname] != value:
1393 try:
1394 self.lookup(value)
1395 except KeyError:
1396 pass
1397 else:
1398 raise ValueError, 'node with key "%s" exists'%value
1400 # this will raise the KeyError if the property isn't valid
1401 # ... we don't use getprops() here because we only care about
1402 # the writeable properties.
1403 try:
1404 prop = self.properties[propname]
1405 except KeyError:
1406 raise KeyError, '"%s" has no property named "%s"'%(
1407 self.classname, propname)
1409 # if the value's the same as the existing value, no sense in
1410 # doing anything
1411 current = node.get(propname, None)
1412 if value == current:
1413 del propvalues[propname]
1414 continue
1415 journalvalues[propname] = current
1417 # do stuff based on the prop type
1418 if isinstance(prop, Link):
1419 link_class = prop.classname
1420 # if it isn't a number, it's a key
1421 if value is not None and not isinstance(value, type('')):
1422 raise ValueError, 'property "%s" link value be a string'%(
1423 propname)
1424 if isinstance(value, type('')) and not num_re.match(value):
1425 try:
1426 value = self.db.classes[link_class].lookup(value)
1427 except (TypeError, KeyError):
1428 raise IndexError, 'new property "%s": %s not a %s'%(
1429 propname, value, prop.classname)
1431 if (value is not None and
1432 not self.db.getclass(link_class).hasnode(value)):
1433 raise IndexError, '%s has no node %s'%(link_class, value)
1435 if self.do_journal and prop.do_journal:
1436 # register the unlink with the old linked node
1437 if node[propname] is not None:
1438 self.db.addjournal(link_class, node[propname], 'unlink',
1439 (self.classname, nodeid, propname))
1441 # register the link with the newly linked node
1442 if value is not None:
1443 self.db.addjournal(link_class, value, 'link',
1444 (self.classname, nodeid, propname))
1446 elif isinstance(prop, Multilink):
1447 if type(value) != type([]):
1448 raise TypeError, 'new property "%s" not a list of'\
1449 ' ids'%propname
1450 link_class = self.properties[propname].classname
1451 l = []
1452 for entry in value:
1453 # if it isn't a number, it's a key
1454 if type(entry) != type(''):
1455 raise ValueError, 'new property "%s" link value ' \
1456 'must be a string'%propname
1457 if not num_re.match(entry):
1458 try:
1459 entry = self.db.classes[link_class].lookup(entry)
1460 except (TypeError, KeyError):
1461 raise IndexError, 'new property "%s": %s not a %s'%(
1462 propname, entry,
1463 self.properties[propname].classname)
1464 l.append(entry)
1465 value = l
1466 propvalues[propname] = value
1468 # figure the journal entry for this property
1469 add = []
1470 remove = []
1472 # handle removals
1473 if node.has_key(propname):
1474 l = node[propname]
1475 else:
1476 l = []
1477 for id in l[:]:
1478 if id in value:
1479 continue
1480 # register the unlink with the old linked node
1481 if self.do_journal and self.properties[propname].do_journal:
1482 self.db.addjournal(link_class, id, 'unlink',
1483 (self.classname, nodeid, propname))
1484 l.remove(id)
1485 remove.append(id)
1487 # handle additions
1488 for id in value:
1489 if not self.db.getclass(link_class).hasnode(id):
1490 raise IndexError, '%s has no node %s'%(link_class, id)
1491 if id in l:
1492 continue
1493 # register the link with the newly linked node
1494 if self.do_journal and self.properties[propname].do_journal:
1495 self.db.addjournal(link_class, id, 'link',
1496 (self.classname, nodeid, propname))
1497 l.append(id)
1498 add.append(id)
1500 # figure the journal entry
1501 l = []
1502 if add:
1503 l.append(('+', add))
1504 if remove:
1505 l.append(('-', remove))
1506 multilink_changes[propname] = (add, remove)
1507 if l:
1508 journalvalues[propname] = tuple(l)
1510 elif isinstance(prop, String):
1511 if value is not None and type(value) != type('') and type(value) != type(u''):
1512 raise TypeError, 'new property "%s" not a string'%propname
1514 elif isinstance(prop, Password):
1515 if not isinstance(value, password.Password):
1516 raise TypeError, 'new property "%s" not a Password'%propname
1517 propvalues[propname] = value
1519 elif value is not None and isinstance(prop, Date):
1520 if not isinstance(value, date.Date):
1521 raise TypeError, 'new property "%s" not a Date'% propname
1522 propvalues[propname] = value
1524 elif value is not None and isinstance(prop, Interval):
1525 if not isinstance(value, date.Interval):
1526 raise TypeError, 'new property "%s" not an '\
1527 'Interval'%propname
1528 propvalues[propname] = value
1530 elif value is not None and isinstance(prop, Number):
1531 try:
1532 float(value)
1533 except ValueError:
1534 raise TypeError, 'new property "%s" not numeric'%propname
1536 elif value is not None and isinstance(prop, Boolean):
1537 try:
1538 int(value)
1539 except ValueError:
1540 raise TypeError, 'new property "%s" not boolean'%propname
1542 # nothing to do?
1543 if not propvalues:
1544 return propvalues
1546 # do the set, and journal it
1547 self.db.setnode(self.classname, nodeid, propvalues, multilink_changes)
1549 if self.do_journal:
1550 self.db.addjournal(self.classname, nodeid, 'set', journalvalues)
1552 self.fireReactors('set', nodeid, oldvalues)
1554 return propvalues
1556 def retire(self, nodeid):
1557 '''Retire a node.
1559 The properties on the node remain available from the get() method,
1560 and the node's id is never reused.
1562 Retired nodes are not returned by the find(), list(), or lookup()
1563 methods, and other nodes may reuse the values of their key properties.
1564 '''
1565 if self.db.journaltag is None:
1566 raise DatabaseError, 'Database open read-only'
1568 self.fireAuditors('retire', nodeid, None)
1570 # use the arg for __retired__ to cope with any odd database type
1571 # conversion (hello, sqlite)
1572 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1573 self.db.arg, self.db.arg)
1574 if __debug__:
1575 print >>hyperdb.DEBUG, 'retire', (self, sql, nodeid)
1576 self.db.cursor.execute(sql, (1, nodeid))
1577 if self.do_journal:
1578 self.db.addjournal(self.classname, nodeid, 'retired', None)
1580 self.fireReactors('retire', nodeid, None)
1582 def restore(self, nodeid):
1583 '''Restore a retired node.
1585 Make node available for all operations like it was before retirement.
1586 '''
1587 if self.db.journaltag is None:
1588 raise DatabaseError, 'Database open read-only'
1590 node = self.db.getnode(self.classname, nodeid)
1591 # check if key property was overrided
1592 key = self.getkey()
1593 try:
1594 id = self.lookup(node[key])
1595 except KeyError:
1596 pass
1597 else:
1598 raise KeyError, "Key property (%s) of retired node clashes with \
1599 existing one (%s)" % (key, node[key])
1601 self.fireAuditors('restore', nodeid, None)
1602 # use the arg for __retired__ to cope with any odd database type
1603 # conversion (hello, sqlite)
1604 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1605 self.db.arg, self.db.arg)
1606 if __debug__:
1607 print >>hyperdb.DEBUG, 'restore', (self, sql, nodeid)
1608 self.db.cursor.execute(sql, (0, nodeid))
1609 if self.do_journal:
1610 self.db.addjournal(self.classname, nodeid, 'restored', None)
1612 self.fireReactors('restore', nodeid, None)
1614 def is_retired(self, nodeid):
1615 '''Return true if the node is rerired
1616 '''
1617 sql = 'select __retired__ from _%s where id=%s'%(self.classname,
1618 self.db.arg)
1619 if __debug__:
1620 print >>hyperdb.DEBUG, 'is_retired', (self, sql, nodeid)
1621 self.db.cursor.execute(sql, (nodeid,))
1622 return int(self.db.sql_fetchone()[0])
1624 def destroy(self, nodeid):
1625 '''Destroy a node.
1627 WARNING: this method should never be used except in extremely rare
1628 situations where there could never be links to the node being
1629 deleted
1630 WARNING: use retire() instead
1631 WARNING: the properties of this node will not be available ever again
1632 WARNING: really, use retire() instead
1634 Well, I think that's enough warnings. This method exists mostly to
1635 support the session storage of the cgi interface.
1637 The node is completely removed from the hyperdb, including all journal
1638 entries. It will no longer be available, and will generally break code
1639 if there are any references to the node.
1640 '''
1641 if self.db.journaltag is None:
1642 raise DatabaseError, 'Database open read-only'
1643 self.db.destroynode(self.classname, nodeid)
1645 def history(self, nodeid):
1646 '''Retrieve the journal of edits on a particular node.
1648 'nodeid' must be the id of an existing node of this class or an
1649 IndexError is raised.
1651 The returned list contains tuples of the form
1653 (nodeid, date, tag, action, params)
1655 'date' is a Timestamp object specifying the time of the change and
1656 'tag' is the journaltag specified when the database was opened.
1657 '''
1658 if not self.do_journal:
1659 raise ValueError, 'Journalling is disabled for this class'
1660 return self.db.getjournal(self.classname, nodeid)
1662 # Locating nodes:
1663 def hasnode(self, nodeid):
1664 '''Determine if the given nodeid actually exists
1665 '''
1666 return self.db.hasnode(self.classname, nodeid)
1668 def setkey(self, propname):
1669 '''Select a String property of this class to be the key property.
1671 'propname' must be the name of a String property of this class or
1672 None, or a TypeError is raised. The values of the key property on
1673 all existing nodes must be unique or a ValueError is raised.
1674 '''
1675 # XXX create an index on the key prop column. We should also
1676 # record that we've created this index in the schema somewhere.
1677 prop = self.getprops()[propname]
1678 if not isinstance(prop, String):
1679 raise TypeError, 'key properties must be String'
1680 self.key = propname
1682 def getkey(self):
1683 '''Return the name of the key property for this class or None.'''
1684 return self.key
1686 def labelprop(self, default_to_id=0):
1687 ''' Return the property name for a label for the given node.
1689 This method attempts to generate a consistent label for the node.
1690 It tries the following in order:
1691 1. key property
1692 2. "name" property
1693 3. "title" property
1694 4. first property from the sorted property name list
1695 '''
1696 k = self.getkey()
1697 if k:
1698 return k
1699 props = self.getprops()
1700 if props.has_key('name'):
1701 return 'name'
1702 elif props.has_key('title'):
1703 return 'title'
1704 if default_to_id:
1705 return 'id'
1706 props = props.keys()
1707 props.sort()
1708 return props[0]
1710 def lookup(self, keyvalue):
1711 '''Locate a particular node by its key property and return its id.
1713 If this class has no key property, a TypeError is raised. If the
1714 'keyvalue' matches one of the values for the key property among
1715 the nodes in this class, the matching node's id is returned;
1716 otherwise a KeyError is raised.
1717 '''
1718 if not self.key:
1719 raise TypeError, 'No key property set for class %s'%self.classname
1721 # use the arg to handle any odd database type conversion (hello,
1722 # sqlite)
1723 sql = "select id from _%s where _%s=%s and __retired__ <> %s"%(
1724 self.classname, self.key, self.db.arg, self.db.arg)
1725 self.db.sql(sql, (keyvalue, 1))
1727 # see if there was a result that's not retired
1728 row = self.db.sql_fetchone()
1729 if not row:
1730 raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key,
1731 keyvalue, self.classname)
1733 # return the id
1734 return row[0]
1736 def find(self, **propspec):
1737 '''Get the ids of nodes in this class which link to the given nodes.
1739 'propspec' consists of keyword args propname=nodeid or
1740 propname={nodeid:1, }
1741 'propname' must be the name of a property in this class, or a
1742 KeyError is raised. That property must be a Link or Multilink
1743 property, or a TypeError is raised.
1745 Any node in this class whose 'propname' property links to any of the
1746 nodeids will be returned. Used by the full text indexing, which knows
1747 that "foo" occurs in msg1, msg3 and file7, so we have hits on these
1748 issues:
1750 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1751 '''
1752 if __debug__:
1753 print >>hyperdb.DEBUG, 'find', (self, propspec)
1755 # shortcut
1756 if not propspec:
1757 return []
1759 # validate the args
1760 props = self.getprops()
1761 propspec = propspec.items()
1762 for propname, nodeids in propspec:
1763 # check the prop is OK
1764 prop = props[propname]
1765 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
1766 raise TypeError, "'%s' not a Link/Multilink property"%propname
1768 # first, links
1769 where = []
1770 allvalues = ()
1771 a = self.db.arg
1772 for prop, values in propspec:
1773 if not isinstance(props[prop], hyperdb.Link):
1774 continue
1775 if type(values) is type(''):
1776 allvalues += (values,)
1777 where.append('_%s = %s'%(prop, a))
1778 elif values is None:
1779 where.append('_%s is NULL'%prop)
1780 else:
1781 allvalues += tuple(values.keys())
1782 where.append('_%s in (%s)'%(prop, ','.join([a]*len(values))))
1783 tables = []
1784 if where:
1785 tables.append('select id as nodeid from _%s where %s'%(
1786 self.classname, ' and '.join(where)))
1788 # now multilinks
1789 for prop, values in propspec:
1790 if not isinstance(props[prop], hyperdb.Multilink):
1791 continue
1792 if type(values) is type(''):
1793 allvalues += (values,)
1794 s = a
1795 else:
1796 allvalues += tuple(values.keys())
1797 s = ','.join([a]*len(values))
1798 tables.append('select nodeid from %s_%s where linkid in (%s)'%(
1799 self.classname, prop, s))
1800 sql = '\nunion\n'.join(tables)
1801 self.db.sql(sql, allvalues)
1802 l = [x[0] for x in self.db.sql_fetchall()]
1803 if __debug__:
1804 print >>hyperdb.DEBUG, 'find ... ', l
1805 return l
1807 def stringFind(self, **requirements):
1808 '''Locate a particular node by matching a set of its String
1809 properties in a caseless search.
1811 If the property is not a String property, a TypeError is raised.
1813 The return is a list of the id of all nodes that match.
1814 '''
1815 where = []
1816 args = []
1817 for propname in requirements.keys():
1818 prop = self.properties[propname]
1819 if isinstance(not prop, String):
1820 raise TypeError, "'%s' not a String property"%propname
1821 where.append(propname)
1822 args.append(requirements[propname].lower())
1824 # generate the where clause
1825 s = ' and '.join(['lower(_%s)=%s'%(col, self.db.arg) for col in where])
1826 sql = 'select id from _%s where %s'%(self.classname, s)
1827 self.db.sql(sql, tuple(args))
1828 l = [x[0] for x in self.db.sql_fetchall()]
1829 if __debug__:
1830 print >>hyperdb.DEBUG, 'find ... ', l
1831 return l
1833 def list(self):
1834 ''' Return a list of the ids of the active nodes in this class.
1835 '''
1836 return self.getnodeids(retired=0)
1838 def getnodeids(self, retired=None):
1839 ''' Retrieve all the ids of the nodes for a particular Class.
1841 Set retired=None to get all nodes. Otherwise it'll get all the
1842 retired or non-retired nodes, depending on the flag.
1843 '''
1844 # flip the sense of the 'retired' flag if we don't want all of them
1845 if retired is not None:
1846 if retired:
1847 args = (0, )
1848 else:
1849 args = (1, )
1850 sql = 'select id from _%s where __retired__ <> %s'%(self.classname,
1851 self.db.arg)
1852 else:
1853 args = ()
1854 sql = 'select id from _%s'%self.classname
1855 if __debug__:
1856 print >>hyperdb.DEBUG, 'getnodeids', (self, sql, retired)
1857 self.db.cursor.execute(sql, args)
1858 ids = [x[0] for x in self.db.cursor.fetchall()]
1859 return ids
1861 def filter(self, search_matches, filterspec, sort=(None,None),
1862 group=(None,None)):
1863 ''' Return a list of the ids of the active nodes in this class that
1864 match the 'filter' spec, sorted by the group spec and then the
1865 sort spec
1867 "filterspec" is {propname: value(s)}
1868 "sort" and "group" are (dir, prop) where dir is '+', '-' or None
1869 and prop is a prop name or None
1870 "search_matches" is {nodeid: marker}
1872 The filter must match all properties specificed - but if the
1873 property value to match is a list, any one of the values in the
1874 list may match for that property to match.
1875 '''
1876 # just don't bother if the full-text search matched diddly
1877 if search_matches == {}:
1878 return []
1880 cn = self.classname
1882 timezone = self.db.getUserTimezone()
1884 # figure the WHERE clause from the filterspec
1885 props = self.getprops()
1886 frum = ['_'+cn]
1887 where = []
1888 args = []
1889 a = self.db.arg
1890 for k, v in filterspec.items():
1891 propclass = props[k]
1892 # now do other where clause stuff
1893 if isinstance(propclass, Multilink):
1894 tn = '%s_%s'%(cn, k)
1895 if v in ('-1', ['-1']):
1896 # only match rows that have count(linkid)=0 in the
1897 # corresponding multilink table)
1898 where.append('id not in (select nodeid from %s)'%tn)
1899 elif isinstance(v, type([])):
1900 frum.append(tn)
1901 s = ','.join([a for x in v])
1902 where.append('id=%s.nodeid and %s.linkid in (%s)'%(tn,tn,s))
1903 args = args + v
1904 else:
1905 frum.append(tn)
1906 where.append('id=%s.nodeid and %s.linkid=%s'%(tn, tn, a))
1907 args.append(v)
1908 elif k == 'id':
1909 if isinstance(v, type([])):
1910 s = ','.join([a for x in v])
1911 where.append('%s in (%s)'%(k, s))
1912 args = args + v
1913 else:
1914 where.append('%s=%s'%(k, a))
1915 args.append(v)
1916 elif isinstance(propclass, String):
1917 if not isinstance(v, type([])):
1918 v = [v]
1920 # Quote the bits in the string that need it and then embed
1921 # in a "substring" search. Note - need to quote the '%' so
1922 # they make it through the python layer happily
1923 v = ['%%'+self.db.sql_stringquote(s)+'%%' for s in v]
1925 # now add to the where clause
1926 where.append(' or '.join(["_%s LIKE '%s'"%(k, s) for s in v]))
1927 # note: args are embedded in the query string now
1928 elif isinstance(propclass, Link):
1929 if isinstance(v, type([])):
1930 if '-1' in v:
1931 v = v[:]
1932 v.remove('-1')
1933 xtra = ' or _%s is NULL'%k
1934 else:
1935 xtra = ''
1936 if v:
1937 s = ','.join([a for x in v])
1938 where.append('(_%s in (%s)%s)'%(k, s, xtra))
1939 args = args + v
1940 else:
1941 where.append('_%s is NULL'%k)
1942 else:
1943 if v == '-1':
1944 v = None
1945 where.append('_%s is NULL'%k)
1946 else:
1947 where.append('_%s=%s'%(k, a))
1948 args.append(v)
1949 elif isinstance(propclass, Date):
1950 if isinstance(v, type([])):
1951 s = ','.join([a for x in v])
1952 where.append('_%s in (%s)'%(k, s))
1953 args = args + [date.Date(x).serialise() for x in v]
1954 else:
1955 try:
1956 # Try to filter on range of dates
1957 date_rng = Range(v, date.Date, offset=timezone)
1958 if (date_rng.from_value):
1959 where.append('_%s >= %s'%(k, a))
1960 args.append(date_rng.from_value.serialise())
1961 if (date_rng.to_value):
1962 where.append('_%s <= %s'%(k, a))
1963 args.append(date_rng.to_value.serialise())
1964 except ValueError:
1965 # If range creation fails - ignore that search parameter
1966 pass
1967 elif isinstance(propclass, Interval):
1968 if isinstance(v, type([])):
1969 s = ','.join([a for x in v])
1970 where.append('_%s in (%s)'%(k, s))
1971 args = args + [date.Interval(x).serialise() for x in v]
1972 else:
1973 try:
1974 # Try to filter on range of intervals
1975 date_rng = Range(v, date.Interval)
1976 if (date_rng.from_value):
1977 where.append('_%s >= %s'%(k, a))
1978 args.append(date_rng.from_value.serialise())
1979 if (date_rng.to_value):
1980 where.append('_%s <= %s'%(k, a))
1981 args.append(date_rng.to_value.serialise())
1982 except ValueError:
1983 # If range creation fails - ignore that search parameter
1984 pass
1985 #where.append('_%s=%s'%(k, a))
1986 #args.append(date.Interval(v).serialise())
1987 else:
1988 if isinstance(v, type([])):
1989 s = ','.join([a for x in v])
1990 where.append('_%s in (%s)'%(k, s))
1991 args = args + v
1992 else:
1993 where.append('_%s=%s'%(k, a))
1994 args.append(v)
1996 # don't match retired nodes
1997 where.append('__retired__ <> 1')
1999 # add results of full text search
2000 if search_matches is not None:
2001 v = search_matches.keys()
2002 s = ','.join([a for x in v])
2003 where.append('id in (%s)'%s)
2004 args = args + v
2006 # "grouping" is just the first-order sorting in the SQL fetch
2007 # can modify it...)
2008 orderby = []
2009 ordercols = []
2010 if group[0] is not None and group[1] is not None:
2011 if group[0] != '-':
2012 orderby.append('_'+group[1])
2013 ordercols.append('_'+group[1])
2014 else:
2015 orderby.append('_'+group[1]+' desc')
2016 ordercols.append('_'+group[1])
2018 # now add in the sorting
2019 group = ''
2020 if sort[0] is not None and sort[1] is not None:
2021 direction, colname = sort
2022 if direction != '-':
2023 if colname == 'id':
2024 orderby.append(colname)
2025 else:
2026 orderby.append('_'+colname)
2027 ordercols.append('_'+colname)
2028 else:
2029 if colname == 'id':
2030 orderby.append(colname+' desc')
2031 ordercols.append(colname)
2032 else:
2033 orderby.append('_'+colname+' desc')
2034 ordercols.append('_'+colname)
2036 # construct the SQL
2037 frum = ','.join(frum)
2038 if where:
2039 where = ' where ' + (' and '.join(where))
2040 else:
2041 where = ''
2042 cols = ['id']
2043 if orderby:
2044 cols = cols + ordercols
2045 order = ' order by %s'%(','.join(orderby))
2046 else:
2047 order = ''
2048 cols = ','.join(cols)
2049 sql = 'select %s from %s %s%s%s'%(cols, frum, where, group, order)
2050 args = tuple(args)
2051 if __debug__:
2052 print >>hyperdb.DEBUG, 'filter', (self, sql, args)
2053 self.db.cursor.execute(sql, args)
2054 l = self.db.cursor.fetchall()
2056 # return the IDs (the first column)
2057 # XXX The filter(None, l) bit is sqlite-specific... if there's _NO_
2058 # XXX matches to a fetch, it returns NULL instead of nothing!?!
2059 return filter(None, [row[0] for row in l])
2061 def count(self):
2062 '''Get the number of nodes in this class.
2064 If the returned integer is 'numnodes', the ids of all the nodes
2065 in this class run from 1 to numnodes, and numnodes+1 will be the
2066 id of the next node to be created in this class.
2067 '''
2068 return self.db.countnodes(self.classname)
2070 # Manipulating properties:
2071 def getprops(self, protected=1):
2072 '''Return a dictionary mapping property names to property objects.
2073 If the "protected" flag is true, we include protected properties -
2074 those which may not be modified.
2075 '''
2076 d = self.properties.copy()
2077 if protected:
2078 d['id'] = String()
2079 d['creation'] = hyperdb.Date()
2080 d['activity'] = hyperdb.Date()
2081 d['creator'] = hyperdb.Link('user')
2082 return d
2084 def addprop(self, **properties):
2085 '''Add properties to this class.
2087 The keyword arguments in 'properties' must map names to property
2088 objects, or a TypeError is raised. None of the keys in 'properties'
2089 may collide with the names of existing properties, or a ValueError
2090 is raised before any properties have been added.
2091 '''
2092 for key in properties.keys():
2093 if self.properties.has_key(key):
2094 raise ValueError, key
2095 self.properties.update(properties)
2097 def index(self, nodeid):
2098 '''Add (or refresh) the node to search indexes
2099 '''
2100 # find all the String properties that have indexme
2101 for prop, propclass in self.getprops().items():
2102 if isinstance(propclass, String) and propclass.indexme:
2103 try:
2104 value = str(self.get(nodeid, prop))
2105 except IndexError:
2106 # node no longer exists - entry should be removed
2107 self.db.indexer.purge_entry((self.classname, nodeid, prop))
2108 else:
2109 # and index them under (classname, nodeid, property)
2110 self.db.indexer.add_text((self.classname, nodeid, prop),
2111 value)
2114 #
2115 # Detector interface
2116 #
2117 def audit(self, event, detector):
2118 '''Register a detector
2119 '''
2120 l = self.auditors[event]
2121 if detector not in l:
2122 self.auditors[event].append(detector)
2124 def fireAuditors(self, action, nodeid, newvalues):
2125 '''Fire all registered auditors.
2126 '''
2127 for audit in self.auditors[action]:
2128 audit(self.db, self, nodeid, newvalues)
2130 def react(self, event, detector):
2131 '''Register a detector
2132 '''
2133 l = self.reactors[event]
2134 if detector not in l:
2135 self.reactors[event].append(detector)
2137 def fireReactors(self, action, nodeid, oldvalues):
2138 '''Fire all registered reactors.
2139 '''
2140 for react in self.reactors[action]:
2141 react(self.db, self, nodeid, oldvalues)
2143 class FileClass(Class, hyperdb.FileClass):
2144 '''This class defines a large chunk of data. To support this, it has a
2145 mandatory String property "content" which is typically saved off
2146 externally to the hyperdb.
2148 The default MIME type of this data is defined by the
2149 "default_mime_type" class attribute, which may be overridden by each
2150 node if the class defines a "type" String property.
2151 '''
2152 default_mime_type = 'text/plain'
2154 def create(self, **propvalues):
2155 ''' snaffle the file propvalue and store in a file
2156 '''
2157 # we need to fire the auditors now, or the content property won't
2158 # be in propvalues for the auditors to play with
2159 self.fireAuditors('create', None, propvalues)
2161 # now remove the content property so it's not stored in the db
2162 content = propvalues['content']
2163 del propvalues['content']
2165 # do the database create
2166 newid = Class.create_inner(self, **propvalues)
2168 # fire reactors
2169 self.fireReactors('create', newid, None)
2171 # store off the content as a file
2172 self.db.storefile(self.classname, newid, None, content)
2173 return newid
2175 def import_list(self, propnames, proplist):
2176 ''' Trap the "content" property...
2177 '''
2178 # dupe this list so we don't affect others
2179 propnames = propnames[:]
2181 # extract the "content" property from the proplist
2182 i = propnames.index('content')
2183 content = eval(proplist[i])
2184 del propnames[i]
2185 del proplist[i]
2187 # do the normal import
2188 newid = Class.import_list(self, propnames, proplist)
2190 # save off the "content" file
2191 self.db.storefile(self.classname, newid, None, content)
2192 return newid
2194 _marker = []
2195 def get(self, nodeid, propname, default=_marker, cache=1):
2196 ''' Trap the content propname and get it from the file
2198 'cache' exists for backwards compatibility, and is not used.
2199 '''
2200 poss_msg = 'Possibly a access right configuration problem.'
2201 if propname == 'content':
2202 try:
2203 return self.db.getfile(self.classname, nodeid, None)
2204 except IOError, (strerror):
2205 # BUG: by catching this we donot see an error in the log.
2206 return 'ERROR reading file: %s%s\n%s\n%s'%(
2207 self.classname, nodeid, poss_msg, strerror)
2208 if default is not self._marker:
2209 return Class.get(self, nodeid, propname, default)
2210 else:
2211 return Class.get(self, nodeid, propname)
2213 def getprops(self, protected=1):
2214 ''' In addition to the actual properties on the node, these methods
2215 provide the "content" property. If the "protected" flag is true,
2216 we include protected properties - those which may not be
2217 modified.
2218 '''
2219 d = Class.getprops(self, protected=protected).copy()
2220 d['content'] = hyperdb.String()
2221 return d
2223 def index(self, nodeid):
2224 ''' Index the node in the search index.
2226 We want to index the content in addition to the normal String
2227 property indexing.
2228 '''
2229 # perform normal indexing
2230 Class.index(self, nodeid)
2232 # get the content to index
2233 content = self.get(nodeid, 'content')
2235 # figure the mime type
2236 if self.properties.has_key('type'):
2237 mime_type = self.get(nodeid, 'type')
2238 else:
2239 mime_type = self.default_mime_type
2241 # and index!
2242 self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
2243 mime_type)
2245 # XXX deviation from spec - was called ItemClass
2246 class IssueClass(Class, roundupdb.IssueClass):
2247 # Overridden methods:
2248 def __init__(self, db, classname, **properties):
2249 '''The newly-created class automatically includes the "messages",
2250 "files", "nosy", and "superseder" properties. If the 'properties'
2251 dictionary attempts to specify any of these properties or a
2252 "creation" or "activity" property, a ValueError is raised.
2253 '''
2254 if not properties.has_key('title'):
2255 properties['title'] = hyperdb.String(indexme='yes')
2256 if not properties.has_key('messages'):
2257 properties['messages'] = hyperdb.Multilink("msg")
2258 if not properties.has_key('files'):
2259 properties['files'] = hyperdb.Multilink("file")
2260 if not properties.has_key('nosy'):
2261 # note: journalling is turned off as it really just wastes
2262 # space. this behaviour may be overridden in an instance
2263 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
2264 if not properties.has_key('superseder'):
2265 properties['superseder'] = hyperdb.Multilink(classname)
2266 Class.__init__(self, db, classname, **properties)