00921016ff1c185a6e67cd47970b7dc2b2e370ef
1 # $Id: rdbms_common.py,v 1.65 2003-10-07 11:58:58 anthonybaxter 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 in self.database_schema.keys():
133 if not self.classes.has_key(classname):
134 self.drop_class(classname)
136 # update the database version of the schema
137 if save:
138 self.sql('delete from schema')
139 self.save_dbschema(self.database_schema)
141 # reindex the db if necessary
142 if self.indexer.should_reindex():
143 self.reindex()
145 # commit
146 self.conn.commit()
148 def refresh_database(self):
149 # now detect changes in the schema
150 for classname, spec in self.classes.items():
151 dbspec = self.database_schema[classname]
152 self.update_class(spec, dbspec, force=1)
153 self.database_schema[classname] = spec.schema()
154 # update the database version of the schema
155 self.sql('delete from schema')
156 self.save_dbschema(self.database_schema)
157 # reindex the db
158 self.reindex()
159 # commit
160 self.conn.commit()
163 def reindex(self):
164 for klass in self.classes.values():
165 for nodeid in klass.list():
166 klass.index(nodeid)
167 self.indexer.save_index()
169 def determine_columns(self, properties):
170 ''' Figure the column names and multilink properties from the spec
172 "properties" is a list of (name, prop) where prop may be an
173 instance of a hyperdb "type" _or_ a string repr of that type.
174 '''
175 cols = ['_activity', '_creator', '_creation']
176 mls = []
177 # add the multilinks separately
178 for col, prop in properties:
179 if isinstance(prop, Multilink):
180 mls.append(col)
181 elif isinstance(prop, type('')) and prop.find('Multilink') != -1:
182 mls.append(col)
183 else:
184 cols.append('_'+col)
185 cols.sort()
186 return cols, mls
188 def update_class(self, spec, old_spec, force=0):
189 ''' Determine the differences between the current spec and the
190 database version of the spec, and update where necessary.
191 If 'force' is true, update the database anyway.
192 '''
193 new_spec = spec
194 new_has = new_spec.properties.has_key
196 new_spec = new_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 # key property changed?
207 if force or old_spec[0] != new_spec[0]:
208 if __debug__:
209 print >>hyperdb.DEBUG, 'update_class setting keyprop', `spec[0]`
210 # XXX turn on indexing for the key property.
211 index_sql = 'drop index _%s_%s_idx'%(
212 spec.classname, old_spec[0])
213 if __debug__:
214 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
215 try:
216 self.cursor.execute(index_sql1)
217 except:
218 # Hackage. Until we update the schema to include some
219 # backend-specific knowledge, assume that this might fail.
220 pass
222 index_sql = 'create index _%s_%s_idx on _%s(%s)'%(
223 spec.classname, new_spec[0],
224 spec.classname, new_spec[0])
225 if __debug__:
226 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
227 self.cursor.execute(index_sql1)
229 # detect multilinks that have been removed, and drop their table
230 old_has = {}
231 for name,prop in old_spec[1]:
232 old_has[name] = 1
233 if (force or not new_has(name)) and isinstance(prop, Multilink):
234 # it's a multilink, and it's been removed - drop the old
235 # table. First drop indexes.
236 index_sqls = [ 'drop index %s_%s_l_idx'%(spec.classname, ml),
237 'drop index %s_%s_n_idx'%(spec.classname, ml) ]
238 for index_sql in index_sqls:
239 if __debug__:
240 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
241 try:
242 self.cursor.execute(index_sql)
243 except:
244 # The database may not actually have any indexes.
245 # assume the worst.
246 pass
247 sql = 'drop table %s_%s'%(spec.classname, prop)
248 if __debug__:
249 print >>hyperdb.DEBUG, 'update_class', (self, sql)
250 self.cursor.execute(sql)
251 continue
252 old_has = old_has.has_key
254 # now figure how we populate the new table
255 fetch = ['_activity', '_creation', '_creator']
256 properties = spec.getprops()
257 for propname,x in new_spec[1]:
258 prop = properties[propname]
259 if isinstance(prop, Multilink):
260 if force or not old_has(propname):
261 # we need to create the new table
262 self.create_multilink_table(spec, propname)
263 elif old_has(propname):
264 # we copy this col over from the old table
265 fetch.append('_'+propname)
267 # select the data out of the old table
268 fetch.append('id')
269 fetch.append('__retired__')
270 fetchcols = ','.join(fetch)
271 cn = spec.classname
272 sql = 'select %s from _%s'%(fetchcols, cn)
273 if __debug__:
274 print >>hyperdb.DEBUG, 'update_class', (self, sql)
275 self.cursor.execute(sql)
276 olddata = self.cursor.fetchall()
278 # drop the old table - indexes first
279 index_sqls = [ 'drop index _%s_id_idx'%cn,
280 'drop index _%s_retired_idx'%cn ]
281 for index_sql in index_sqls:
282 if __debug__:
283 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
284 try:
285 self.cursor.execute(index_sql)
286 except:
287 # The database may not actually have any indexes.
288 # assume the worst.
289 pass
290 self.cursor.execute('drop table _%s'%cn)
291 # create the new table
292 self.create_class_table(spec)
294 if olddata:
295 # do the insert
296 args = ','.join([self.arg for x in fetch])
297 sql = 'insert into _%s (%s) values (%s)'%(cn, fetchcols, args)
298 if __debug__:
299 print >>hyperdb.DEBUG, 'update_class', (self, sql, olddata[0])
300 for entry in olddata:
301 self.cursor.execute(sql, tuple(entry))
303 return 1
305 def create_class_table(self, spec):
306 ''' create the class table for the given spec
307 '''
308 cols, mls = self.determine_columns(spec.properties.items())
310 # add on our special columns
311 cols.append('id')
312 cols.append('__retired__')
314 # create the base table
315 scols = ','.join(['%s varchar'%x for x in cols])
316 sql = 'create table _%s (%s)'%(spec.classname, scols)
317 if __debug__:
318 print >>hyperdb.DEBUG, 'create_class', (self, sql)
319 self.cursor.execute(sql)
320 index_sql1 = 'create index _%s_id_idx on _%s(id)'%(
321 spec.classname, spec.classname)
322 if __debug__:
323 print >>hyperdb.DEBUG, 'create_index', (self, index_sql1)
324 self.cursor.execute(index_sql1)
325 index_sql2 = 'create index _%s_retired_idx on _%s(__retired__)'%(
326 spec.classname, spec.classname)
327 if __debug__:
328 print >>hyperdb.DEBUG, 'create_index', (self, index_sql2)
329 self.cursor.execute(index_sql2)
331 return cols, mls
333 def create_journal_table(self, spec):
334 ''' create the journal table for a class given the spec and
335 already-determined cols
336 '''
337 # journal table
338 cols = ','.join(['%s varchar'%x
339 for x in 'nodeid date tag action params'.split()])
340 sql = 'create table %s__journal (%s)'%(spec.classname, cols)
341 if __debug__:
342 print >>hyperdb.DEBUG, 'create_class', (self, sql)
343 self.cursor.execute(sql)
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 sql = 'create table %s_%s (linkid varchar, nodeid varchar)'%(
355 spec.classname, ml)
356 if __debug__:
357 print >>hyperdb.DEBUG, 'create_class', (self, sql)
358 self.cursor.execute(sql)
359 index_sql = 'create index %s_%s_l_idx on %s_%s(linkid)'%(
360 spec.classname, ml, spec.classname, ml)
361 if __debug__:
362 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
363 self.cursor.execute(index_sql)
364 index_sql = 'create index %s_%s_n_idx on %s_%s(nodeid)'%(
365 spec.classname, ml, spec.classname, ml)
366 if __debug__:
367 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
368 self.cursor.execute(index_sql)
370 def create_class(self, spec):
371 ''' Create a database table according to the given spec.
372 '''
373 cols, mls = self.create_class_table(spec)
374 self.create_journal_table(spec)
376 # now create the multilink tables
377 for ml in mls:
378 self.create_multilink_table(spec, ml)
380 # ID counter
381 sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
382 vals = (spec.classname, 1)
383 if __debug__:
384 print >>hyperdb.DEBUG, 'create_class', (self, sql, vals)
385 self.cursor.execute(sql, vals)
387 def drop_class(self, spec):
388 ''' Drop the given table from the database.
390 Drop the journal and multilink tables too.
391 '''
392 # figure the multilinks
393 mls = []
394 for col, prop in spec.properties.items():
395 if isinstance(prop, Multilink):
396 mls.append(col)
398 index_sqls = [ 'drop index _%s_id_idx'%cn,
399 'drop index _%s_retired_idx'%cn,
400 'drop index %s_journ_idx'%cn ]
401 for index_sql in index_sqls:
402 if __debug__:
403 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
404 try:
405 self.cursor.execute(index_sql)
406 except:
407 # The database may not actually have any indexes.
408 # assume the worst.
409 pass
411 sql = 'drop table _%s'%spec.classname
412 if __debug__:
413 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
414 self.cursor.execute(sql)
415 sql = 'drop table %s__journal'%spec.classname
416 if __debug__:
417 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
418 self.cursor.execute(sql)
420 for ml in mls:
421 index_sqls = [
422 'drop index %s_%s_n_idx'%(spec.classname, ml),
423 'drop index %s_%s_l_idx'%(spec.classname, ml),
424 ]
425 for index_sql in index_sqls:
426 if __debug__:
427 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
428 try:
429 self.cursor.execute(index_sql)
430 except:
431 # The database may not actually have any indexes.
432 # assume the worst.
433 pass
434 sql = 'drop table %s_%s'%(spec.classname, ml)
435 if __debug__:
436 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
437 self.cursor.execute(sql)
439 #
440 # Classes
441 #
442 def __getattr__(self, classname):
443 ''' A convenient way of calling self.getclass(classname).
444 '''
445 if self.classes.has_key(classname):
446 if __debug__:
447 print >>hyperdb.DEBUG, '__getattr__', (self, classname)
448 return self.classes[classname]
449 raise AttributeError, classname
451 def addclass(self, cl):
452 ''' Add a Class to the hyperdatabase.
453 '''
454 if __debug__:
455 print >>hyperdb.DEBUG, 'addclass', (self, cl)
456 cn = cl.classname
457 if self.classes.has_key(cn):
458 raise ValueError, cn
459 self.classes[cn] = cl
461 def getclasses(self):
462 ''' Return a list of the names of all existing classes.
463 '''
464 if __debug__:
465 print >>hyperdb.DEBUG, 'getclasses', (self,)
466 l = self.classes.keys()
467 l.sort()
468 return l
470 def getclass(self, classname):
471 '''Get the Class object representing a particular class.
473 If 'classname' is not a valid class name, a KeyError is raised.
474 '''
475 if __debug__:
476 print >>hyperdb.DEBUG, 'getclass', (self, classname)
477 try:
478 return self.classes[classname]
479 except KeyError:
480 raise KeyError, 'There is no class called "%s"'%classname
482 def clear(self):
483 ''' Delete all database contents.
485 Note: I don't commit here, which is different behaviour to the
486 "nuke from orbit" behaviour in the *dbms.
487 '''
488 if __debug__:
489 print >>hyperdb.DEBUG, 'clear', (self,)
490 for cn in self.classes.keys():
491 sql = 'delete from _%s'%cn
492 if __debug__:
493 print >>hyperdb.DEBUG, 'clear', (self, sql)
494 self.cursor.execute(sql)
496 #
497 # Node IDs
498 #
499 def newid(self, classname):
500 ''' Generate a new id for the given class
501 '''
502 # get the next ID
503 sql = 'select num from ids where name=%s'%self.arg
504 if __debug__:
505 print >>hyperdb.DEBUG, 'newid', (self, sql, classname)
506 self.cursor.execute(sql, (classname, ))
507 newid = self.cursor.fetchone()[0]
509 # update the counter
510 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
511 vals = (int(newid)+1, classname)
512 if __debug__:
513 print >>hyperdb.DEBUG, 'newid', (self, sql, vals)
514 self.cursor.execute(sql, vals)
516 # return as string
517 return str(newid)
519 def setid(self, classname, setid):
520 ''' Set the id counter: used during import of database
521 '''
522 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
523 vals = (setid, classname)
524 if __debug__:
525 print >>hyperdb.DEBUG, 'setid', (self, sql, vals)
526 self.cursor.execute(sql, vals)
528 #
529 # Nodes
530 #
531 def addnode(self, classname, nodeid, node):
532 ''' Add the specified node to its class's db.
533 '''
534 if __debug__:
535 print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
537 # determine the column definitions and multilink tables
538 cl = self.classes[classname]
539 cols, mls = self.determine_columns(cl.properties.items())
541 # we'll be supplied these props if we're doing an import
542 if not node.has_key('creator'):
543 # add in the "calculated" properties (dupe so we don't affect
544 # calling code's node assumptions)
545 node = node.copy()
546 node['creation'] = node['activity'] = date.Date()
547 node['creator'] = self.getuid()
549 # default the non-multilink columns
550 for col, prop in cl.properties.items():
551 if not node.has_key(col):
552 if isinstance(prop, Multilink):
553 node[col] = []
554 else:
555 node[col] = None
557 # clear this node out of the cache if it's in there
558 key = (classname, nodeid)
559 if self.cache.has_key(key):
560 del self.cache[key]
561 self.cache_lru.remove(key)
563 # make the node data safe for the DB
564 node = self.serialise(classname, node)
566 # make sure the ordering is correct for column name -> column value
567 vals = tuple([node[col[1:]] for col in cols]) + (nodeid, 0)
568 s = ','.join([self.arg for x in cols]) + ',%s,%s'%(self.arg, self.arg)
569 cols = ','.join(cols) + ',id,__retired__'
571 # perform the inserts
572 sql = 'insert into _%s (%s) values (%s)'%(classname, cols, s)
573 if __debug__:
574 print >>hyperdb.DEBUG, 'addnode', (self, sql, vals)
575 self.cursor.execute(sql, vals)
577 # insert the multilink rows
578 for col in mls:
579 t = '%s_%s'%(classname, col)
580 for entry in node[col]:
581 sql = 'insert into %s (linkid, nodeid) values (%s,%s)'%(t,
582 self.arg, self.arg)
583 self.sql(sql, (entry, nodeid))
585 # make sure we do the commit-time extra stuff for this node
586 self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
588 def setnode(self, classname, nodeid, values, multilink_changes):
589 ''' Change the specified node.
590 '''
591 if __debug__:
592 print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, values)
594 # clear this node out of the cache if it's in there
595 key = (classname, nodeid)
596 if self.cache.has_key(key):
597 del self.cache[key]
598 self.cache_lru.remove(key)
600 # add the special props
601 values = values.copy()
602 values['activity'] = date.Date()
604 # make db-friendly
605 values = self.serialise(classname, values)
607 cl = self.classes[classname]
608 cols = []
609 mls = []
610 # add the multilinks separately
611 props = cl.getprops()
612 for col in values.keys():
613 prop = props[col]
614 if isinstance(prop, Multilink):
615 mls.append(col)
616 else:
617 cols.append('_'+col)
618 cols.sort()
620 # if there's any updates to regular columns, do them
621 if cols:
622 # make sure the ordering is correct for column name -> column value
623 sqlvals = tuple([values[col[1:]] for col in cols]) + (nodeid,)
624 s = ','.join(['%s=%s'%(x, self.arg) for x in cols])
625 cols = ','.join(cols)
627 # perform the update
628 sql = 'update _%s set %s where id=%s'%(classname, s, self.arg)
629 if __debug__:
630 print >>hyperdb.DEBUG, 'setnode', (self, sql, sqlvals)
631 self.cursor.execute(sql, sqlvals)
633 # now the fun bit, updating the multilinks ;)
634 for col, (add, remove) in multilink_changes.items():
635 tn = '%s_%s'%(classname, col)
636 if add:
637 sql = 'insert into %s (nodeid, linkid) values (%s,%s)'%(tn,
638 self.arg, self.arg)
639 for addid in add:
640 self.sql(sql, (nodeid, addid))
641 if remove:
642 sql = 'delete from %s where nodeid=%s and linkid=%s'%(tn,
643 self.arg, self.arg)
644 for removeid in remove:
645 self.sql(sql, (nodeid, removeid))
647 # make sure we do the commit-time extra stuff for this node
648 self.transactions.append((self.doSaveNode, (classname, nodeid, values)))
650 def getnode(self, classname, nodeid):
651 ''' Get a node from the database.
652 '''
653 if __debug__:
654 print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid)
656 # see if we have this node cached
657 key = (classname, nodeid)
658 if self.cache.has_key(key):
659 # push us back to the top of the LRU
660 self.cache_lru.remove(key)
661 self.cache_lru.insert(0, key)
662 # return the cached information
663 return self.cache[key]
665 # figure the columns we're fetching
666 cl = self.classes[classname]
667 cols, mls = self.determine_columns(cl.properties.items())
668 scols = ','.join(cols)
670 # perform the basic property fetch
671 sql = 'select %s from _%s where id=%s'%(scols, classname, self.arg)
672 self.sql(sql, (nodeid,))
674 values = self.sql_fetchone()
675 if values is None:
676 raise IndexError, 'no such %s node %s'%(classname, nodeid)
678 # make up the node
679 node = {}
680 for col in range(len(cols)):
681 node[cols[col][1:]] = values[col]
683 # now the multilinks
684 for col in mls:
685 # get the link ids
686 sql = 'select linkid from %s_%s where nodeid=%s'%(classname, col,
687 self.arg)
688 self.cursor.execute(sql, (nodeid,))
689 # extract the first column from the result
690 node[col] = [x[0] for x in self.cursor.fetchall()]
692 # un-dbificate the node data
693 node = self.unserialise(classname, node)
695 # save off in the cache
696 key = (classname, nodeid)
697 self.cache[key] = node
698 # update the LRU
699 self.cache_lru.insert(0, key)
700 if len(self.cache_lru) > ROW_CACHE_SIZE:
701 del self.cache[self.cache_lru.pop()]
703 return node
705 def destroynode(self, classname, nodeid):
706 '''Remove a node from the database. Called exclusively by the
707 destroy() method on Class.
708 '''
709 if __debug__:
710 print >>hyperdb.DEBUG, 'destroynode', (self, classname, nodeid)
712 # make sure the node exists
713 if not self.hasnode(classname, nodeid):
714 raise IndexError, '%s has no node %s'%(classname, nodeid)
716 # see if we have this node cached
717 if self.cache.has_key((classname, nodeid)):
718 del self.cache[(classname, nodeid)]
720 # see if there's any obvious commit actions that we should get rid of
721 for entry in self.transactions[:]:
722 if entry[1][:2] == (classname, nodeid):
723 self.transactions.remove(entry)
725 # now do the SQL
726 sql = 'delete from _%s where id=%s'%(classname, self.arg)
727 self.sql(sql, (nodeid,))
729 # remove from multilnks
730 cl = self.getclass(classname)
731 x, mls = self.determine_columns(cl.properties.items())
732 for col in mls:
733 # get the link ids
734 sql = 'delete from %s_%s where nodeid=%s'%(classname, col, self.arg)
735 self.cursor.execute(sql, (nodeid,))
737 # remove journal entries
738 sql = 'delete from %s__journal where nodeid=%s'%(classname, self.arg)
739 self.sql(sql, (nodeid,))
741 def serialise(self, classname, node):
742 '''Copy the node contents, converting non-marshallable data into
743 marshallable data.
744 '''
745 if __debug__:
746 print >>hyperdb.DEBUG, 'serialise', classname, node
747 properties = self.getclass(classname).getprops()
748 d = {}
749 for k, v in node.items():
750 # if the property doesn't exist, or is the "retired" flag then
751 # it won't be in the properties dict
752 if not properties.has_key(k):
753 d[k] = v
754 continue
756 # get the property spec
757 prop = properties[k]
759 if isinstance(prop, Password) and v is not None:
760 d[k] = str(v)
761 elif isinstance(prop, Date) and v is not None:
762 d[k] = v.serialise()
763 elif isinstance(prop, Interval) and v is not None:
764 d[k] = v.serialise()
765 else:
766 d[k] = v
767 return d
769 def unserialise(self, classname, node):
770 '''Decode the marshalled node data
771 '''
772 if __debug__:
773 print >>hyperdb.DEBUG, 'unserialise', classname, node
774 properties = self.getclass(classname).getprops()
775 d = {}
776 for k, v in node.items():
777 # if the property doesn't exist, or is the "retired" flag then
778 # it won't be in the properties dict
779 if not properties.has_key(k):
780 d[k] = v
781 continue
783 # get the property spec
784 prop = properties[k]
786 if isinstance(prop, Date) and v is not None:
787 d[k] = date.Date(v)
788 elif isinstance(prop, Interval) and v is not None:
789 d[k] = date.Interval(v)
790 elif isinstance(prop, Password) and v is not None:
791 p = password.Password()
792 p.unpack(v)
793 d[k] = p
794 elif (isinstance(prop, Boolean) or isinstance(prop, Number)) and v is not None:
795 d[k]=float(v)
796 else:
797 d[k] = v
798 return d
800 def hasnode(self, classname, nodeid):
801 ''' Determine if the database has a given node.
802 '''
803 sql = 'select count(*) from _%s where id=%s'%(classname, self.arg)
804 if __debug__:
805 print >>hyperdb.DEBUG, 'hasnode', (self, sql, nodeid)
806 self.cursor.execute(sql, (nodeid,))
807 return int(self.cursor.fetchone()[0])
809 def countnodes(self, classname):
810 ''' Count the number of nodes that exist for a particular Class.
811 '''
812 sql = 'select count(*) from _%s'%classname
813 if __debug__:
814 print >>hyperdb.DEBUG, 'countnodes', (self, sql)
815 self.cursor.execute(sql)
816 return self.cursor.fetchone()[0]
818 def addjournal(self, classname, nodeid, action, params, creator=None,
819 creation=None):
820 ''' Journal the Action
821 'action' may be:
823 'create' or 'set' -- 'params' is a dictionary of property values
824 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
825 'retire' -- 'params' is None
826 '''
827 # serialise the parameters now if necessary
828 if isinstance(params, type({})):
829 if action in ('set', 'create'):
830 params = self.serialise(classname, params)
832 # handle supply of the special journalling parameters (usually
833 # supplied on importing an existing database)
834 if creator:
835 journaltag = creator
836 else:
837 journaltag = self.getuid()
838 if creation:
839 journaldate = creation.serialise()
840 else:
841 journaldate = date.Date().serialise()
843 # create the journal entry
844 cols = ','.join('nodeid date tag action params'.split())
846 if __debug__:
847 print >>hyperdb.DEBUG, 'addjournal', (nodeid, journaldate,
848 journaltag, action, params)
850 self.save_journal(classname, cols, nodeid, journaldate,
851 journaltag, action, params)
853 def save_journal(self, classname, cols, nodeid, journaldate,
854 journaltag, action, params):
855 ''' Save the journal entry to the database
856 '''
857 raise NotImplemented
859 def getjournal(self, classname, nodeid):
860 ''' get the journal for id
861 '''
862 # make sure the node exists
863 if not self.hasnode(classname, nodeid):
864 raise IndexError, '%s has no node %s'%(classname, nodeid)
866 cols = ','.join('nodeid date tag action params'.split())
867 return self.load_journal(classname, cols, nodeid)
869 def load_journal(self, classname, cols, nodeid):
870 ''' Load the journal from the database
871 '''
872 raise NotImplemented
874 def pack(self, pack_before):
875 ''' Delete all journal entries except "create" before 'pack_before'.
876 '''
877 # get a 'yyyymmddhhmmss' version of the date
878 date_stamp = pack_before.serialise()
880 # do the delete
881 for classname in self.classes.keys():
882 sql = "delete from %s__journal where date<%s and "\
883 "action<>'create'"%(classname, self.arg)
884 if __debug__:
885 print >>hyperdb.DEBUG, 'pack', (self, sql, date_stamp)
886 self.cursor.execute(sql, (date_stamp,))
888 def sql_commit(self):
889 ''' Actually commit to the database.
890 '''
891 self.conn.commit()
893 def commit(self):
894 ''' Commit the current transactions.
896 Save all data changed since the database was opened or since the
897 last commit() or rollback().
898 '''
899 if __debug__:
900 print >>hyperdb.DEBUG, 'commit', (self,)
902 # commit the database
903 self.sql_commit()
905 # now, do all the other transaction stuff
906 reindex = {}
907 for method, args in self.transactions:
908 reindex[method(*args)] = 1
910 # reindex the nodes that request it
911 for classname, nodeid in filter(None, reindex.keys()):
912 print >>hyperdb.DEBUG, 'commit.reindex', (classname, nodeid)
913 self.getclass(classname).index(nodeid)
915 # save the indexer state
916 self.indexer.save_index()
918 # clear out the transactions
919 self.transactions = []
921 def rollback(self):
922 ''' Reverse all actions from the current transaction.
924 Undo all the changes made since the database was opened or the last
925 commit() or rollback() was performed.
926 '''
927 if __debug__:
928 print >>hyperdb.DEBUG, 'rollback', (self,)
930 # roll back
931 self.conn.rollback()
933 # roll back "other" transaction stuff
934 for method, args in self.transactions:
935 # delete temporary files
936 if method == self.doStoreFile:
937 self.rollbackStoreFile(*args)
938 self.transactions = []
940 # clear the cache
941 self.clearCache()
943 def doSaveNode(self, classname, nodeid, node):
944 ''' dummy that just generates a reindex event
945 '''
946 # return the classname, nodeid so we reindex this content
947 return (classname, nodeid)
949 def close(self):
950 ''' Close off the connection.
951 '''
952 self.conn.close()
953 if self.lockfile is not None:
954 locking.release_lock(self.lockfile)
955 if self.lockfile is not None:
956 self.lockfile.close()
957 self.lockfile = None
959 #
960 # The base Class class
961 #
962 class Class(hyperdb.Class):
963 ''' The handle to a particular class of nodes in a hyperdatabase.
965 All methods except __repr__ and getnode must be implemented by a
966 concrete backend Class.
967 '''
969 def __init__(self, db, classname, **properties):
970 '''Create a new class with a given name and property specification.
972 'classname' must not collide with the name of an existing class,
973 or a ValueError is raised. The keyword arguments in 'properties'
974 must map names to property objects, or a TypeError is raised.
975 '''
976 if (properties.has_key('creation') or properties.has_key('activity')
977 or properties.has_key('creator')):
978 raise ValueError, '"creation", "activity" and "creator" are '\
979 'reserved'
981 self.classname = classname
982 self.properties = properties
983 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
984 self.key = ''
986 # should we journal changes (default yes)
987 self.do_journal = 1
989 # do the db-related init stuff
990 db.addclass(self)
992 self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
993 self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
995 def schema(self):
996 ''' A dumpable version of the schema that we can store in the
997 database
998 '''
999 return (self.key, [(x, repr(y)) for x,y in self.properties.items()])
1001 def enableJournalling(self):
1002 '''Turn journalling on for this class
1003 '''
1004 self.do_journal = 1
1006 def disableJournalling(self):
1007 '''Turn journalling off for this class
1008 '''
1009 self.do_journal = 0
1011 # Editing nodes:
1012 def create(self, **propvalues):
1013 ''' Create a new node of this class and return its id.
1015 The keyword arguments in 'propvalues' map property names to values.
1017 The values of arguments must be acceptable for the types of their
1018 corresponding properties or a TypeError is raised.
1020 If this class has a key property, it must be present and its value
1021 must not collide with other key strings or a ValueError is raised.
1023 Any other properties on this class that are missing from the
1024 'propvalues' dictionary are set to None.
1026 If an id in a link or multilink property does not refer to a valid
1027 node, an IndexError is raised.
1028 '''
1029 self.fireAuditors('create', None, propvalues)
1030 newid = self.create_inner(**propvalues)
1031 self.fireReactors('create', newid, None)
1032 return newid
1034 def create_inner(self, **propvalues):
1035 ''' Called by create, in-between the audit and react calls.
1036 '''
1037 if propvalues.has_key('id'):
1038 raise KeyError, '"id" is reserved'
1040 if self.db.journaltag is None:
1041 raise DatabaseError, 'Database open read-only'
1043 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1044 raise KeyError, '"creation" and "activity" are reserved'
1046 # new node's id
1047 newid = self.db.newid(self.classname)
1049 # validate propvalues
1050 num_re = re.compile('^\d+$')
1051 for key, value in propvalues.items():
1052 if key == self.key:
1053 try:
1054 self.lookup(value)
1055 except KeyError:
1056 pass
1057 else:
1058 raise ValueError, 'node with key "%s" exists'%value
1060 # try to handle this property
1061 try:
1062 prop = self.properties[key]
1063 except KeyError:
1064 raise KeyError, '"%s" has no property "%s"'%(self.classname,
1065 key)
1067 if value is not None and isinstance(prop, Link):
1068 if type(value) != type(''):
1069 raise ValueError, 'link value must be String'
1070 link_class = self.properties[key].classname
1071 # if it isn't a number, it's a key
1072 if not num_re.match(value):
1073 try:
1074 value = self.db.classes[link_class].lookup(value)
1075 except (TypeError, KeyError):
1076 raise IndexError, 'new property "%s": %s not a %s'%(
1077 key, value, link_class)
1078 elif not self.db.getclass(link_class).hasnode(value):
1079 raise IndexError, '%s has no node %s'%(link_class, value)
1081 # save off the value
1082 propvalues[key] = value
1084 # register the link with the newly linked node
1085 if self.do_journal and self.properties[key].do_journal:
1086 self.db.addjournal(link_class, value, 'link',
1087 (self.classname, newid, key))
1089 elif isinstance(prop, Multilink):
1090 if type(value) != type([]):
1091 raise TypeError, 'new property "%s" not a list of ids'%key
1093 # clean up and validate the list of links
1094 link_class = self.properties[key].classname
1095 l = []
1096 for entry in value:
1097 if type(entry) != type(''):
1098 raise ValueError, '"%s" multilink value (%r) '\
1099 'must contain Strings'%(key, value)
1100 # if it isn't a number, it's a key
1101 if not num_re.match(entry):
1102 try:
1103 entry = self.db.classes[link_class].lookup(entry)
1104 except (TypeError, KeyError):
1105 raise IndexError, 'new property "%s": %s not a %s'%(
1106 key, entry, self.properties[key].classname)
1107 l.append(entry)
1108 value = l
1109 propvalues[key] = value
1111 # handle additions
1112 for nodeid in value:
1113 if not self.db.getclass(link_class).hasnode(nodeid):
1114 raise IndexError, '%s has no node %s'%(link_class,
1115 nodeid)
1116 # register the link with the newly linked node
1117 if self.do_journal and self.properties[key].do_journal:
1118 self.db.addjournal(link_class, nodeid, 'link',
1119 (self.classname, newid, key))
1121 elif isinstance(prop, String):
1122 if type(value) != type('') and type(value) != type(u''):
1123 raise TypeError, 'new property "%s" not a string'%key
1125 elif isinstance(prop, Password):
1126 if not isinstance(value, password.Password):
1127 raise TypeError, 'new property "%s" not a Password'%key
1129 elif isinstance(prop, Date):
1130 if value is not None and not isinstance(value, date.Date):
1131 raise TypeError, 'new property "%s" not a Date'%key
1133 elif isinstance(prop, Interval):
1134 if value is not None and not isinstance(value, date.Interval):
1135 raise TypeError, 'new property "%s" not an Interval'%key
1137 elif value is not None and isinstance(prop, Number):
1138 try:
1139 float(value)
1140 except ValueError:
1141 raise TypeError, 'new property "%s" not numeric'%key
1143 elif value is not None and isinstance(prop, Boolean):
1144 try:
1145 int(value)
1146 except ValueError:
1147 raise TypeError, 'new property "%s" not boolean'%key
1149 # make sure there's data where there needs to be
1150 for key, prop in self.properties.items():
1151 if propvalues.has_key(key):
1152 continue
1153 if key == self.key:
1154 raise ValueError, 'key property "%s" is required'%key
1155 if isinstance(prop, Multilink):
1156 propvalues[key] = []
1157 else:
1158 propvalues[key] = None
1160 # done
1161 self.db.addnode(self.classname, newid, propvalues)
1162 if self.do_journal:
1163 self.db.addjournal(self.classname, newid, 'create', {})
1165 return newid
1167 def export_list(self, propnames, nodeid):
1168 ''' Export a node - generate a list of CSV-able data in the order
1169 specified by propnames for the given node.
1170 '''
1171 properties = self.getprops()
1172 l = []
1173 for prop in propnames:
1174 proptype = properties[prop]
1175 value = self.get(nodeid, prop)
1176 # "marshal" data where needed
1177 if value is None:
1178 pass
1179 elif isinstance(proptype, hyperdb.Date):
1180 value = value.get_tuple()
1181 elif isinstance(proptype, hyperdb.Interval):
1182 value = value.get_tuple()
1183 elif isinstance(proptype, hyperdb.Password):
1184 value = str(value)
1185 l.append(repr(value))
1186 l.append(self.is_retired(nodeid))
1187 return l
1189 def import_list(self, propnames, proplist):
1190 ''' Import a node - all information including "id" is present and
1191 should not be sanity checked. Triggers are not triggered. The
1192 journal should be initialised using the "creator" and "created"
1193 information.
1195 Return the nodeid of the node imported.
1196 '''
1197 if self.db.journaltag is None:
1198 raise DatabaseError, 'Database open read-only'
1199 properties = self.getprops()
1201 # make the new node's property map
1202 d = {}
1203 retire = 0
1204 newid = None
1205 for i in range(len(propnames)):
1206 # Use eval to reverse the repr() used to output the CSV
1207 value = eval(proplist[i])
1209 # Figure the property for this column
1210 propname = propnames[i]
1212 # "unmarshal" where necessary
1213 if propname == 'id':
1214 newid = value
1215 continue
1216 elif propname == 'is retired':
1217 # is the item retired?
1218 if int(value):
1219 retire = 1
1220 continue
1221 elif value is None:
1222 d[propname] = None
1223 continue
1225 prop = properties[propname]
1226 if value is None:
1227 # don't set Nones
1228 continue
1229 elif isinstance(prop, hyperdb.Date):
1230 value = date.Date(value)
1231 elif isinstance(prop, hyperdb.Interval):
1232 value = date.Interval(value)
1233 elif isinstance(prop, hyperdb.Password):
1234 pwd = password.Password()
1235 pwd.unpack(value)
1236 value = pwd
1237 d[propname] = value
1239 # get a new id if necessary
1240 if newid is None:
1241 newid = self.db.newid(self.classname)
1243 # retire?
1244 if retire:
1245 # use the arg for __retired__ to cope with any odd database type
1246 # conversion (hello, sqlite)
1247 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1248 self.db.arg, self.db.arg)
1249 if __debug__:
1250 print >>hyperdb.DEBUG, 'retire', (self, sql, newid)
1251 self.db.cursor.execute(sql, (1, newid))
1253 # add the node and journal
1254 self.db.addnode(self.classname, newid, d)
1256 # extract the extraneous journalling gumpf and nuke it
1257 if d.has_key('creator'):
1258 creator = d['creator']
1259 del d['creator']
1260 else:
1261 creator = None
1262 if d.has_key('creation'):
1263 creation = d['creation']
1264 del d['creation']
1265 else:
1266 creation = None
1267 if d.has_key('activity'):
1268 del d['activity']
1269 self.db.addjournal(self.classname, newid, 'create', {}, creator,
1270 creation)
1271 return newid
1273 _marker = []
1274 def get(self, nodeid, propname, default=_marker, cache=1):
1275 '''Get the value of a property on an existing node of this class.
1277 'nodeid' must be the id of an existing node of this class or an
1278 IndexError is raised. 'propname' must be the name of a property
1279 of this class or a KeyError is raised.
1281 'cache' exists for backwards compatibility, and is not used.
1282 '''
1283 if propname == 'id':
1284 return nodeid
1286 # get the node's dict
1287 d = self.db.getnode(self.classname, nodeid)
1289 if propname == 'creation':
1290 if d.has_key('creation'):
1291 return d['creation']
1292 else:
1293 return date.Date()
1294 if propname == 'activity':
1295 if d.has_key('activity'):
1296 return d['activity']
1297 else:
1298 return date.Date()
1299 if propname == 'creator':
1300 if d.has_key('creator'):
1301 return d['creator']
1302 else:
1303 return self.db.getuid()
1305 # get the property (raises KeyErorr if invalid)
1306 prop = self.properties[propname]
1308 if not d.has_key(propname):
1309 if default is self._marker:
1310 if isinstance(prop, Multilink):
1311 return []
1312 else:
1313 return None
1314 else:
1315 return default
1317 # don't pass our list to other code
1318 if isinstance(prop, Multilink):
1319 return d[propname][:]
1321 return d[propname]
1323 def getnode(self, nodeid, cache=1):
1324 ''' Return a convenience wrapper for the node.
1326 'nodeid' must be the id of an existing node of this class or an
1327 IndexError is raised.
1329 'cache' exists for backwards compatibility, and is not used.
1330 '''
1331 return Node(self, nodeid)
1333 def set(self, nodeid, **propvalues):
1334 '''Modify a property on an existing node of this class.
1336 'nodeid' must be the id of an existing node of this class or an
1337 IndexError is raised.
1339 Each key in 'propvalues' must be the name of a property of this
1340 class or a KeyError is raised.
1342 All values in 'propvalues' must be acceptable types for their
1343 corresponding properties or a TypeError is raised.
1345 If the value of the key property is set, it must not collide with
1346 other key strings or a ValueError is raised.
1348 If the value of a Link or Multilink property contains an invalid
1349 node id, a ValueError is raised.
1350 '''
1351 if not propvalues:
1352 return propvalues
1354 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1355 raise KeyError, '"creation" and "activity" are reserved'
1357 if propvalues.has_key('id'):
1358 raise KeyError, '"id" is reserved'
1360 if self.db.journaltag is None:
1361 raise DatabaseError, 'Database open read-only'
1363 self.fireAuditors('set', nodeid, propvalues)
1364 # Take a copy of the node dict so that the subsequent set
1365 # operation doesn't modify the oldvalues structure.
1366 # XXX used to try the cache here first
1367 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
1369 node = self.db.getnode(self.classname, nodeid)
1370 if self.is_retired(nodeid):
1371 raise IndexError, 'Requested item is retired'
1372 num_re = re.compile('^\d+$')
1374 # if the journal value is to be different, store it in here
1375 journalvalues = {}
1377 # remember the add/remove stuff for multilinks, making it easier
1378 # for the Database layer to do its stuff
1379 multilink_changes = {}
1381 for propname, value in propvalues.items():
1382 # check to make sure we're not duplicating an existing key
1383 if propname == self.key and node[propname] != value:
1384 try:
1385 self.lookup(value)
1386 except KeyError:
1387 pass
1388 else:
1389 raise ValueError, 'node with key "%s" exists'%value
1391 # this will raise the KeyError if the property isn't valid
1392 # ... we don't use getprops() here because we only care about
1393 # the writeable properties.
1394 try:
1395 prop = self.properties[propname]
1396 except KeyError:
1397 raise KeyError, '"%s" has no property named "%s"'%(
1398 self.classname, propname)
1400 # if the value's the same as the existing value, no sense in
1401 # doing anything
1402 current = node.get(propname, None)
1403 if value == current:
1404 del propvalues[propname]
1405 continue
1406 journalvalues[propname] = current
1408 # do stuff based on the prop type
1409 if isinstance(prop, Link):
1410 link_class = prop.classname
1411 # if it isn't a number, it's a key
1412 if value is not None and not isinstance(value, type('')):
1413 raise ValueError, 'property "%s" link value be a string'%(
1414 propname)
1415 if isinstance(value, type('')) and not num_re.match(value):
1416 try:
1417 value = self.db.classes[link_class].lookup(value)
1418 except (TypeError, KeyError):
1419 raise IndexError, 'new property "%s": %s not a %s'%(
1420 propname, value, prop.classname)
1422 if (value is not None and
1423 not self.db.getclass(link_class).hasnode(value)):
1424 raise IndexError, '%s has no node %s'%(link_class, value)
1426 if self.do_journal and prop.do_journal:
1427 # register the unlink with the old linked node
1428 if node[propname] is not None:
1429 self.db.addjournal(link_class, node[propname], 'unlink',
1430 (self.classname, nodeid, propname))
1432 # register the link with the newly linked node
1433 if value is not None:
1434 self.db.addjournal(link_class, value, 'link',
1435 (self.classname, nodeid, propname))
1437 elif isinstance(prop, Multilink):
1438 if type(value) != type([]):
1439 raise TypeError, 'new property "%s" not a list of'\
1440 ' ids'%propname
1441 link_class = self.properties[propname].classname
1442 l = []
1443 for entry in value:
1444 # if it isn't a number, it's a key
1445 if type(entry) != type(''):
1446 raise ValueError, 'new property "%s" link value ' \
1447 'must be a string'%propname
1448 if not num_re.match(entry):
1449 try:
1450 entry = self.db.classes[link_class].lookup(entry)
1451 except (TypeError, KeyError):
1452 raise IndexError, 'new property "%s": %s not a %s'%(
1453 propname, entry,
1454 self.properties[propname].classname)
1455 l.append(entry)
1456 value = l
1457 propvalues[propname] = value
1459 # figure the journal entry for this property
1460 add = []
1461 remove = []
1463 # handle removals
1464 if node.has_key(propname):
1465 l = node[propname]
1466 else:
1467 l = []
1468 for id in l[:]:
1469 if id in value:
1470 continue
1471 # register the unlink with the old linked node
1472 if self.do_journal and self.properties[propname].do_journal:
1473 self.db.addjournal(link_class, id, 'unlink',
1474 (self.classname, nodeid, propname))
1475 l.remove(id)
1476 remove.append(id)
1478 # handle additions
1479 for id in value:
1480 if not self.db.getclass(link_class).hasnode(id):
1481 raise IndexError, '%s has no node %s'%(link_class, id)
1482 if id in l:
1483 continue
1484 # register the link with the newly linked node
1485 if self.do_journal and self.properties[propname].do_journal:
1486 self.db.addjournal(link_class, id, 'link',
1487 (self.classname, nodeid, propname))
1488 l.append(id)
1489 add.append(id)
1491 # figure the journal entry
1492 l = []
1493 if add:
1494 l.append(('+', add))
1495 if remove:
1496 l.append(('-', remove))
1497 multilink_changes[propname] = (add, remove)
1498 if l:
1499 journalvalues[propname] = tuple(l)
1501 elif isinstance(prop, String):
1502 if value is not None and type(value) != type('') and type(value) != type(u''):
1503 raise TypeError, 'new property "%s" not a string'%propname
1505 elif isinstance(prop, Password):
1506 if not isinstance(value, password.Password):
1507 raise TypeError, 'new property "%s" not a Password'%propname
1508 propvalues[propname] = value
1510 elif value is not None and isinstance(prop, Date):
1511 if not isinstance(value, date.Date):
1512 raise TypeError, 'new property "%s" not a Date'% propname
1513 propvalues[propname] = value
1515 elif value is not None and isinstance(prop, Interval):
1516 if not isinstance(value, date.Interval):
1517 raise TypeError, 'new property "%s" not an '\
1518 'Interval'%propname
1519 propvalues[propname] = value
1521 elif value is not None and isinstance(prop, Number):
1522 try:
1523 float(value)
1524 except ValueError:
1525 raise TypeError, 'new property "%s" not numeric'%propname
1527 elif value is not None and isinstance(prop, Boolean):
1528 try:
1529 int(value)
1530 except ValueError:
1531 raise TypeError, 'new property "%s" not boolean'%propname
1533 # nothing to do?
1534 if not propvalues:
1535 return propvalues
1537 # do the set, and journal it
1538 self.db.setnode(self.classname, nodeid, propvalues, multilink_changes)
1540 if self.do_journal:
1541 self.db.addjournal(self.classname, nodeid, 'set', journalvalues)
1543 self.fireReactors('set', nodeid, oldvalues)
1545 return propvalues
1547 def retire(self, nodeid):
1548 '''Retire a node.
1550 The properties on the node remain available from the get() method,
1551 and the node's id is never reused.
1553 Retired nodes are not returned by the find(), list(), or lookup()
1554 methods, and other nodes may reuse the values of their key properties.
1555 '''
1556 if self.db.journaltag is None:
1557 raise DatabaseError, 'Database open read-only'
1559 self.fireAuditors('retire', nodeid, None)
1561 # use the arg for __retired__ to cope with any odd database type
1562 # conversion (hello, sqlite)
1563 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1564 self.db.arg, self.db.arg)
1565 if __debug__:
1566 print >>hyperdb.DEBUG, 'retire', (self, sql, nodeid)
1567 self.db.cursor.execute(sql, (1, nodeid))
1568 if self.do_journal:
1569 self.db.addjournal(self.classname, nodeid, 'retired', None)
1571 self.fireReactors('retire', nodeid, None)
1573 def restore(self, nodeid):
1574 '''Restore a retired node.
1576 Make node available for all operations like it was before retirement.
1577 '''
1578 if self.db.journaltag is None:
1579 raise DatabaseError, 'Database open read-only'
1581 node = self.db.getnode(self.classname, nodeid)
1582 # check if key property was overrided
1583 key = self.getkey()
1584 try:
1585 id = self.lookup(node[key])
1586 except KeyError:
1587 pass
1588 else:
1589 raise KeyError, "Key property (%s) of retired node clashes with \
1590 existing one (%s)" % (key, node[key])
1592 self.fireAuditors('restore', nodeid, None)
1593 # use the arg for __retired__ to cope with any odd database type
1594 # conversion (hello, sqlite)
1595 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1596 self.db.arg, self.db.arg)
1597 if __debug__:
1598 print >>hyperdb.DEBUG, 'restore', (self, sql, nodeid)
1599 self.db.cursor.execute(sql, (0, nodeid))
1600 if self.do_journal:
1601 self.db.addjournal(self.classname, nodeid, 'restored', None)
1603 self.fireReactors('restore', nodeid, None)
1605 def is_retired(self, nodeid):
1606 '''Return true if the node is rerired
1607 '''
1608 sql = 'select __retired__ from _%s where id=%s'%(self.classname,
1609 self.db.arg)
1610 if __debug__:
1611 print >>hyperdb.DEBUG, 'is_retired', (self, sql, nodeid)
1612 self.db.cursor.execute(sql, (nodeid,))
1613 return int(self.db.sql_fetchone()[0])
1615 def destroy(self, nodeid):
1616 '''Destroy a node.
1618 WARNING: this method should never be used except in extremely rare
1619 situations where there could never be links to the node being
1620 deleted
1621 WARNING: use retire() instead
1622 WARNING: the properties of this node will not be available ever again
1623 WARNING: really, use retire() instead
1625 Well, I think that's enough warnings. This method exists mostly to
1626 support the session storage of the cgi interface.
1628 The node is completely removed from the hyperdb, including all journal
1629 entries. It will no longer be available, and will generally break code
1630 if there are any references to the node.
1631 '''
1632 if self.db.journaltag is None:
1633 raise DatabaseError, 'Database open read-only'
1634 self.db.destroynode(self.classname, nodeid)
1636 def history(self, nodeid):
1637 '''Retrieve the journal of edits on a particular node.
1639 'nodeid' must be the id of an existing node of this class or an
1640 IndexError is raised.
1642 The returned list contains tuples of the form
1644 (nodeid, date, tag, action, params)
1646 'date' is a Timestamp object specifying the time of the change and
1647 'tag' is the journaltag specified when the database was opened.
1648 '''
1649 if not self.do_journal:
1650 raise ValueError, 'Journalling is disabled for this class'
1651 return self.db.getjournal(self.classname, nodeid)
1653 # Locating nodes:
1654 def hasnode(self, nodeid):
1655 '''Determine if the given nodeid actually exists
1656 '''
1657 return self.db.hasnode(self.classname, nodeid)
1659 def setkey(self, propname):
1660 '''Select a String property of this class to be the key property.
1662 'propname' must be the name of a String property of this class or
1663 None, or a TypeError is raised. The values of the key property on
1664 all existing nodes must be unique or a ValueError is raised.
1665 '''
1666 # XXX create an index on the key prop column. We should also
1667 # record that we've created this index in the schema somewhere.
1668 prop = self.getprops()[propname]
1669 if not isinstance(prop, String):
1670 raise TypeError, 'key properties must be String'
1671 self.key = propname
1673 def getkey(self):
1674 '''Return the name of the key property for this class or None.'''
1675 return self.key
1677 def labelprop(self, default_to_id=0):
1678 ''' Return the property name for a label for the given node.
1680 This method attempts to generate a consistent label for the node.
1681 It tries the following in order:
1682 1. key property
1683 2. "name" property
1684 3. "title" property
1685 4. first property from the sorted property name list
1686 '''
1687 k = self.getkey()
1688 if k:
1689 return k
1690 props = self.getprops()
1691 if props.has_key('name'):
1692 return 'name'
1693 elif props.has_key('title'):
1694 return 'title'
1695 if default_to_id:
1696 return 'id'
1697 props = props.keys()
1698 props.sort()
1699 return props[0]
1701 def lookup(self, keyvalue):
1702 '''Locate a particular node by its key property and return its id.
1704 If this class has no key property, a TypeError is raised. If the
1705 'keyvalue' matches one of the values for the key property among
1706 the nodes in this class, the matching node's id is returned;
1707 otherwise a KeyError is raised.
1708 '''
1709 if not self.key:
1710 raise TypeError, 'No key property set for class %s'%self.classname
1712 # use the arg to handle any odd database type conversion (hello,
1713 # sqlite)
1714 sql = "select id from _%s where _%s=%s and __retired__ <> %s"%(
1715 self.classname, self.key, self.db.arg, self.db.arg)
1716 self.db.sql(sql, (keyvalue, 1))
1718 # see if there was a result that's not retired
1719 row = self.db.sql_fetchone()
1720 if not row:
1721 raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key,
1722 keyvalue, self.classname)
1724 # return the id
1725 return row[0]
1727 def find(self, **propspec):
1728 '''Get the ids of nodes in this class which link to the given nodes.
1730 'propspec' consists of keyword args propname=nodeid or
1731 propname={nodeid:1, }
1732 'propname' must be the name of a property in this class, or a
1733 KeyError is raised. That property must be a Link or Multilink
1734 property, or a TypeError is raised.
1736 Any node in this class whose 'propname' property links to any of the
1737 nodeids will be returned. Used by the full text indexing, which knows
1738 that "foo" occurs in msg1, msg3 and file7, so we have hits on these
1739 issues:
1741 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1742 '''
1743 if __debug__:
1744 print >>hyperdb.DEBUG, 'find', (self, propspec)
1746 # shortcut
1747 if not propspec:
1748 return []
1750 # validate the args
1751 props = self.getprops()
1752 propspec = propspec.items()
1753 for propname, nodeids in propspec:
1754 # check the prop is OK
1755 prop = props[propname]
1756 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
1757 raise TypeError, "'%s' not a Link/Multilink property"%propname
1759 # first, links
1760 where = []
1761 allvalues = ()
1762 a = self.db.arg
1763 for prop, values in propspec:
1764 if not isinstance(props[prop], hyperdb.Link):
1765 continue
1766 if type(values) is type(''):
1767 allvalues += (values,)
1768 where.append('_%s = %s'%(prop, a))
1769 elif values is None:
1770 where.append('_%s is NULL'%prop)
1771 else:
1772 allvalues += tuple(values.keys())
1773 where.append('_%s in (%s)'%(prop, ','.join([a]*len(values))))
1774 tables = []
1775 if where:
1776 tables.append('select id as nodeid from _%s where %s'%(
1777 self.classname, ' and '.join(where)))
1779 # now multilinks
1780 for prop, values in propspec:
1781 if not isinstance(props[prop], hyperdb.Multilink):
1782 continue
1783 if type(values) is type(''):
1784 allvalues += (values,)
1785 s = a
1786 else:
1787 allvalues += tuple(values.keys())
1788 s = ','.join([a]*len(values))
1789 tables.append('select nodeid from %s_%s where linkid in (%s)'%(
1790 self.classname, prop, s))
1791 sql = '\nunion\n'.join(tables)
1792 self.db.sql(sql, allvalues)
1793 l = [x[0] for x in self.db.sql_fetchall()]
1794 if __debug__:
1795 print >>hyperdb.DEBUG, 'find ... ', l
1796 return l
1798 def stringFind(self, **requirements):
1799 '''Locate a particular node by matching a set of its String
1800 properties in a caseless search.
1802 If the property is not a String property, a TypeError is raised.
1804 The return is a list of the id of all nodes that match.
1805 '''
1806 where = []
1807 args = []
1808 for propname in requirements.keys():
1809 prop = self.properties[propname]
1810 if isinstance(not prop, String):
1811 raise TypeError, "'%s' not a String property"%propname
1812 where.append(propname)
1813 args.append(requirements[propname].lower())
1815 # generate the where clause
1816 s = ' and '.join(['lower(_%s)=%s'%(col, self.db.arg) for col in where])
1817 sql = 'select id from _%s where %s'%(self.classname, s)
1818 self.db.sql(sql, tuple(args))
1819 l = [x[0] for x in self.db.sql_fetchall()]
1820 if __debug__:
1821 print >>hyperdb.DEBUG, 'find ... ', l
1822 return l
1824 def list(self):
1825 ''' Return a list of the ids of the active nodes in this class.
1826 '''
1827 return self.getnodeids(retired=0)
1829 def getnodeids(self, retired=None):
1830 ''' Retrieve all the ids of the nodes for a particular Class.
1832 Set retired=None to get all nodes. Otherwise it'll get all the
1833 retired or non-retired nodes, depending on the flag.
1834 '''
1835 # flip the sense of the 'retired' flag if we don't want all of them
1836 if retired is not None:
1837 if retired:
1838 args = (0, )
1839 else:
1840 args = (1, )
1841 sql = 'select id from _%s where __retired__ <> %s'%(self.classname,
1842 self.db.arg)
1843 else:
1844 args = ()
1845 sql = 'select id from _%s'%self.classname
1846 if __debug__:
1847 print >>hyperdb.DEBUG, 'getnodeids', (self, sql, retired)
1848 self.db.cursor.execute(sql, args)
1849 ids = [x[0] for x in self.db.cursor.fetchall()]
1850 return ids
1852 def filter(self, search_matches, filterspec, sort=(None,None),
1853 group=(None,None)):
1854 ''' Return a list of the ids of the active nodes in this class that
1855 match the 'filter' spec, sorted by the group spec and then the
1856 sort spec
1858 "filterspec" is {propname: value(s)}
1859 "sort" and "group" are (dir, prop) where dir is '+', '-' or None
1860 and prop is a prop name or None
1861 "search_matches" is {nodeid: marker}
1863 The filter must match all properties specificed - but if the
1864 property value to match is a list, any one of the values in the
1865 list may match for that property to match.
1866 '''
1867 # just don't bother if the full-text search matched diddly
1868 if search_matches == {}:
1869 return []
1871 cn = self.classname
1873 timezone = self.db.getUserTimezone()
1875 # figure the WHERE clause from the filterspec
1876 props = self.getprops()
1877 frum = ['_'+cn]
1878 where = []
1879 args = []
1880 a = self.db.arg
1881 for k, v in filterspec.items():
1882 propclass = props[k]
1883 # now do other where clause stuff
1884 if isinstance(propclass, Multilink):
1885 tn = '%s_%s'%(cn, k)
1886 if v in ('-1', ['-1']):
1887 # only match rows that have count(linkid)=0 in the
1888 # corresponding multilink table)
1889 where.append('id not in (select nodeid from %s)'%tn)
1890 elif isinstance(v, type([])):
1891 frum.append(tn)
1892 s = ','.join([a for x in v])
1893 where.append('id=%s.nodeid and %s.linkid in (%s)'%(tn,tn,s))
1894 args = args + v
1895 else:
1896 frum.append(tn)
1897 where.append('id=%s.nodeid and %s.linkid=%s'%(tn, tn, a))
1898 args.append(v)
1899 elif k == 'id':
1900 if isinstance(v, type([])):
1901 s = ','.join([a for x in v])
1902 where.append('%s in (%s)'%(k, s))
1903 args = args + v
1904 else:
1905 where.append('%s=%s'%(k, a))
1906 args.append(v)
1907 elif isinstance(propclass, String):
1908 if not isinstance(v, type([])):
1909 v = [v]
1911 # Quote the bits in the string that need it and then embed
1912 # in a "substring" search. Note - need to quote the '%' so
1913 # they make it through the python layer happily
1914 v = ['%%'+self.db.sql_stringquote(s)+'%%' for s in v]
1916 # now add to the where clause
1917 where.append(' or '.join(["_%s LIKE '%s'"%(k, s) for s in v]))
1918 # note: args are embedded in the query string now
1919 elif isinstance(propclass, Link):
1920 if isinstance(v, type([])):
1921 if '-1' in v:
1922 v = v[:]
1923 v.remove('-1')
1924 xtra = ' or _%s is NULL'%k
1925 else:
1926 xtra = ''
1927 if v:
1928 s = ','.join([a for x in v])
1929 where.append('(_%s in (%s)%s)'%(k, s, xtra))
1930 args = args + v
1931 else:
1932 where.append('_%s is NULL'%k)
1933 else:
1934 if v == '-1':
1935 v = None
1936 where.append('_%s is NULL'%k)
1937 else:
1938 where.append('_%s=%s'%(k, a))
1939 args.append(v)
1940 elif isinstance(propclass, Date):
1941 if isinstance(v, type([])):
1942 s = ','.join([a for x in v])
1943 where.append('_%s in (%s)'%(k, s))
1944 args = args + [date.Date(x).serialise() for x in v]
1945 else:
1946 try:
1947 # Try to filter on range of dates
1948 date_rng = Range(v, date.Date, offset=timezone)
1949 if (date_rng.from_value):
1950 where.append('_%s >= %s'%(k, a))
1951 args.append(date_rng.from_value.serialise())
1952 if (date_rng.to_value):
1953 where.append('_%s <= %s'%(k, a))
1954 args.append(date_rng.to_value.serialise())
1955 except ValueError:
1956 # If range creation fails - ignore that search parameter
1957 pass
1958 elif isinstance(propclass, Interval):
1959 if isinstance(v, type([])):
1960 s = ','.join([a for x in v])
1961 where.append('_%s in (%s)'%(k, s))
1962 args = args + [date.Interval(x).serialise() for x in v]
1963 else:
1964 try:
1965 # Try to filter on range of intervals
1966 date_rng = Range(v, date.Interval)
1967 if (date_rng.from_value):
1968 where.append('_%s >= %s'%(k, a))
1969 args.append(date_rng.from_value.serialise())
1970 if (date_rng.to_value):
1971 where.append('_%s <= %s'%(k, a))
1972 args.append(date_rng.to_value.serialise())
1973 except ValueError:
1974 # If range creation fails - ignore that search parameter
1975 pass
1976 #where.append('_%s=%s'%(k, a))
1977 #args.append(date.Interval(v).serialise())
1978 else:
1979 if isinstance(v, type([])):
1980 s = ','.join([a for x in v])
1981 where.append('_%s in (%s)'%(k, s))
1982 args = args + v
1983 else:
1984 where.append('_%s=%s'%(k, a))
1985 args.append(v)
1987 # don't match retired nodes
1988 where.append('__retired__ <> 1')
1990 # add results of full text search
1991 if search_matches is not None:
1992 v = search_matches.keys()
1993 s = ','.join([a for x in v])
1994 where.append('id in (%s)'%s)
1995 args = args + v
1997 # "grouping" is just the first-order sorting in the SQL fetch
1998 # can modify it...)
1999 orderby = []
2000 ordercols = []
2001 if group[0] is not None and group[1] is not None:
2002 if group[0] != '-':
2003 orderby.append('_'+group[1])
2004 ordercols.append('_'+group[1])
2005 else:
2006 orderby.append('_'+group[1]+' desc')
2007 ordercols.append('_'+group[1])
2009 # now add in the sorting
2010 group = ''
2011 if sort[0] is not None and sort[1] is not None:
2012 direction, colname = sort
2013 if direction != '-':
2014 if colname == 'id':
2015 orderby.append(colname)
2016 else:
2017 orderby.append('_'+colname)
2018 ordercols.append('_'+colname)
2019 else:
2020 if colname == 'id':
2021 orderby.append(colname+' desc')
2022 ordercols.append(colname)
2023 else:
2024 orderby.append('_'+colname+' desc')
2025 ordercols.append('_'+colname)
2027 # construct the SQL
2028 frum = ','.join(frum)
2029 if where:
2030 where = ' where ' + (' and '.join(where))
2031 else:
2032 where = ''
2033 cols = ['id']
2034 if orderby:
2035 cols = cols + ordercols
2036 order = ' order by %s'%(','.join(orderby))
2037 else:
2038 order = ''
2039 cols = ','.join(cols)
2040 sql = 'select %s from %s %s%s%s'%(cols, frum, where, group, order)
2041 args = tuple(args)
2042 if __debug__:
2043 print >>hyperdb.DEBUG, 'filter', (self, sql, args)
2044 self.db.cursor.execute(sql, args)
2045 l = self.db.cursor.fetchall()
2047 # return the IDs (the first column)
2048 # XXX The filter(None, l) bit is sqlite-specific... if there's _NO_
2049 # XXX matches to a fetch, it returns NULL instead of nothing!?!
2050 return filter(None, [row[0] for row in l])
2052 def count(self):
2053 '''Get the number of nodes in this class.
2055 If the returned integer is 'numnodes', the ids of all the nodes
2056 in this class run from 1 to numnodes, and numnodes+1 will be the
2057 id of the next node to be created in this class.
2058 '''
2059 return self.db.countnodes(self.classname)
2061 # Manipulating properties:
2062 def getprops(self, protected=1):
2063 '''Return a dictionary mapping property names to property objects.
2064 If the "protected" flag is true, we include protected properties -
2065 those which may not be modified.
2066 '''
2067 d = self.properties.copy()
2068 if protected:
2069 d['id'] = String()
2070 d['creation'] = hyperdb.Date()
2071 d['activity'] = hyperdb.Date()
2072 d['creator'] = hyperdb.Link('user')
2073 return d
2075 def addprop(self, **properties):
2076 '''Add properties to this class.
2078 The keyword arguments in 'properties' must map names to property
2079 objects, or a TypeError is raised. None of the keys in 'properties'
2080 may collide with the names of existing properties, or a ValueError
2081 is raised before any properties have been added.
2082 '''
2083 for key in properties.keys():
2084 if self.properties.has_key(key):
2085 raise ValueError, key
2086 self.properties.update(properties)
2088 def index(self, nodeid):
2089 '''Add (or refresh) the node to search indexes
2090 '''
2091 # find all the String properties that have indexme
2092 for prop, propclass in self.getprops().items():
2093 if isinstance(propclass, String) and propclass.indexme:
2094 try:
2095 value = str(self.get(nodeid, prop))
2096 except IndexError:
2097 # node no longer exists - entry should be removed
2098 self.db.indexer.purge_entry((self.classname, nodeid, prop))
2099 else:
2100 # and index them under (classname, nodeid, property)
2101 self.db.indexer.add_text((self.classname, nodeid, prop),
2102 value)
2105 #
2106 # Detector interface
2107 #
2108 def audit(self, event, detector):
2109 '''Register a detector
2110 '''
2111 l = self.auditors[event]
2112 if detector not in l:
2113 self.auditors[event].append(detector)
2115 def fireAuditors(self, action, nodeid, newvalues):
2116 '''Fire all registered auditors.
2117 '''
2118 for audit in self.auditors[action]:
2119 audit(self.db, self, nodeid, newvalues)
2121 def react(self, event, detector):
2122 '''Register a detector
2123 '''
2124 l = self.reactors[event]
2125 if detector not in l:
2126 self.reactors[event].append(detector)
2128 def fireReactors(self, action, nodeid, oldvalues):
2129 '''Fire all registered reactors.
2130 '''
2131 for react in self.reactors[action]:
2132 react(self.db, self, nodeid, oldvalues)
2134 class FileClass(Class, hyperdb.FileClass):
2135 '''This class defines a large chunk of data. To support this, it has a
2136 mandatory String property "content" which is typically saved off
2137 externally to the hyperdb.
2139 The default MIME type of this data is defined by the
2140 "default_mime_type" class attribute, which may be overridden by each
2141 node if the class defines a "type" String property.
2142 '''
2143 default_mime_type = 'text/plain'
2145 def create(self, **propvalues):
2146 ''' snaffle the file propvalue and store in a file
2147 '''
2148 # we need to fire the auditors now, or the content property won't
2149 # be in propvalues for the auditors to play with
2150 self.fireAuditors('create', None, propvalues)
2152 # now remove the content property so it's not stored in the db
2153 content = propvalues['content']
2154 del propvalues['content']
2156 # do the database create
2157 newid = Class.create_inner(self, **propvalues)
2159 # fire reactors
2160 self.fireReactors('create', newid, None)
2162 # store off the content as a file
2163 self.db.storefile(self.classname, newid, None, content)
2164 return newid
2166 def import_list(self, propnames, proplist):
2167 ''' Trap the "content" property...
2168 '''
2169 # dupe this list so we don't affect others
2170 propnames = propnames[:]
2172 # extract the "content" property from the proplist
2173 i = propnames.index('content')
2174 content = eval(proplist[i])
2175 del propnames[i]
2176 del proplist[i]
2178 # do the normal import
2179 newid = Class.import_list(self, propnames, proplist)
2181 # save off the "content" file
2182 self.db.storefile(self.classname, newid, None, content)
2183 return newid
2185 _marker = []
2186 def get(self, nodeid, propname, default=_marker, cache=1):
2187 ''' Trap the content propname and get it from the file
2189 'cache' exists for backwards compatibility, and is not used.
2190 '''
2191 poss_msg = 'Possibly a access right configuration problem.'
2192 if propname == 'content':
2193 try:
2194 return self.db.getfile(self.classname, nodeid, None)
2195 except IOError, (strerror):
2196 # BUG: by catching this we donot see an error in the log.
2197 return 'ERROR reading file: %s%s\n%s\n%s'%(
2198 self.classname, nodeid, poss_msg, strerror)
2199 if default is not self._marker:
2200 return Class.get(self, nodeid, propname, default)
2201 else:
2202 return Class.get(self, nodeid, propname)
2204 def getprops(self, protected=1):
2205 ''' In addition to the actual properties on the node, these methods
2206 provide the "content" property. If the "protected" flag is true,
2207 we include protected properties - those which may not be
2208 modified.
2209 '''
2210 d = Class.getprops(self, protected=protected).copy()
2211 d['content'] = hyperdb.String()
2212 return d
2214 def index(self, nodeid):
2215 ''' Index the node in the search index.
2217 We want to index the content in addition to the normal String
2218 property indexing.
2219 '''
2220 # perform normal indexing
2221 Class.index(self, nodeid)
2223 # get the content to index
2224 content = self.get(nodeid, 'content')
2226 # figure the mime type
2227 if self.properties.has_key('type'):
2228 mime_type = self.get(nodeid, 'type')
2229 else:
2230 mime_type = self.default_mime_type
2232 # and index!
2233 self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
2234 mime_type)
2236 # XXX deviation from spec - was called ItemClass
2237 class IssueClass(Class, roundupdb.IssueClass):
2238 # Overridden methods:
2239 def __init__(self, db, classname, **properties):
2240 '''The newly-created class automatically includes the "messages",
2241 "files", "nosy", and "superseder" properties. If the 'properties'
2242 dictionary attempts to specify any of these properties or a
2243 "creation" or "activity" property, a ValueError is raised.
2244 '''
2245 if not properties.has_key('title'):
2246 properties['title'] = hyperdb.String(indexme='yes')
2247 if not properties.has_key('messages'):
2248 properties['messages'] = hyperdb.Multilink("msg")
2249 if not properties.has_key('files'):
2250 properties['files'] = hyperdb.Multilink("file")
2251 if not properties.has_key('nosy'):
2252 # note: journalling is turned off as it really just wastes
2253 # space. this behaviour may be overridden in an instance
2254 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
2255 if not properties.has_key('superseder'):
2256 properties['superseder'] = hyperdb.Multilink(classname)
2257 Class.__init__(self, db, classname, **properties)