1 # $Id: rdbms_common.py,v 1.68 2003-11-12 01:00:58 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.sql_open_connection()
74 def clearCache(self):
75 self.cache = {}
76 self.cache_lru = []
78 def sql_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 return self.cursor.fetchone()
98 def sql_fetchall(self):
99 ''' Fetch all rows. If there's nothing to fetch, return [].
100 '''
101 return self.cursor.fetchall()
103 def sql_stringquote(self, value):
104 ''' Quote the string so it's safe to put in the 'sql quotes'
105 '''
106 return re.sub("'", "''", str(value))
108 def save_dbschema(self, schema):
109 ''' Save the schema definition that the database currently implements
110 '''
111 s = repr(self.database_schema)
112 self.sql('insert into schema values (%s)', (s,))
114 def load_dbschema(self):
115 ''' Load the schema definition that the database currently implements
116 '''
117 self.cursor.execute('select schema from schema')
118 return eval(self.cursor.fetchone()[0])
120 def post_init(self):
121 ''' Called once the schema initialisation has finished.
123 We should now confirm that the schema defined by our "classes"
124 attribute actually matches the schema in the database.
125 '''
126 # now detect changes in the schema
127 save = 0
128 for classname, spec in self.classes.items():
129 if self.database_schema.has_key(classname):
130 dbspec = self.database_schema[classname]
131 if self.update_class(spec, dbspec):
132 self.database_schema[classname] = spec.schema()
133 save = 1
134 else:
135 self.create_class(spec)
136 self.database_schema[classname] = spec.schema()
137 save = 1
139 for classname, spec in self.database_schema.items():
140 if not self.classes.has_key(classname):
141 self.drop_class(classname, spec)
142 del self.database_schema[classname]
143 save = 1
145 # update the database version of the schema
146 if save:
147 self.sql('delete from schema')
148 self.save_dbschema(self.database_schema)
150 # reindex the db if necessary
151 if self.indexer.should_reindex():
152 self.reindex()
154 # commit
155 self.conn.commit()
157 def refresh_database(self):
158 # now detect changes in the schema
159 for classname, spec in self.classes.items():
160 dbspec = self.database_schema[classname]
161 self.update_class(spec, dbspec, force=1)
162 self.database_schema[classname] = spec.schema()
163 # update the database version of the schema
164 self.sql('delete from schema')
165 self.save_dbschema(self.database_schema)
166 # reindex the db
167 self.reindex()
168 # commit
169 self.conn.commit()
172 def reindex(self):
173 for klass in self.classes.values():
174 for nodeid in klass.list():
175 klass.index(nodeid)
176 self.indexer.save_index()
178 def determine_columns(self, properties):
179 ''' Figure the column names and multilink properties from the spec
181 "properties" is a list of (name, prop) where prop may be an
182 instance of a hyperdb "type" _or_ a string repr of that type.
183 '''
184 cols = ['_activity', '_creator', '_creation']
185 mls = []
186 # add the multilinks separately
187 for col, prop in properties:
188 if isinstance(prop, Multilink):
189 mls.append(col)
190 elif isinstance(prop, type('')) and prop.find('Multilink') != -1:
191 mls.append(col)
192 else:
193 cols.append('_'+col)
194 cols.sort()
195 return cols, mls
197 def update_class(self, spec, old_spec, force=0):
198 ''' Determine the differences between the current spec and the
199 database version of the spec, and update where necessary.
201 If 'force' is true, update the database anyway.
202 '''
203 new_has = spec.properties.has_key
204 new_spec = spec.schema()
205 new_spec[1].sort()
206 old_spec[1].sort()
207 if not force and new_spec == old_spec:
208 # no changes
209 return 0
211 if __debug__:
212 print >>hyperdb.DEBUG, 'update_class FIRING'
214 # detect multilinks that have been removed, and drop their table
215 old_has = {}
216 for name,prop in old_spec[1]:
217 old_has[name] = 1
218 if new_has(name) or not isinstance(prop, Multilink):
219 continue
220 # it's a multilink, and it's been removed - drop the old
221 # table. First drop indexes.
222 self.drop_multilink_table_indexes(spec.classname, ml)
223 sql = 'drop table %s_%s'%(spec.classname, prop)
224 if __debug__:
225 print >>hyperdb.DEBUG, 'update_class', (self, sql)
226 self.cursor.execute(sql)
227 old_has = old_has.has_key
229 # now figure how we populate the new table
230 fetch = ['_activity', '_creation', '_creator']
231 properties = spec.getprops()
232 for propname,x in new_spec[1]:
233 prop = properties[propname]
234 if isinstance(prop, Multilink):
235 if force or not old_has(propname):
236 # we need to create the new table
237 self.create_multilink_table(spec, propname)
238 elif old_has(propname):
239 # we copy this col over from the old table
240 fetch.append('_'+propname)
242 # select the data out of the old table
243 fetch.append('id')
244 fetch.append('__retired__')
245 fetchcols = ','.join(fetch)
246 cn = spec.classname
247 sql = 'select %s from _%s'%(fetchcols, cn)
248 if __debug__:
249 print >>hyperdb.DEBUG, 'update_class', (self, sql)
250 self.cursor.execute(sql)
251 olddata = self.cursor.fetchall()
253 # TODO: update all the other index dropping code
254 self.drop_class_table_indexes(cn, old_spec[0])
256 # drop the old table
257 self.cursor.execute('drop table _%s'%cn)
259 # create the new table
260 self.create_class_table(spec)
262 if olddata:
263 # do the insert
264 args = ','.join([self.arg for x in fetch])
265 sql = 'insert into _%s (%s) values (%s)'%(cn, fetchcols, args)
266 if __debug__:
267 print >>hyperdb.DEBUG, 'update_class', (self, sql, olddata[0])
268 for entry in olddata:
269 self.cursor.execute(sql, tuple(entry))
271 return 1
273 def create_class_table(self, spec):
274 ''' create the class table for the given spec
275 '''
276 cols, mls = self.determine_columns(spec.properties.items())
278 # add on our special columns
279 cols.append('id')
280 cols.append('__retired__')
282 # create the base table
283 scols = ','.join(['%s varchar'%x for x in cols])
284 sql = 'create table _%s (%s)'%(spec.classname, scols)
285 if __debug__:
286 print >>hyperdb.DEBUG, 'create_class', (self, sql)
287 self.cursor.execute(sql)
289 self.create_class_table_indexes(spec)
291 return cols, mls
293 def create_class_table_indexes(self, spec):
294 ''' create the class table for the given spec
295 '''
296 # create id index
297 index_sql1 = 'create index _%s_id_idx on _%s(id)'%(
298 spec.classname, spec.classname)
299 if __debug__:
300 print >>hyperdb.DEBUG, 'create_index', (self, index_sql1)
301 self.cursor.execute(index_sql1)
303 # create __retired__ index
304 index_sql2 = 'create index _%s_retired_idx on _%s(__retired__)'%(
305 spec.classname, spec.classname)
306 if __debug__:
307 print >>hyperdb.DEBUG, 'create_index', (self, index_sql2)
308 self.cursor.execute(index_sql2)
310 # create index for key property
311 if spec.key:
312 if __debug__:
313 print >>hyperdb.DEBUG, 'update_class setting keyprop %r'% \
314 spec.key
315 index_sql3 = 'create index _%s_%s_idx on _%s(_%s)'%(
316 spec.classname, spec.key,
317 spec.classname, spec.key)
318 if __debug__:
319 print >>hyperdb.DEBUG, 'create_index', (self, index_sql3)
320 self.cursor.execute(index_sql3)
322 def drop_class_table_indexes(self, cn, key):
323 # drop the old table indexes first
324 l = ['_%s_id_idx'%cn, '_%s_retired_idx'%cn]
325 if key:
326 # key prop too?
327 l.append('_%s_%s_idx'%(cn, key))
329 # TODO: update all the other index dropping code
330 table_name = '_%s'%cn
331 for index_name in l:
332 if not self.sql_index_exists(table_name, index_name):
333 continue
334 index_sql = 'drop index '+index_name
335 if __debug__:
336 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
337 self.cursor.execute(index_sql)
339 def create_journal_table(self, spec):
340 ''' create the journal table for a class given the spec and
341 already-determined cols
342 '''
343 # journal table
344 cols = ','.join(['%s varchar'%x
345 for x in 'nodeid date tag action params'.split()])
346 sql = 'create table %s__journal (%s)'%(spec.classname, cols)
347 if __debug__:
348 print >>hyperdb.DEBUG, 'create_class', (self, sql)
349 self.cursor.execute(sql)
350 self.create_journal_table_indexes(spec)
352 def create_journal_table_indexes(self, spec):
353 # index on nodeid
354 index_sql = 'create index %s_journ_idx on %s__journal(nodeid)'%(
355 spec.classname, spec.classname)
356 if __debug__:
357 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
358 self.cursor.execute(index_sql)
360 def drop_journal_table_indexes(self, classname):
361 index_name = '%s_journ_idx'%classname
362 if not self.sql_index_exists('%s__journal'%classname, index_name):
363 return
364 index_sql = 'drop index '+index_name
365 if __debug__:
366 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
367 self.cursor.execute(index_sql)
369 def create_multilink_table(self, spec, ml):
370 ''' Create a multilink table for the "ml" property of the class
371 given by the spec
372 '''
373 # create the table
374 sql = 'create table %s_%s (linkid varchar, nodeid varchar)'%(
375 spec.classname, ml)
376 if __debug__:
377 print >>hyperdb.DEBUG, 'create_class', (self, sql)
378 self.cursor.execute(sql)
379 self.create_multilink_table_indexes(spec, ml)
381 def create_multilink_table_indexes(self, spec, ml):
382 # create index on linkid
383 index_sql = 'create index %s_%s_l_idx on %s_%s(linkid)'%(
384 spec.classname, ml, spec.classname, ml)
385 if __debug__:
386 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
387 self.cursor.execute(index_sql)
389 # create index on nodeid
390 index_sql = 'create index %s_%s_n_idx on %s_%s(nodeid)'%(
391 spec.classname, ml, spec.classname, ml)
392 if __debug__:
393 print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
394 self.cursor.execute(index_sql)
396 def drop_multilink_table_indexes(self, classname, ml):
397 l = [
398 '%s_%s_l_idx'%(classname, ml),
399 '%s_%s_n_idx'%(classname, ml)
400 ]
401 table_name = '%s_%s'%(classname, ml)
402 for index_name in l:
403 if not self.sql_index_exists(table_name, index_name):
404 continue
405 index_sql = 'drop index %s'%index_name
406 if __debug__:
407 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
408 self.cursor.execute(index_sql)
410 def create_class(self, spec):
411 ''' Create a database table according to the given spec.
412 '''
413 cols, mls = self.create_class_table(spec)
414 self.create_journal_table(spec)
416 # now create the multilink tables
417 for ml in mls:
418 self.create_multilink_table(spec, ml)
420 # ID counter
421 sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
422 vals = (spec.classname, 1)
423 if __debug__:
424 print >>hyperdb.DEBUG, 'create_class', (self, sql, vals)
425 self.cursor.execute(sql, vals)
427 def drop_class(self, cn, spec):
428 ''' Drop the given table from the database.
430 Drop the journal and multilink tables too.
431 '''
432 properties = spec[1]
433 # figure the multilinks
434 mls = []
435 for propanme, prop in properties:
436 if isinstance(prop, Multilink):
437 mls.append(propname)
439 # drop class table and indexes
440 self.drop_class_table_indexes(cn, spec[0])
441 sql = 'drop table _%s'%cn
442 if __debug__:
443 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
444 self.cursor.execute(sql)
446 # drop journal table and indexes
447 self.drop_journal_table_indexes(cn)
448 sql = 'drop table %s__journal'%cn
449 if __debug__:
450 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
451 self.cursor.execute(sql)
453 for ml in mls:
454 # drop multilink table and indexes
455 self.drop_multilink_table_indexes(cn, ml)
456 sql = 'drop table %s_%s'%(spec.classname, ml)
457 if __debug__:
458 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
459 self.cursor.execute(sql)
461 #
462 # Classes
463 #
464 def __getattr__(self, classname):
465 ''' A convenient way of calling self.getclass(classname).
466 '''
467 if self.classes.has_key(classname):
468 if __debug__:
469 print >>hyperdb.DEBUG, '__getattr__', (self, classname)
470 return self.classes[classname]
471 raise AttributeError, classname
473 def addclass(self, cl):
474 ''' Add a Class to the hyperdatabase.
475 '''
476 if __debug__:
477 print >>hyperdb.DEBUG, 'addclass', (self, cl)
478 cn = cl.classname
479 if self.classes.has_key(cn):
480 raise ValueError, cn
481 self.classes[cn] = cl
483 def getclasses(self):
484 ''' Return a list of the names of all existing classes.
485 '''
486 if __debug__:
487 print >>hyperdb.DEBUG, 'getclasses', (self,)
488 l = self.classes.keys()
489 l.sort()
490 return l
492 def getclass(self, classname):
493 '''Get the Class object representing a particular class.
495 If 'classname' is not a valid class name, a KeyError is raised.
496 '''
497 if __debug__:
498 print >>hyperdb.DEBUG, 'getclass', (self, classname)
499 try:
500 return self.classes[classname]
501 except KeyError:
502 raise KeyError, 'There is no class called "%s"'%classname
504 def clear(self):
505 ''' Delete all database contents.
507 Note: I don't commit here, which is different behaviour to the
508 "nuke from orbit" behaviour in the *dbms.
509 '''
510 if __debug__:
511 print >>hyperdb.DEBUG, 'clear', (self,)
512 for cn in self.classes.keys():
513 sql = 'delete from _%s'%cn
514 if __debug__:
515 print >>hyperdb.DEBUG, 'clear', (self, sql)
516 self.cursor.execute(sql)
518 #
519 # Node IDs
520 #
521 def newid(self, classname):
522 ''' Generate a new id for the given class
523 '''
524 # get the next ID
525 sql = 'select num from ids where name=%s'%self.arg
526 if __debug__:
527 print >>hyperdb.DEBUG, 'newid', (self, sql, classname)
528 self.cursor.execute(sql, (classname, ))
529 newid = self.cursor.fetchone()[0]
531 # update the counter
532 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
533 vals = (int(newid)+1, classname)
534 if __debug__:
535 print >>hyperdb.DEBUG, 'newid', (self, sql, vals)
536 self.cursor.execute(sql, vals)
538 # return as string
539 return str(newid)
541 def setid(self, classname, setid):
542 ''' Set the id counter: used during import of database
543 '''
544 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
545 vals = (setid, classname)
546 if __debug__:
547 print >>hyperdb.DEBUG, 'setid', (self, sql, vals)
548 self.cursor.execute(sql, vals)
550 #
551 # Nodes
552 #
553 def addnode(self, classname, nodeid, node):
554 ''' Add the specified node to its class's db.
555 '''
556 if __debug__:
557 print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
559 # determine the column definitions and multilink tables
560 cl = self.classes[classname]
561 cols, mls = self.determine_columns(cl.properties.items())
563 # we'll be supplied these props if we're doing an import
564 if not node.has_key('creator'):
565 # add in the "calculated" properties (dupe so we don't affect
566 # calling code's node assumptions)
567 node = node.copy()
568 node['creation'] = node['activity'] = date.Date()
569 node['creator'] = self.getuid()
571 # default the non-multilink columns
572 for col, prop in cl.properties.items():
573 if not node.has_key(col):
574 if isinstance(prop, Multilink):
575 node[col] = []
576 else:
577 node[col] = None
579 # clear this node out of the cache if it's in there
580 key = (classname, nodeid)
581 if self.cache.has_key(key):
582 del self.cache[key]
583 self.cache_lru.remove(key)
585 # make the node data safe for the DB
586 node = self.serialise(classname, node)
588 # make sure the ordering is correct for column name -> column value
589 vals = tuple([node[col[1:]] for col in cols]) + (nodeid, 0)
590 s = ','.join([self.arg for x in cols]) + ',%s,%s'%(self.arg, self.arg)
591 cols = ','.join(cols) + ',id,__retired__'
593 # perform the inserts
594 sql = 'insert into _%s (%s) values (%s)'%(classname, cols, s)
595 if __debug__:
596 print >>hyperdb.DEBUG, 'addnode', (self, sql, vals)
597 self.cursor.execute(sql, vals)
599 # insert the multilink rows
600 for col in mls:
601 t = '%s_%s'%(classname, col)
602 for entry in node[col]:
603 sql = 'insert into %s (linkid, nodeid) values (%s,%s)'%(t,
604 self.arg, self.arg)
605 self.sql(sql, (entry, nodeid))
607 # make sure we do the commit-time extra stuff for this node
608 self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
610 def setnode(self, classname, nodeid, values, multilink_changes):
611 ''' Change the specified node.
612 '''
613 if __debug__:
614 print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, values)
616 # clear this node out of the cache if it's in there
617 key = (classname, nodeid)
618 if self.cache.has_key(key):
619 del self.cache[key]
620 self.cache_lru.remove(key)
622 # add the special props
623 values = values.copy()
624 values['activity'] = date.Date()
626 # make db-friendly
627 values = self.serialise(classname, values)
629 cl = self.classes[classname]
630 cols = []
631 mls = []
632 # add the multilinks separately
633 props = cl.getprops()
634 for col in values.keys():
635 prop = props[col]
636 if isinstance(prop, Multilink):
637 mls.append(col)
638 else:
639 cols.append('_'+col)
640 cols.sort()
642 # if there's any updates to regular columns, do them
643 if cols:
644 # make sure the ordering is correct for column name -> column value
645 sqlvals = tuple([values[col[1:]] for col in cols]) + (nodeid,)
646 s = ','.join(['%s=%s'%(x, self.arg) for x in cols])
647 cols = ','.join(cols)
649 # perform the update
650 sql = 'update _%s set %s where id=%s'%(classname, s, self.arg)
651 if __debug__:
652 print >>hyperdb.DEBUG, 'setnode', (self, sql, sqlvals)
653 self.cursor.execute(sql, sqlvals)
655 # now the fun bit, updating the multilinks ;)
656 for col, (add, remove) in multilink_changes.items():
657 tn = '%s_%s'%(classname, col)
658 if add:
659 sql = 'insert into %s (nodeid, linkid) values (%s,%s)'%(tn,
660 self.arg, self.arg)
661 for addid in add:
662 self.sql(sql, (nodeid, addid))
663 if remove:
664 sql = 'delete from %s where nodeid=%s and linkid=%s'%(tn,
665 self.arg, self.arg)
666 for removeid in remove:
667 self.sql(sql, (nodeid, removeid))
669 # make sure we do the commit-time extra stuff for this node
670 self.transactions.append((self.doSaveNode, (classname, nodeid, values)))
672 def getnode(self, classname, nodeid):
673 ''' Get a node from the database.
674 '''
675 if __debug__:
676 print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid)
678 # see if we have this node cached
679 key = (classname, nodeid)
680 if self.cache.has_key(key):
681 # push us back to the top of the LRU
682 self.cache_lru.remove(key)
683 self.cache_lru.insert(0, key)
684 # return the cached information
685 return self.cache[key]
687 # figure the columns we're fetching
688 cl = self.classes[classname]
689 cols, mls = self.determine_columns(cl.properties.items())
690 scols = ','.join(cols)
692 # perform the basic property fetch
693 sql = 'select %s from _%s where id=%s'%(scols, classname, self.arg)
694 self.sql(sql, (nodeid,))
696 values = self.sql_fetchone()
697 if values is None:
698 raise IndexError, 'no such %s node %s'%(classname, nodeid)
700 # make up the node
701 node = {}
702 for col in range(len(cols)):
703 node[cols[col][1:]] = values[col]
705 # now the multilinks
706 for col in mls:
707 # get the link ids
708 sql = 'select linkid from %s_%s where nodeid=%s'%(classname, col,
709 self.arg)
710 self.cursor.execute(sql, (nodeid,))
711 # extract the first column from the result
712 node[col] = [x[0] for x in self.cursor.fetchall()]
714 # un-dbificate the node data
715 node = self.unserialise(classname, node)
717 # save off in the cache
718 key = (classname, nodeid)
719 self.cache[key] = node
720 # update the LRU
721 self.cache_lru.insert(0, key)
722 if len(self.cache_lru) > ROW_CACHE_SIZE:
723 del self.cache[self.cache_lru.pop()]
725 return node
727 def destroynode(self, classname, nodeid):
728 '''Remove a node from the database. Called exclusively by the
729 destroy() method on Class.
730 '''
731 if __debug__:
732 print >>hyperdb.DEBUG, 'destroynode', (self, classname, nodeid)
734 # make sure the node exists
735 if not self.hasnode(classname, nodeid):
736 raise IndexError, '%s has no node %s'%(classname, nodeid)
738 # see if we have this node cached
739 if self.cache.has_key((classname, nodeid)):
740 del self.cache[(classname, nodeid)]
742 # see if there's any obvious commit actions that we should get rid of
743 for entry in self.transactions[:]:
744 if entry[1][:2] == (classname, nodeid):
745 self.transactions.remove(entry)
747 # now do the SQL
748 sql = 'delete from _%s where id=%s'%(classname, self.arg)
749 self.sql(sql, (nodeid,))
751 # remove from multilnks
752 cl = self.getclass(classname)
753 x, mls = self.determine_columns(cl.properties.items())
754 for col in mls:
755 # get the link ids
756 sql = 'delete from %s_%s where nodeid=%s'%(classname, col, self.arg)
757 self.sql(sql, (nodeid,))
759 # remove journal entries
760 sql = 'delete from %s__journal where nodeid=%s'%(classname, self.arg)
761 self.sql(sql, (nodeid,))
763 def serialise(self, classname, node):
764 '''Copy the node contents, converting non-marshallable data into
765 marshallable data.
766 '''
767 if __debug__:
768 print >>hyperdb.DEBUG, 'serialise', classname, node
769 properties = self.getclass(classname).getprops()
770 d = {}
771 for k, v in node.items():
772 # if the property doesn't exist, or is the "retired" flag then
773 # it won't be in the properties dict
774 if not properties.has_key(k):
775 d[k] = v
776 continue
778 # get the property spec
779 prop = properties[k]
781 if isinstance(prop, Password) and v is not None:
782 d[k] = str(v)
783 elif isinstance(prop, Date) and v is not None:
784 d[k] = v.serialise()
785 elif isinstance(prop, Interval) and v is not None:
786 d[k] = v.serialise()
787 else:
788 d[k] = v
789 return d
791 def unserialise(self, classname, node):
792 '''Decode the marshalled node data
793 '''
794 if __debug__:
795 print >>hyperdb.DEBUG, 'unserialise', classname, node
796 properties = self.getclass(classname).getprops()
797 d = {}
798 for k, v in node.items():
799 # if the property doesn't exist, or is the "retired" flag then
800 # it won't be in the properties dict
801 if not properties.has_key(k):
802 d[k] = v
803 continue
805 # get the property spec
806 prop = properties[k]
808 if isinstance(prop, Date) and v is not None:
809 d[k] = date.Date(v)
810 elif isinstance(prop, Interval) and v is not None:
811 d[k] = date.Interval(v)
812 elif isinstance(prop, Password) and v is not None:
813 p = password.Password()
814 p.unpack(v)
815 d[k] = p
816 elif isinstance(prop, Boolean) and v is not None:
817 d[k] = int(v)
818 elif isinstance(prop, Number) and v is not None:
819 # try int first, then assume it's a float
820 try:
821 d[k] = int(v)
822 except ValueError:
823 d[k] = float(v)
824 else:
825 d[k] = v
826 return d
828 def hasnode(self, classname, nodeid):
829 ''' Determine if the database has a given node.
830 '''
831 sql = 'select count(*) from _%s where id=%s'%(classname, self.arg)
832 if __debug__:
833 print >>hyperdb.DEBUG, 'hasnode', (self, sql, nodeid)
834 self.cursor.execute(sql, (nodeid,))
835 return int(self.cursor.fetchone()[0])
837 def countnodes(self, classname):
838 ''' Count the number of nodes that exist for a particular Class.
839 '''
840 sql = 'select count(*) from _%s'%classname
841 if __debug__:
842 print >>hyperdb.DEBUG, 'countnodes', (self, sql)
843 self.cursor.execute(sql)
844 return self.cursor.fetchone()[0]
846 def addjournal(self, classname, nodeid, action, params, creator=None,
847 creation=None):
848 ''' Journal the Action
849 'action' may be:
851 'create' or 'set' -- 'params' is a dictionary of property values
852 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
853 'retire' -- 'params' is None
854 '''
855 # serialise the parameters now if necessary
856 if isinstance(params, type({})):
857 if action in ('set', 'create'):
858 params = self.serialise(classname, params)
860 # handle supply of the special journalling parameters (usually
861 # supplied on importing an existing database)
862 if creator:
863 journaltag = creator
864 else:
865 journaltag = self.getuid()
866 if creation:
867 journaldate = creation.serialise()
868 else:
869 journaldate = date.Date().serialise()
871 # create the journal entry
872 cols = ','.join('nodeid date tag action params'.split())
874 if __debug__:
875 print >>hyperdb.DEBUG, 'addjournal', (nodeid, journaldate,
876 journaltag, action, params)
878 self.save_journal(classname, cols, nodeid, journaldate,
879 journaltag, action, params)
881 def getjournal(self, classname, nodeid):
882 ''' get the journal for id
883 '''
884 # make sure the node exists
885 if not self.hasnode(classname, nodeid):
886 raise IndexError, '%s has no node %s'%(classname, nodeid)
888 cols = ','.join('nodeid date tag action params'.split())
889 return self.load_journal(classname, cols, nodeid)
891 def save_journal(self, classname, cols, nodeid, journaldate,
892 journaltag, action, params):
893 ''' Save the journal entry to the database
894 '''
895 # make the params db-friendly
896 params = repr(params)
897 entry = (nodeid, journaldate, journaltag, action, params)
899 # do the insert
900 a = self.arg
901 sql = 'insert into %s__journal (%s) values (%s,%s,%s,%s,%s)'%(classname,
902 cols, a, a, a, a, a)
903 if __debug__:
904 print >>hyperdb.DEBUG, 'addjournal', (self, sql, entry)
905 self.cursor.execute(sql, entry)
907 def load_journal(self, classname, cols, nodeid):
908 ''' Load the journal from the database
909 '''
910 # now get the journal entries
911 sql = 'select %s from %s__journal where nodeid=%s'%(cols, classname,
912 self.arg)
913 if __debug__:
914 print >>hyperdb.DEBUG, 'load_journal', (self, sql, nodeid)
915 self.cursor.execute(sql, (nodeid,))
916 res = []
917 for nodeid, date_stamp, user, action, params in self.cursor.fetchall():
918 params = eval(params)
919 res.append((nodeid, date.Date(date_stamp), user, action, params))
920 return res
922 def pack(self, pack_before):
923 ''' Delete all journal entries except "create" before 'pack_before'.
924 '''
925 # get a 'yyyymmddhhmmss' version of the date
926 date_stamp = pack_before.serialise()
928 # do the delete
929 for classname in self.classes.keys():
930 sql = "delete from %s__journal where date<%s and "\
931 "action<>'create'"%(classname, self.arg)
932 if __debug__:
933 print >>hyperdb.DEBUG, 'pack', (self, sql, date_stamp)
934 self.cursor.execute(sql, (date_stamp,))
936 def sql_commit(self):
937 ''' Actually commit to the database.
938 '''
939 self.conn.commit()
941 def commit(self):
942 ''' Commit the current transactions.
944 Save all data changed since the database was opened or since the
945 last commit() or rollback().
946 '''
947 if __debug__:
948 print >>hyperdb.DEBUG, 'commit', (self,)
950 # commit the database
951 self.sql_commit()
953 # now, do all the other transaction stuff
954 reindex = {}
955 for method, args in self.transactions:
956 reindex[method(*args)] = 1
958 # reindex the nodes that request it
959 for classname, nodeid in filter(None, reindex.keys()):
960 print >>hyperdb.DEBUG, 'commit.reindex', (classname, nodeid)
961 self.getclass(classname).index(nodeid)
963 # save the indexer state
964 self.indexer.save_index()
966 # clear out the transactions
967 self.transactions = []
969 def sql_rollback(self):
970 self.conn.rollback()
972 def rollback(self):
973 ''' Reverse all actions from the current transaction.
975 Undo all the changes made since the database was opened or the last
976 commit() or rollback() was performed.
977 '''
978 if __debug__:
979 print >>hyperdb.DEBUG, 'rollback', (self,)
981 self.sql_rollback()
983 # roll back "other" transaction stuff
984 for method, args in self.transactions:
985 # delete temporary files
986 if method == self.doStoreFile:
987 self.rollbackStoreFile(*args)
988 self.transactions = []
990 # clear the cache
991 self.clearCache()
993 def doSaveNode(self, classname, nodeid, node):
994 ''' dummy that just generates a reindex event
995 '''
996 # return the classname, nodeid so we reindex this content
997 return (classname, nodeid)
999 def sql_close(self):
1000 self.conn.close()
1002 def close(self):
1003 ''' Close off the connection.
1004 '''
1005 self.sql_close()
1006 if self.lockfile is not None:
1007 locking.release_lock(self.lockfile)
1008 if self.lockfile is not None:
1009 self.lockfile.close()
1010 self.lockfile = None
1012 #
1013 # The base Class class
1014 #
1015 class Class(hyperdb.Class):
1016 ''' The handle to a particular class of nodes in a hyperdatabase.
1018 All methods except __repr__ and getnode must be implemented by a
1019 concrete backend Class.
1020 '''
1022 def __init__(self, db, classname, **properties):
1023 '''Create a new class with a given name and property specification.
1025 'classname' must not collide with the name of an existing class,
1026 or a ValueError is raised. The keyword arguments in 'properties'
1027 must map names to property objects, or a TypeError is raised.
1028 '''
1029 if (properties.has_key('creation') or properties.has_key('activity')
1030 or properties.has_key('creator')):
1031 raise ValueError, '"creation", "activity" and "creator" are '\
1032 'reserved'
1034 self.classname = classname
1035 self.properties = properties
1036 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
1037 self.key = ''
1039 # should we journal changes (default yes)
1040 self.do_journal = 1
1042 # do the db-related init stuff
1043 db.addclass(self)
1045 self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
1046 self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
1048 def schema(self):
1049 ''' A dumpable version of the schema that we can store in the
1050 database
1051 '''
1052 return (self.key, [(x, repr(y)) for x,y in self.properties.items()])
1054 def enableJournalling(self):
1055 '''Turn journalling on for this class
1056 '''
1057 self.do_journal = 1
1059 def disableJournalling(self):
1060 '''Turn journalling off for this class
1061 '''
1062 self.do_journal = 0
1064 # Editing nodes:
1065 def create(self, **propvalues):
1066 ''' Create a new node of this class and return its id.
1068 The keyword arguments in 'propvalues' map property names to values.
1070 The values of arguments must be acceptable for the types of their
1071 corresponding properties or a TypeError is raised.
1073 If this class has a key property, it must be present and its value
1074 must not collide with other key strings or a ValueError is raised.
1076 Any other properties on this class that are missing from the
1077 'propvalues' dictionary are set to None.
1079 If an id in a link or multilink property does not refer to a valid
1080 node, an IndexError is raised.
1081 '''
1082 self.fireAuditors('create', None, propvalues)
1083 newid = self.create_inner(**propvalues)
1084 self.fireReactors('create', newid, None)
1085 return newid
1087 def create_inner(self, **propvalues):
1088 ''' Called by create, in-between the audit and react calls.
1089 '''
1090 if propvalues.has_key('id'):
1091 raise KeyError, '"id" is reserved'
1093 if self.db.journaltag is None:
1094 raise DatabaseError, 'Database open read-only'
1096 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1097 raise KeyError, '"creation" and "activity" are reserved'
1099 # new node's id
1100 newid = self.db.newid(self.classname)
1102 # validate propvalues
1103 num_re = re.compile('^\d+$')
1104 for key, value in propvalues.items():
1105 if key == self.key:
1106 try:
1107 self.lookup(value)
1108 except KeyError:
1109 pass
1110 else:
1111 raise ValueError, 'node with key "%s" exists'%value
1113 # try to handle this property
1114 try:
1115 prop = self.properties[key]
1116 except KeyError:
1117 raise KeyError, '"%s" has no property "%s"'%(self.classname,
1118 key)
1120 if value is not None and isinstance(prop, Link):
1121 if type(value) != type(''):
1122 raise ValueError, 'link value must be String'
1123 link_class = self.properties[key].classname
1124 # if it isn't a number, it's a key
1125 if not num_re.match(value):
1126 try:
1127 value = self.db.classes[link_class].lookup(value)
1128 except (TypeError, KeyError):
1129 raise IndexError, 'new property "%s": %s not a %s'%(
1130 key, value, link_class)
1131 elif not self.db.getclass(link_class).hasnode(value):
1132 raise IndexError, '%s has no node %s'%(link_class, value)
1134 # save off the value
1135 propvalues[key] = value
1137 # register the link with the newly linked node
1138 if self.do_journal and self.properties[key].do_journal:
1139 self.db.addjournal(link_class, value, 'link',
1140 (self.classname, newid, key))
1142 elif isinstance(prop, Multilink):
1143 if type(value) != type([]):
1144 raise TypeError, 'new property "%s" not a list of ids'%key
1146 # clean up and validate the list of links
1147 link_class = self.properties[key].classname
1148 l = []
1149 for entry in value:
1150 if type(entry) != type(''):
1151 raise ValueError, '"%s" multilink value (%r) '\
1152 'must contain Strings'%(key, value)
1153 # if it isn't a number, it's a key
1154 if not num_re.match(entry):
1155 try:
1156 entry = self.db.classes[link_class].lookup(entry)
1157 except (TypeError, KeyError):
1158 raise IndexError, 'new property "%s": %s not a %s'%(
1159 key, entry, self.properties[key].classname)
1160 l.append(entry)
1161 value = l
1162 propvalues[key] = value
1164 # handle additions
1165 for nodeid in value:
1166 if not self.db.getclass(link_class).hasnode(nodeid):
1167 raise IndexError, '%s has no node %s'%(link_class,
1168 nodeid)
1169 # register the link with the newly linked node
1170 if self.do_journal and self.properties[key].do_journal:
1171 self.db.addjournal(link_class, nodeid, 'link',
1172 (self.classname, newid, key))
1174 elif isinstance(prop, String):
1175 if type(value) != type('') and type(value) != type(u''):
1176 raise TypeError, 'new property "%s" not a string'%key
1178 elif isinstance(prop, Password):
1179 if not isinstance(value, password.Password):
1180 raise TypeError, 'new property "%s" not a Password'%key
1182 elif isinstance(prop, Date):
1183 if value is not None and not isinstance(value, date.Date):
1184 raise TypeError, 'new property "%s" not a Date'%key
1186 elif isinstance(prop, Interval):
1187 if value is not None and not isinstance(value, date.Interval):
1188 raise TypeError, 'new property "%s" not an Interval'%key
1190 elif value is not None and isinstance(prop, Number):
1191 try:
1192 float(value)
1193 except ValueError:
1194 raise TypeError, 'new property "%s" not numeric'%key
1196 elif value is not None and isinstance(prop, Boolean):
1197 try:
1198 int(value)
1199 except ValueError:
1200 raise TypeError, 'new property "%s" not boolean'%key
1202 # make sure there's data where there needs to be
1203 for key, prop in self.properties.items():
1204 if propvalues.has_key(key):
1205 continue
1206 if key == self.key:
1207 raise ValueError, 'key property "%s" is required'%key
1208 if isinstance(prop, Multilink):
1209 propvalues[key] = []
1210 else:
1211 propvalues[key] = None
1213 # done
1214 self.db.addnode(self.classname, newid, propvalues)
1215 if self.do_journal:
1216 self.db.addjournal(self.classname, newid, 'create', {})
1218 return newid
1220 def export_list(self, propnames, nodeid):
1221 ''' Export a node - generate a list of CSV-able data in the order
1222 specified by propnames for the given node.
1223 '''
1224 properties = self.getprops()
1225 l = []
1226 for prop in propnames:
1227 proptype = properties[prop]
1228 value = self.get(nodeid, prop)
1229 # "marshal" data where needed
1230 if value is None:
1231 pass
1232 elif isinstance(proptype, hyperdb.Date):
1233 value = value.get_tuple()
1234 elif isinstance(proptype, hyperdb.Interval):
1235 value = value.get_tuple()
1236 elif isinstance(proptype, hyperdb.Password):
1237 value = str(value)
1238 l.append(repr(value))
1239 l.append(self.is_retired(nodeid))
1240 return l
1242 def import_list(self, propnames, proplist):
1243 ''' Import a node - all information including "id" is present and
1244 should not be sanity checked. Triggers are not triggered. The
1245 journal should be initialised using the "creator" and "created"
1246 information.
1248 Return the nodeid of the node imported.
1249 '''
1250 if self.db.journaltag is None:
1251 raise DatabaseError, 'Database open read-only'
1252 properties = self.getprops()
1254 # make the new node's property map
1255 d = {}
1256 retire = 0
1257 newid = None
1258 for i in range(len(propnames)):
1259 # Use eval to reverse the repr() used to output the CSV
1260 value = eval(proplist[i])
1262 # Figure the property for this column
1263 propname = propnames[i]
1265 # "unmarshal" where necessary
1266 if propname == 'id':
1267 newid = value
1268 continue
1269 elif propname == 'is retired':
1270 # is the item retired?
1271 if int(value):
1272 retire = 1
1273 continue
1274 elif value is None:
1275 d[propname] = None
1276 continue
1278 prop = properties[propname]
1279 if value is None:
1280 # don't set Nones
1281 continue
1282 elif isinstance(prop, hyperdb.Date):
1283 value = date.Date(value)
1284 elif isinstance(prop, hyperdb.Interval):
1285 value = date.Interval(value)
1286 elif isinstance(prop, hyperdb.Password):
1287 pwd = password.Password()
1288 pwd.unpack(value)
1289 value = pwd
1290 d[propname] = value
1292 # get a new id if necessary
1293 if newid is None:
1294 newid = self.db.newid(self.classname)
1296 # retire?
1297 if retire:
1298 # use the arg for __retired__ to cope with any odd database type
1299 # conversion (hello, sqlite)
1300 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1301 self.db.arg, self.db.arg)
1302 if __debug__:
1303 print >>hyperdb.DEBUG, 'retire', (self, sql, newid)
1304 self.db.cursor.execute(sql, (1, newid))
1306 # add the node and journal
1307 self.db.addnode(self.classname, newid, d)
1309 # extract the extraneous journalling gumpf and nuke it
1310 if d.has_key('creator'):
1311 creator = d['creator']
1312 del d['creator']
1313 else:
1314 creator = None
1315 if d.has_key('creation'):
1316 creation = d['creation']
1317 del d['creation']
1318 else:
1319 creation = None
1320 if d.has_key('activity'):
1321 del d['activity']
1322 self.db.addjournal(self.classname, newid, 'create', {}, creator,
1323 creation)
1324 return newid
1326 _marker = []
1327 def get(self, nodeid, propname, default=_marker, cache=1):
1328 '''Get the value of a property on an existing node of this class.
1330 'nodeid' must be the id of an existing node of this class or an
1331 IndexError is raised. 'propname' must be the name of a property
1332 of this class or a KeyError is raised.
1334 'cache' exists for backwards compatibility, and is not used.
1335 '''
1336 if propname == 'id':
1337 return nodeid
1339 # get the node's dict
1340 d = self.db.getnode(self.classname, nodeid)
1342 if propname == 'creation':
1343 if d.has_key('creation'):
1344 return d['creation']
1345 else:
1346 return date.Date()
1347 if propname == 'activity':
1348 if d.has_key('activity'):
1349 return d['activity']
1350 else:
1351 return date.Date()
1352 if propname == 'creator':
1353 if d.has_key('creator'):
1354 return d['creator']
1355 else:
1356 return self.db.getuid()
1358 # get the property (raises KeyErorr if invalid)
1359 prop = self.properties[propname]
1361 if not d.has_key(propname):
1362 if default is self._marker:
1363 if isinstance(prop, Multilink):
1364 return []
1365 else:
1366 return None
1367 else:
1368 return default
1370 # don't pass our list to other code
1371 if isinstance(prop, Multilink):
1372 return d[propname][:]
1374 return d[propname]
1376 def getnode(self, nodeid, cache=1):
1377 ''' Return a convenience wrapper for the node.
1379 'nodeid' must be the id of an existing node of this class or an
1380 IndexError is raised.
1382 'cache' exists for backwards compatibility, and is not used.
1383 '''
1384 return Node(self, nodeid)
1386 def set(self, nodeid, **propvalues):
1387 '''Modify a property on an existing node of this class.
1389 'nodeid' must be the id of an existing node of this class or an
1390 IndexError is raised.
1392 Each key in 'propvalues' must be the name of a property of this
1393 class or a KeyError is raised.
1395 All values in 'propvalues' must be acceptable types for their
1396 corresponding properties or a TypeError is raised.
1398 If the value of the key property is set, it must not collide with
1399 other key strings or a ValueError is raised.
1401 If the value of a Link or Multilink property contains an invalid
1402 node id, a ValueError is raised.
1403 '''
1404 if not propvalues:
1405 return propvalues
1407 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1408 raise KeyError, '"creation" and "activity" are reserved'
1410 if propvalues.has_key('id'):
1411 raise KeyError, '"id" is reserved'
1413 if self.db.journaltag is None:
1414 raise DatabaseError, 'Database open read-only'
1416 self.fireAuditors('set', nodeid, propvalues)
1417 # Take a copy of the node dict so that the subsequent set
1418 # operation doesn't modify the oldvalues structure.
1419 # XXX used to try the cache here first
1420 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
1422 node = self.db.getnode(self.classname, nodeid)
1423 if self.is_retired(nodeid):
1424 raise IndexError, 'Requested item is retired'
1425 num_re = re.compile('^\d+$')
1427 # if the journal value is to be different, store it in here
1428 journalvalues = {}
1430 # remember the add/remove stuff for multilinks, making it easier
1431 # for the Database layer to do its stuff
1432 multilink_changes = {}
1434 for propname, value in propvalues.items():
1435 # check to make sure we're not duplicating an existing key
1436 if propname == self.key and node[propname] != value:
1437 try:
1438 self.lookup(value)
1439 except KeyError:
1440 pass
1441 else:
1442 raise ValueError, 'node with key "%s" exists'%value
1444 # this will raise the KeyError if the property isn't valid
1445 # ... we don't use getprops() here because we only care about
1446 # the writeable properties.
1447 try:
1448 prop = self.properties[propname]
1449 except KeyError:
1450 raise KeyError, '"%s" has no property named "%s"'%(
1451 self.classname, propname)
1453 # if the value's the same as the existing value, no sense in
1454 # doing anything
1455 current = node.get(propname, None)
1456 if value == current:
1457 del propvalues[propname]
1458 continue
1459 journalvalues[propname] = current
1461 # do stuff based on the prop type
1462 if isinstance(prop, Link):
1463 link_class = prop.classname
1464 # if it isn't a number, it's a key
1465 if value is not None and not isinstance(value, type('')):
1466 raise ValueError, 'property "%s" link value be a string'%(
1467 propname)
1468 if isinstance(value, type('')) and not num_re.match(value):
1469 try:
1470 value = self.db.classes[link_class].lookup(value)
1471 except (TypeError, KeyError):
1472 raise IndexError, 'new property "%s": %s not a %s'%(
1473 propname, value, prop.classname)
1475 if (value is not None and
1476 not self.db.getclass(link_class).hasnode(value)):
1477 raise IndexError, '%s has no node %s'%(link_class, value)
1479 if self.do_journal and prop.do_journal:
1480 # register the unlink with the old linked node
1481 if node[propname] is not None:
1482 self.db.addjournal(link_class, node[propname], 'unlink',
1483 (self.classname, nodeid, propname))
1485 # register the link with the newly linked node
1486 if value is not None:
1487 self.db.addjournal(link_class, value, 'link',
1488 (self.classname, nodeid, propname))
1490 elif isinstance(prop, Multilink):
1491 if type(value) != type([]):
1492 raise TypeError, 'new property "%s" not a list of'\
1493 ' ids'%propname
1494 link_class = self.properties[propname].classname
1495 l = []
1496 for entry in value:
1497 # if it isn't a number, it's a key
1498 if type(entry) != type(''):
1499 raise ValueError, 'new property "%s" link value ' \
1500 'must be a string'%propname
1501 if not num_re.match(entry):
1502 try:
1503 entry = self.db.classes[link_class].lookup(entry)
1504 except (TypeError, KeyError):
1505 raise IndexError, 'new property "%s": %s not a %s'%(
1506 propname, entry,
1507 self.properties[propname].classname)
1508 l.append(entry)
1509 value = l
1510 propvalues[propname] = value
1512 # figure the journal entry for this property
1513 add = []
1514 remove = []
1516 # handle removals
1517 if node.has_key(propname):
1518 l = node[propname]
1519 else:
1520 l = []
1521 for id in l[:]:
1522 if id in value:
1523 continue
1524 # register the unlink with the old linked node
1525 if self.do_journal and self.properties[propname].do_journal:
1526 self.db.addjournal(link_class, id, 'unlink',
1527 (self.classname, nodeid, propname))
1528 l.remove(id)
1529 remove.append(id)
1531 # handle additions
1532 for id in value:
1533 if not self.db.getclass(link_class).hasnode(id):
1534 raise IndexError, '%s has no node %s'%(link_class, id)
1535 if id in l:
1536 continue
1537 # register the link with the newly linked node
1538 if self.do_journal and self.properties[propname].do_journal:
1539 self.db.addjournal(link_class, id, 'link',
1540 (self.classname, nodeid, propname))
1541 l.append(id)
1542 add.append(id)
1544 # figure the journal entry
1545 l = []
1546 if add:
1547 l.append(('+', add))
1548 if remove:
1549 l.append(('-', remove))
1550 multilink_changes[propname] = (add, remove)
1551 if l:
1552 journalvalues[propname] = tuple(l)
1554 elif isinstance(prop, String):
1555 if value is not None and type(value) != type('') and type(value) != type(u''):
1556 raise TypeError, 'new property "%s" not a string'%propname
1558 elif isinstance(prop, Password):
1559 if not isinstance(value, password.Password):
1560 raise TypeError, 'new property "%s" not a Password'%propname
1561 propvalues[propname] = value
1563 elif value is not None and isinstance(prop, Date):
1564 if not isinstance(value, date.Date):
1565 raise TypeError, 'new property "%s" not a Date'% propname
1566 propvalues[propname] = value
1568 elif value is not None and isinstance(prop, Interval):
1569 if not isinstance(value, date.Interval):
1570 raise TypeError, 'new property "%s" not an '\
1571 'Interval'%propname
1572 propvalues[propname] = value
1574 elif value is not None and isinstance(prop, Number):
1575 try:
1576 float(value)
1577 except ValueError:
1578 raise TypeError, 'new property "%s" not numeric'%propname
1580 elif value is not None and isinstance(prop, Boolean):
1581 try:
1582 int(value)
1583 except ValueError:
1584 raise TypeError, 'new property "%s" not boolean'%propname
1586 # nothing to do?
1587 if not propvalues:
1588 return propvalues
1590 # do the set, and journal it
1591 self.db.setnode(self.classname, nodeid, propvalues, multilink_changes)
1593 if self.do_journal:
1594 self.db.addjournal(self.classname, nodeid, 'set', journalvalues)
1596 self.fireReactors('set', nodeid, oldvalues)
1598 return propvalues
1600 def retire(self, nodeid):
1601 '''Retire a node.
1603 The properties on the node remain available from the get() method,
1604 and the node's id is never reused.
1606 Retired nodes are not returned by the find(), list(), or lookup()
1607 methods, and other nodes may reuse the values of their key properties.
1608 '''
1609 if self.db.journaltag is None:
1610 raise DatabaseError, 'Database open read-only'
1612 self.fireAuditors('retire', nodeid, None)
1614 # use the arg for __retired__ to cope with any odd database type
1615 # conversion (hello, sqlite)
1616 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1617 self.db.arg, self.db.arg)
1618 if __debug__:
1619 print >>hyperdb.DEBUG, 'retire', (self, sql, nodeid)
1620 self.db.cursor.execute(sql, (1, nodeid))
1621 if self.do_journal:
1622 self.db.addjournal(self.classname, nodeid, 'retired', None)
1624 self.fireReactors('retire', nodeid, None)
1626 def restore(self, nodeid):
1627 '''Restore a retired node.
1629 Make node available for all operations like it was before retirement.
1630 '''
1631 if self.db.journaltag is None:
1632 raise DatabaseError, 'Database open read-only'
1634 node = self.db.getnode(self.classname, nodeid)
1635 # check if key property was overrided
1636 key = self.getkey()
1637 try:
1638 id = self.lookup(node[key])
1639 except KeyError:
1640 pass
1641 else:
1642 raise KeyError, "Key property (%s) of retired node clashes with \
1643 existing one (%s)" % (key, node[key])
1645 self.fireAuditors('restore', nodeid, None)
1646 # use the arg for __retired__ to cope with any odd database type
1647 # conversion (hello, sqlite)
1648 sql = 'update _%s set __retired__=%s where id=%s'%(self.classname,
1649 self.db.arg, self.db.arg)
1650 if __debug__:
1651 print >>hyperdb.DEBUG, 'restore', (self, sql, nodeid)
1652 self.db.cursor.execute(sql, (0, nodeid))
1653 if self.do_journal:
1654 self.db.addjournal(self.classname, nodeid, 'restored', None)
1656 self.fireReactors('restore', nodeid, None)
1658 def is_retired(self, nodeid):
1659 '''Return true if the node is rerired
1660 '''
1661 sql = 'select __retired__ from _%s where id=%s'%(self.classname,
1662 self.db.arg)
1663 if __debug__:
1664 print >>hyperdb.DEBUG, 'is_retired', (self, sql, nodeid)
1665 self.db.cursor.execute(sql, (nodeid,))
1666 return int(self.db.sql_fetchone()[0])
1668 def destroy(self, nodeid):
1669 '''Destroy a node.
1671 WARNING: this method should never be used except in extremely rare
1672 situations where there could never be links to the node being
1673 deleted
1674 WARNING: use retire() instead
1675 WARNING: the properties of this node will not be available ever again
1676 WARNING: really, use retire() instead
1678 Well, I think that's enough warnings. This method exists mostly to
1679 support the session storage of the cgi interface.
1681 The node is completely removed from the hyperdb, including all journal
1682 entries. It will no longer be available, and will generally break code
1683 if there are any references to the node.
1684 '''
1685 if self.db.journaltag is None:
1686 raise DatabaseError, 'Database open read-only'
1687 self.db.destroynode(self.classname, nodeid)
1689 def history(self, nodeid):
1690 '''Retrieve the journal of edits on a particular node.
1692 'nodeid' must be the id of an existing node of this class or an
1693 IndexError is raised.
1695 The returned list contains tuples of the form
1697 (nodeid, date, tag, action, params)
1699 'date' is a Timestamp object specifying the time of the change and
1700 'tag' is the journaltag specified when the database was opened.
1701 '''
1702 if not self.do_journal:
1703 raise ValueError, 'Journalling is disabled for this class'
1704 return self.db.getjournal(self.classname, nodeid)
1706 # Locating nodes:
1707 def hasnode(self, nodeid):
1708 '''Determine if the given nodeid actually exists
1709 '''
1710 return self.db.hasnode(self.classname, nodeid)
1712 def setkey(self, propname):
1713 '''Select a String property of this class to be the key property.
1715 'propname' must be the name of a String property of this class or
1716 None, or a TypeError is raised. The values of the key property on
1717 all existing nodes must be unique or a ValueError is raised.
1718 '''
1719 # XXX create an index on the key prop column. We should also
1720 # record that we've created this index in the schema somewhere.
1721 prop = self.getprops()[propname]
1722 if not isinstance(prop, String):
1723 raise TypeError, 'key properties must be String'
1724 self.key = propname
1726 def getkey(self):
1727 '''Return the name of the key property for this class or None.'''
1728 return self.key
1730 def labelprop(self, default_to_id=0):
1731 ''' Return the property name for a label for the given node.
1733 This method attempts to generate a consistent label for the node.
1734 It tries the following in order:
1735 1. key property
1736 2. "name" property
1737 3. "title" property
1738 4. first property from the sorted property name list
1739 '''
1740 k = self.getkey()
1741 if k:
1742 return k
1743 props = self.getprops()
1744 if props.has_key('name'):
1745 return 'name'
1746 elif props.has_key('title'):
1747 return 'title'
1748 if default_to_id:
1749 return 'id'
1750 props = props.keys()
1751 props.sort()
1752 return props[0]
1754 def lookup(self, keyvalue):
1755 '''Locate a particular node by its key property and return its id.
1757 If this class has no key property, a TypeError is raised. If the
1758 'keyvalue' matches one of the values for the key property among
1759 the nodes in this class, the matching node's id is returned;
1760 otherwise a KeyError is raised.
1761 '''
1762 if not self.key:
1763 raise TypeError, 'No key property set for class %s'%self.classname
1765 # use the arg to handle any odd database type conversion (hello,
1766 # sqlite)
1767 sql = "select id from _%s where _%s=%s and __retired__ <> %s"%(
1768 self.classname, self.key, self.db.arg, self.db.arg)
1769 self.db.sql(sql, (keyvalue, 1))
1771 # see if there was a result that's not retired
1772 row = self.db.sql_fetchone()
1773 if not row:
1774 raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key,
1775 keyvalue, self.classname)
1777 # return the id
1778 return row[0]
1780 def find(self, **propspec):
1781 '''Get the ids of nodes in this class which link to the given nodes.
1783 'propspec' consists of keyword args propname=nodeid or
1784 propname={nodeid:1, }
1785 'propname' must be the name of a property in this class, or a
1786 KeyError is raised. That property must be a Link or Multilink
1787 property, or a TypeError is raised.
1789 Any node in this class whose 'propname' property links to any of the
1790 nodeids will be returned. Used by the full text indexing, which knows
1791 that "foo" occurs in msg1, msg3 and file7, so we have hits on these
1792 issues:
1794 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1795 '''
1796 if __debug__:
1797 print >>hyperdb.DEBUG, 'find', (self, propspec)
1799 # shortcut
1800 if not propspec:
1801 return []
1803 # validate the args
1804 props = self.getprops()
1805 propspec = propspec.items()
1806 for propname, nodeids in propspec:
1807 # check the prop is OK
1808 prop = props[propname]
1809 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
1810 raise TypeError, "'%s' not a Link/Multilink property"%propname
1812 # first, links
1813 where = []
1814 allvalues = ()
1815 a = self.db.arg
1816 for prop, values in propspec:
1817 if not isinstance(props[prop], hyperdb.Link):
1818 continue
1819 if type(values) is type(''):
1820 allvalues += (values,)
1821 where.append('_%s = %s'%(prop, a))
1822 elif values is None:
1823 where.append('_%s is NULL'%prop)
1824 else:
1825 allvalues += tuple(values.keys())
1826 where.append('_%s in (%s)'%(prop, ','.join([a]*len(values))))
1827 tables = []
1828 if where:
1829 tables.append('select id as nodeid from _%s where %s'%(
1830 self.classname, ' and '.join(where)))
1832 # now multilinks
1833 for prop, values in propspec:
1834 if not isinstance(props[prop], hyperdb.Multilink):
1835 continue
1836 if type(values) is type(''):
1837 allvalues += (values,)
1838 s = a
1839 else:
1840 allvalues += tuple(values.keys())
1841 s = ','.join([a]*len(values))
1842 tables.append('select nodeid from %s_%s where linkid in (%s)'%(
1843 self.classname, prop, s))
1844 sql = '\nunion\n'.join(tables)
1845 self.db.sql(sql, allvalues)
1846 l = [x[0] for x in self.db.sql_fetchall()]
1847 if __debug__:
1848 print >>hyperdb.DEBUG, 'find ... ', l
1849 return l
1851 def stringFind(self, **requirements):
1852 '''Locate a particular node by matching a set of its String
1853 properties in a caseless search.
1855 If the property is not a String property, a TypeError is raised.
1857 The return is a list of the id of all nodes that match.
1858 '''
1859 where = []
1860 args = []
1861 for propname in requirements.keys():
1862 prop = self.properties[propname]
1863 if isinstance(not prop, String):
1864 raise TypeError, "'%s' not a String property"%propname
1865 where.append(propname)
1866 args.append(requirements[propname].lower())
1868 # generate the where clause
1869 s = ' and '.join(['lower(_%s)=%s'%(col, self.db.arg) for col in where])
1870 sql = 'select id from _%s where %s'%(self.classname, s)
1871 self.db.sql(sql, tuple(args))
1872 l = [x[0] for x in self.db.sql_fetchall()]
1873 if __debug__:
1874 print >>hyperdb.DEBUG, 'find ... ', l
1875 return l
1877 def list(self):
1878 ''' Return a list of the ids of the active nodes in this class.
1879 '''
1880 return self.getnodeids(retired=0)
1882 def getnodeids(self, retired=None):
1883 ''' Retrieve all the ids of the nodes for a particular Class.
1885 Set retired=None to get all nodes. Otherwise it'll get all the
1886 retired or non-retired nodes, depending on the flag.
1887 '''
1888 # flip the sense of the 'retired' flag if we don't want all of them
1889 if retired is not None:
1890 if retired:
1891 args = (0, )
1892 else:
1893 args = (1, )
1894 sql = 'select id from _%s where __retired__ <> %s'%(self.classname,
1895 self.db.arg)
1896 else:
1897 args = ()
1898 sql = 'select id from _%s'%self.classname
1899 if __debug__:
1900 print >>hyperdb.DEBUG, 'getnodeids', (self, sql, retired)
1901 self.db.cursor.execute(sql, args)
1902 ids = [x[0] for x in self.db.cursor.fetchall()]
1903 return ids
1905 def filter(self, search_matches, filterspec, sort=(None,None),
1906 group=(None,None)):
1907 ''' Return a list of the ids of the active nodes in this class that
1908 match the 'filter' spec, sorted by the group spec and then the
1909 sort spec
1911 "filterspec" is {propname: value(s)}
1912 "sort" and "group" are (dir, prop) where dir is '+', '-' or None
1913 and prop is a prop name or None
1914 "search_matches" is {nodeid: marker}
1916 The filter must match all properties specificed - but if the
1917 property value to match is a list, any one of the values in the
1918 list may match for that property to match.
1919 '''
1920 # just don't bother if the full-text search matched diddly
1921 if search_matches == {}:
1922 return []
1924 cn = self.classname
1926 timezone = self.db.getUserTimezone()
1928 # figure the WHERE clause from the filterspec
1929 props = self.getprops()
1930 frum = ['_'+cn]
1931 where = []
1932 args = []
1933 a = self.db.arg
1934 for k, v in filterspec.items():
1935 propclass = props[k]
1936 # now do other where clause stuff
1937 if isinstance(propclass, Multilink):
1938 tn = '%s_%s'%(cn, k)
1939 if v in ('-1', ['-1']):
1940 # only match rows that have count(linkid)=0 in the
1941 # corresponding multilink table)
1942 where.append('id not in (select nodeid from %s)'%tn)
1943 elif isinstance(v, type([])):
1944 frum.append(tn)
1945 s = ','.join([a for x in v])
1946 where.append('id=%s.nodeid and %s.linkid in (%s)'%(tn,tn,s))
1947 args = args + v
1948 else:
1949 frum.append(tn)
1950 where.append('id=%s.nodeid and %s.linkid=%s'%(tn, tn, a))
1951 args.append(v)
1952 elif k == 'id':
1953 if isinstance(v, type([])):
1954 s = ','.join([a for x in v])
1955 where.append('%s in (%s)'%(k, s))
1956 args = args + v
1957 else:
1958 where.append('%s=%s'%(k, a))
1959 args.append(v)
1960 elif isinstance(propclass, String):
1961 if not isinstance(v, type([])):
1962 v = [v]
1964 # Quote the bits in the string that need it and then embed
1965 # in a "substring" search. Note - need to quote the '%' so
1966 # they make it through the python layer happily
1967 v = ['%%'+self.db.sql_stringquote(s)+'%%' for s in v]
1969 # now add to the where clause
1970 where.append(' or '.join(["_%s LIKE '%s'"%(k, s) for s in v]))
1971 # note: args are embedded in the query string now
1972 elif isinstance(propclass, Link):
1973 if isinstance(v, type([])):
1974 if '-1' in v:
1975 v = v[:]
1976 v.remove('-1')
1977 xtra = ' or _%s is NULL'%k
1978 else:
1979 xtra = ''
1980 if v:
1981 s = ','.join([a for x in v])
1982 where.append('(_%s in (%s)%s)'%(k, s, xtra))
1983 args = args + v
1984 else:
1985 where.append('_%s is NULL'%k)
1986 else:
1987 if v == '-1':
1988 v = None
1989 where.append('_%s is NULL'%k)
1990 else:
1991 where.append('_%s=%s'%(k, a))
1992 args.append(v)
1993 elif isinstance(propclass, Date):
1994 if isinstance(v, type([])):
1995 s = ','.join([a for x in v])
1996 where.append('_%s in (%s)'%(k, s))
1997 args = args + [date.Date(x).serialise() for x in v]
1998 else:
1999 try:
2000 # Try to filter on range of dates
2001 date_rng = Range(v, date.Date, offset=timezone)
2002 if (date_rng.from_value):
2003 where.append('_%s >= %s'%(k, a))
2004 args.append(date_rng.from_value.serialise())
2005 if (date_rng.to_value):
2006 where.append('_%s <= %s'%(k, a))
2007 args.append(date_rng.to_value.serialise())
2008 except ValueError:
2009 # If range creation fails - ignore that search parameter
2010 pass
2011 elif isinstance(propclass, Interval):
2012 if isinstance(v, type([])):
2013 s = ','.join([a for x in v])
2014 where.append('_%s in (%s)'%(k, s))
2015 args = args + [date.Interval(x).serialise() for x in v]
2016 else:
2017 try:
2018 # Try to filter on range of intervals
2019 date_rng = Range(v, date.Interval)
2020 if (date_rng.from_value):
2021 where.append('_%s >= %s'%(k, a))
2022 args.append(date_rng.from_value.serialise())
2023 if (date_rng.to_value):
2024 where.append('_%s <= %s'%(k, a))
2025 args.append(date_rng.to_value.serialise())
2026 except ValueError:
2027 # If range creation fails - ignore that search parameter
2028 pass
2029 #where.append('_%s=%s'%(k, a))
2030 #args.append(date.Interval(v).serialise())
2031 else:
2032 if isinstance(v, type([])):
2033 s = ','.join([a for x in v])
2034 where.append('_%s in (%s)'%(k, s))
2035 args = args + v
2036 else:
2037 where.append('_%s=%s'%(k, a))
2038 args.append(v)
2040 # don't match retired nodes
2041 where.append('__retired__ <> 1')
2043 # add results of full text search
2044 if search_matches is not None:
2045 v = search_matches.keys()
2046 s = ','.join([a for x in v])
2047 where.append('id in (%s)'%s)
2048 args = args + v
2050 # "grouping" is just the first-order sorting in the SQL fetch
2051 # can modify it...)
2052 orderby = []
2053 ordercols = []
2054 if group[0] is not None and group[1] is not None:
2055 if group[0] != '-':
2056 orderby.append('_'+group[1])
2057 ordercols.append('_'+group[1])
2058 else:
2059 orderby.append('_'+group[1]+' desc')
2060 ordercols.append('_'+group[1])
2062 # now add in the sorting
2063 group = ''
2064 if sort[0] is not None and sort[1] is not None:
2065 direction, colname = sort
2066 if direction != '-':
2067 if colname == 'id':
2068 orderby.append(colname)
2069 else:
2070 orderby.append('_'+colname)
2071 ordercols.append('_'+colname)
2072 else:
2073 if colname == 'id':
2074 orderby.append(colname+' desc')
2075 ordercols.append(colname)
2076 else:
2077 orderby.append('_'+colname+' desc')
2078 ordercols.append('_'+colname)
2080 # construct the SQL
2081 frum = ','.join(frum)
2082 if where:
2083 where = ' where ' + (' and '.join(where))
2084 else:
2085 where = ''
2086 cols = ['id']
2087 if orderby:
2088 cols = cols + ordercols
2089 order = ' order by %s'%(','.join(orderby))
2090 else:
2091 order = ''
2092 cols = ','.join(cols)
2093 sql = 'select %s from %s %s%s%s'%(cols, frum, where, group, order)
2094 args = tuple(args)
2095 if __debug__:
2096 print >>hyperdb.DEBUG, 'filter', (self, sql, args)
2097 if args:
2098 self.db.cursor.execute(sql, args)
2099 else:
2100 # psycopg doesn't like empty args
2101 self.db.cursor.execute(sql)
2102 l = self.db.sql_fetchall()
2104 # return the IDs (the first column)
2105 return [row[0] for row in l]
2107 def count(self):
2108 '''Get the number of nodes in this class.
2110 If the returned integer is 'numnodes', the ids of all the nodes
2111 in this class run from 1 to numnodes, and numnodes+1 will be the
2112 id of the next node to be created in this class.
2113 '''
2114 return self.db.countnodes(self.classname)
2116 # Manipulating properties:
2117 def getprops(self, protected=1):
2118 '''Return a dictionary mapping property names to property objects.
2119 If the "protected" flag is true, we include protected properties -
2120 those which may not be modified.
2121 '''
2122 d = self.properties.copy()
2123 if protected:
2124 d['id'] = String()
2125 d['creation'] = hyperdb.Date()
2126 d['activity'] = hyperdb.Date()
2127 d['creator'] = hyperdb.Link('user')
2128 return d
2130 def addprop(self, **properties):
2131 '''Add properties to this class.
2133 The keyword arguments in 'properties' must map names to property
2134 objects, or a TypeError is raised. None of the keys in 'properties'
2135 may collide with the names of existing properties, or a ValueError
2136 is raised before any properties have been added.
2137 '''
2138 for key in properties.keys():
2139 if self.properties.has_key(key):
2140 raise ValueError, key
2141 self.properties.update(properties)
2143 def index(self, nodeid):
2144 '''Add (or refresh) the node to search indexes
2145 '''
2146 # find all the String properties that have indexme
2147 for prop, propclass in self.getprops().items():
2148 if isinstance(propclass, String) and propclass.indexme:
2149 try:
2150 value = str(self.get(nodeid, prop))
2151 except IndexError:
2152 # node no longer exists - entry should be removed
2153 self.db.indexer.purge_entry((self.classname, nodeid, prop))
2154 else:
2155 # and index them under (classname, nodeid, property)
2156 self.db.indexer.add_text((self.classname, nodeid, prop),
2157 value)
2160 #
2161 # Detector interface
2162 #
2163 def audit(self, event, detector):
2164 '''Register a detector
2165 '''
2166 l = self.auditors[event]
2167 if detector not in l:
2168 self.auditors[event].append(detector)
2170 def fireAuditors(self, action, nodeid, newvalues):
2171 '''Fire all registered auditors.
2172 '''
2173 for audit in self.auditors[action]:
2174 audit(self.db, self, nodeid, newvalues)
2176 def react(self, event, detector):
2177 '''Register a detector
2178 '''
2179 l = self.reactors[event]
2180 if detector not in l:
2181 self.reactors[event].append(detector)
2183 def fireReactors(self, action, nodeid, oldvalues):
2184 '''Fire all registered reactors.
2185 '''
2186 for react in self.reactors[action]:
2187 react(self.db, self, nodeid, oldvalues)
2189 class FileClass(Class, hyperdb.FileClass):
2190 '''This class defines a large chunk of data. To support this, it has a
2191 mandatory String property "content" which is typically saved off
2192 externally to the hyperdb.
2194 The default MIME type of this data is defined by the
2195 "default_mime_type" class attribute, which may be overridden by each
2196 node if the class defines a "type" String property.
2197 '''
2198 default_mime_type = 'text/plain'
2200 def create(self, **propvalues):
2201 ''' snaffle the file propvalue and store in a file
2202 '''
2203 # we need to fire the auditors now, or the content property won't
2204 # be in propvalues for the auditors to play with
2205 self.fireAuditors('create', None, propvalues)
2207 # now remove the content property so it's not stored in the db
2208 content = propvalues['content']
2209 del propvalues['content']
2211 # do the database create
2212 newid = Class.create_inner(self, **propvalues)
2214 # fire reactors
2215 self.fireReactors('create', newid, None)
2217 # store off the content as a file
2218 self.db.storefile(self.classname, newid, None, content)
2219 return newid
2221 def import_list(self, propnames, proplist):
2222 ''' Trap the "content" property...
2223 '''
2224 # dupe this list so we don't affect others
2225 propnames = propnames[:]
2227 # extract the "content" property from the proplist
2228 i = propnames.index('content')
2229 content = eval(proplist[i])
2230 del propnames[i]
2231 del proplist[i]
2233 # do the normal import
2234 newid = Class.import_list(self, propnames, proplist)
2236 # save off the "content" file
2237 self.db.storefile(self.classname, newid, None, content)
2238 return newid
2240 _marker = []
2241 def get(self, nodeid, propname, default=_marker, cache=1):
2242 ''' Trap the content propname and get it from the file
2244 'cache' exists for backwards compatibility, and is not used.
2245 '''
2246 poss_msg = 'Possibly a access right configuration problem.'
2247 if propname == 'content':
2248 try:
2249 return self.db.getfile(self.classname, nodeid, None)
2250 except IOError, (strerror):
2251 # BUG: by catching this we donot see an error in the log.
2252 return 'ERROR reading file: %s%s\n%s\n%s'%(
2253 self.classname, nodeid, poss_msg, strerror)
2254 if default is not self._marker:
2255 return Class.get(self, nodeid, propname, default)
2256 else:
2257 return Class.get(self, nodeid, propname)
2259 def getprops(self, protected=1):
2260 ''' In addition to the actual properties on the node, these methods
2261 provide the "content" property. If the "protected" flag is true,
2262 we include protected properties - those which may not be
2263 modified.
2264 '''
2265 d = Class.getprops(self, protected=protected).copy()
2266 d['content'] = hyperdb.String()
2267 return d
2269 def index(self, nodeid):
2270 ''' Index the node in the search index.
2272 We want to index the content in addition to the normal String
2273 property indexing.
2274 '''
2275 # perform normal indexing
2276 Class.index(self, nodeid)
2278 # get the content to index
2279 content = self.get(nodeid, 'content')
2281 # figure the mime type
2282 if self.properties.has_key('type'):
2283 mime_type = self.get(nodeid, 'type')
2284 else:
2285 mime_type = self.default_mime_type
2287 # and index!
2288 self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
2289 mime_type)
2291 # XXX deviation from spec - was called ItemClass
2292 class IssueClass(Class, roundupdb.IssueClass):
2293 # Overridden methods:
2294 def __init__(self, db, classname, **properties):
2295 '''The newly-created class automatically includes the "messages",
2296 "files", "nosy", and "superseder" properties. If the 'properties'
2297 dictionary attempts to specify any of these properties or a
2298 "creation" or "activity" property, a ValueError is raised.
2299 '''
2300 if not properties.has_key('title'):
2301 properties['title'] = hyperdb.String(indexme='yes')
2302 if not properties.has_key('messages'):
2303 properties['messages'] = hyperdb.Multilink("msg")
2304 if not properties.has_key('files'):
2305 properties['files'] = hyperdb.Multilink("file")
2306 if not properties.has_key('nosy'):
2307 # note: journalling is turned off as it really just wastes
2308 # space. this behaviour may be overridden in an instance
2309 properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
2310 if not properties.has_key('superseder'):
2311 properties['superseder'] = hyperdb.Multilink(classname)
2312 Class.__init__(self, db, classname, **properties)