Code

Finished implementation of session and one-time-key stores for RDBMS
[roundup.git] / roundup / backends / rdbms_common.py
index e7d141c64c516dbfb1b49cb5c05b374140bdbf57..2894cc63e35a8374b17ed070bd0fd0f6955353a9 100644 (file)
@@ -1,4 +1,4 @@
-# $Id: rdbms_common.py,v 1.75 2004-02-11 23:55:09 richard Exp $
+# $Id: rdbms_common.py,v 1.81 2004-03-18 01:58:45 richard Exp $
 ''' Relational database (SQL) backend common code.
 
 Basics:
@@ -19,6 +19,12 @@ sql_* methods, since everything else should be fairly generic. There's
 probably a bit of work to be done if a database is used that actually
 honors column typing, since the initial databases don't (sqlite stores
 everything as a string.)
+
+The schema of the hyperdb being mapped to the database is stored in the
+database itself as a repr()'ed dictionary of information about each Class
+that maps to a table. If that information differs from the hyperdb schema,
+then we update it. We also store in the schema dict a version which
+allows us to upgrade the database schema when necessary. See upgrade_db().
 '''
 __docformat__ = 'restructuredtext'
 
@@ -34,7 +40,7 @@ from roundup.backends import locking
 # support
 from blobfiles import FileStorage
 from roundup.indexer import Indexer
-from sessions import Sessions, OneTimeKeys
+from sessions_rdbms import Sessions, OneTimeKeys
 from roundup.date import Range
 
 # number of rows to keep in memory
@@ -54,8 +60,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         self.dir = config.DATABASE
         self.classes = {}
         self.indexer = Indexer(self.dir)
-        self.sessions = Sessions(self.config)
-        self.otks = OneTimeKeys(self.config)
         self.security = security.Security(self)
 
         # additional transaction support for external files and the like
@@ -70,14 +74,22 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         self.lockfile = None
 
         # open a connection to the database, creating the "conn" attribute
-        self.sql_open_connection()
+        self.open_connection()
 
     def clearCache(self):
         self.cache = {}
         self.cache_lru = []
 
-    def sql_open_connection(self):
-        ''' Open a connection to the database, creating it if necessary
+    def getSessionManager(self):
+        return Sessions(self)
+
+    def getOTKManager(self):
+        return OneTimeKeys(self)
+
+    def open_connection(self):
+        ''' Open a connection to the database, creating it if necessary.
+
+            Must call self.load_dbschema()
         '''
         raise NotImplemented
 
@@ -106,17 +118,27 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         '''
         return re.sub("'", "''", str(value))
 
-    def save_dbschema(self, schema):
-        ''' Save the schema definition that the database currently implements
-        '''
-        s = repr(self.database_schema)
-        self.sql('insert into schema values (%s)', (s,))
+    def init_dbschema(self):
+        self.database_schema = {
+            'version': self.current_db_version,
+            'tables': {}
+        }
 
     def load_dbschema(self):
         ''' Load the schema definition that the database currently implements
         '''
         self.cursor.execute('select schema from schema')
-        return eval(self.cursor.fetchone()[0])
+        schema = self.cursor.fetchone()
+        if schema:
+            self.database_schema = eval(schema[0])
+        else:
+            self.database_schema = {}
+
+    def save_dbschema(self, schema):
+        ''' Save the schema definition that the database currently implements
+        '''
+        s = repr(self.database_schema)
+        self.sql('insert into schema values (%s)', (s,))
 
     def post_init(self):
         ''' Called once the schema initialisation has finished.
@@ -124,23 +146,25 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             We should now confirm that the schema defined by our "classes"
             attribute actually matches the schema in the database.
         '''
+        save = self.upgrade_db()
+
         # now detect changes in the schema
-        save = 0
+        tables = self.database_schema['tables']
         for classname, spec in self.classes.items():
-            if self.database_schema.has_key(classname):
-                dbspec = self.database_schema[classname]
+            if tables.has_key(classname):
+                dbspec = tables[classname]
                 if self.update_class(spec, dbspec):
-                    self.database_schema[classname] = spec.schema()
+                    tables[classname] = spec.schema()
                     save = 1
             else:
                 self.create_class(spec)
-                self.database_schema[classname] = spec.schema()
+                tables[classname] = spec.schema()
                 save = 1
 
-        for classname, spec in self.database_schema.items():
+        for classname, spec in tables.items():
             if not self.classes.has_key(classname):
-                self.drop_class(classname, spec)
-                del self.database_schema[classname]
+                self.drop_class(classname, tables[classname])
+                del tables[classname]
                 save = 1
 
         # update the database version of the schema
@@ -155,6 +179,30 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         # commit
         self.conn.commit()
 
+    # update this number when we need to make changes to the SQL structure
+    # of the backen database
+    current_db_version = 2
+    def upgrade_db(self):
+        ''' Update the SQL database to reflect changes in the backend code.
+
+            Return boolean whether we need to save the schema.
+        '''
+        version = self.database_schema.get('version', 1)
+        if version == self.current_db_version:
+            # nothing to do
+            return 0
+
+        if version == 1:
+            # version 1 doesn't have the OTK, session and indexing in the
+            # database
+            self.create_version_2_tables()
+            # version 1 also didn't have the actor column
+            self.add_actor_column()
+
+        self.database_schema['version'] = self.current_db_version
+        return 1
+
+
     def refresh_database(self):
         self.post_init()
 
@@ -170,7 +218,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             "properties" is a list of (name, prop) where prop may be an
             instance of a hyperdb "type" _or_ a string repr of that type.
         '''
-        cols = ['_activity', '_creator', '_creation']
+        cols = ['_actor', '_activity', '_creator', '_creation']
         mls = []
         # add the multilinks separately
         for col, prop in properties:
@@ -200,62 +248,64 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         if __debug__:
             print >>hyperdb.DEBUG, 'update_class FIRING'
 
+        # detect key prop change for potential index change
+        keyprop_changes = 0
+        if new_spec[0] != old_spec[0]:
+            keyprop_changes = {'remove': old_spec[0], 'add': new_spec[0]}
+
         # detect multilinks that have been removed, and drop their table
         old_has = {}
-        for name,prop in old_spec[1]:
+        for name, prop in old_spec[1]:
             old_has[name] = 1
-            if new_has(name) or not isinstance(prop, Multilink):
+            if new_has(name):
                 continue
-            # it's a multilink, and it's been removed - drop the old
-            # table. First drop indexes.
-            self.drop_multilink_table_indexes(spec.classname, ml)
-            sql = 'drop table %s_%s'%(spec.classname, prop)
+
+            if isinstance(prop, Multilink):
+                # first drop indexes.
+                self.drop_multilink_table_indexes(spec.classname, ml)
+
+                # now the multilink table itself
+                sql = 'drop table %s_%s'%(spec.classname, prop)
+            else:
+                # if this is the key prop, drop the index first
+                if old_spec[0] == prop:
+                    self.drop_class_table_key_index(spec.classname, prop)
+                    del keyprop_changes['remove']
+
+                # drop the column
+                sql = 'alter table _%s drop column _%s'%(spec.classname, prop)
+
             if __debug__:
                 print >>hyperdb.DEBUG, 'update_class', (self, sql)
             self.cursor.execute(sql)
         old_has = old_has.has_key
 
-        # now figure how we populate the new table
-        fetch = ['_activity', '_creation', '_creator']
-        properties = spec.getprops()
-        for propname,x in new_spec[1]:
-            prop = properties[propname]
-            if isinstance(prop, Multilink):
-                if force or not old_has(propname):
-                    # we need to create the new table
-                    self.create_multilink_table(spec, propname)
-            elif old_has(propname):
-                # we copy this col over from the old table
-                fetch.append('_'+propname)
-
-        # select the data out of the old table
-        fetch.append('id')
-        fetch.append('__retired__')
-        fetchcols = ','.join(fetch)
-        cn = spec.classname
-        sql = 'select %s from _%s'%(fetchcols, cn)
-        if __debug__:
-            print >>hyperdb.DEBUG, 'update_class', (self, sql)
-        self.cursor.execute(sql)
-        olddata = self.cursor.fetchall()
-
-        # TODO: update all the other index dropping code
-        self.drop_class_table_indexes(cn, old_spec[0])
+        # if we didn't remove the key prop just then, but the key prop has
+        # changed, we still need to remove the old index
+        if keyprop_changes.has_key('remove'):
+            self.drop_class_table_key_index(spec.classname,
+                keyprop_changes['remove'])
 
-        # drop the old table
-        self.cursor.execute('drop table _%s'%cn)
+        # add new columns
+        for propname, x in new_spec[1]:
+            if old_has(propname):
+                continue
+            sql = 'alter table _%s add column _%s varchar(255)'%(
+                spec.classname, propname)
+            if __debug__:
+                print >>hyperdb.DEBUG, 'update_class', (self, sql)
+            self.cursor.execute(sql)
 
-        # create the new table
-        self.create_class_table(spec)
+            # if the new column is a key prop, we need an index!
+            if new_spec[0] == propname:
+                self.create_class_table_key_index(spec.classname, propname)
+                del keyprop_changes['add']
 
-        if olddata:
-            # do the insert
-            args = ','.join([self.arg for x in fetch])
-            sql = 'insert into _%s (%s) values (%s)'%(cn, fetchcols, args)
-            if __debug__:
-                print >>hyperdb.DEBUG, 'update_class', (self, sql, olddata[0])
-            for entry in olddata:
-                self.cursor.execute(sql, tuple(entry))
+        # if we didn't add the key prop just then, but the key prop has
+        # changed, we still need to add the new index
+        if keyprop_changes.has_key('add'):
+            self.create_class_table_key_index(spec.classname,
+                keyprop_changes['add'])
 
         return 1
 
@@ -312,10 +362,8 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         # drop the old table indexes first
         l = ['_%s_id_idx'%cn, '_%s_retired_idx'%cn]
         if key:
-            # key prop too?
             l.append('_%s_%s_idx'%(cn, key))
 
-        # TODO: update all the other index dropping code
         table_name = '_%s'%cn
         for index_name in l:
             if not self.sql_index_exists(table_name, index_name):
@@ -325,6 +373,28 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
                 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
             self.cursor.execute(index_sql)
 
+    def create_class_table_key_index(self, cn, key):
+        ''' create the class table for the given spec
+        '''
+        if __debug__:
+            print >>hyperdb.DEBUG, 'update_class setting keyprop %r'% \
+                key
+        index_sql3 = 'create index _%s_%s_idx on _%s(_%s)'%(cn, key,
+            cn, key)
+        if __debug__:
+            print >>hyperdb.DEBUG, 'create_index', (self, index_sql3)
+        self.cursor.execute(index_sql3)
+
+    def drop_class_table_key_index(self, cn, key):
+        table_name = '_%s'%cn
+        index_name = '_%s_%s_idx'%(cn, key)
+        if not self.sql_index_exists(table_name, index_name):
+            return
+        sql = 'drop index '+index_name
+        if __debug__:
+            print >>hyperdb.DEBUG, 'drop_index', (self, sql)
+        self.cursor.execute(sql)
+
     def create_journal_table(self, spec):
         ''' create the journal table for a class given the spec and 
             already-determined cols
@@ -340,11 +410,11 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
 
     def create_journal_table_indexes(self, spec):
         # index on nodeid
-        index_sql = 'create index %s_journ_idx on %s__journal(nodeid)'%(
+        sql = 'create index %s_journ_idx on %s__journal(nodeid)'%(
                         spec.classname, spec.classname)
         if __debug__:
-            print >>hyperdb.DEBUG, 'create_index', (self, index_sql)
-        self.cursor.execute(index_sql)
+            print >>hyperdb.DEBUG, 'create_index', (self, sql)
+        self.cursor.execute(sql)
 
     def drop_journal_table_indexes(self, classname):
         index_name = '%s_journ_idx'%classname
@@ -469,6 +539,12 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             raise ValueError, cn
         self.classes[cn] = cl
 
+        # add default Edit and View permissions
+        self.security.addPermission(name="Edit", klass=cn,
+            description="User is allowed to edit "+cn)
+        self.security.addPermission(name="View", klass=cn,
+            description="User is allowed to access "+cn)
+
     def getclasses(self):
         ''' Return a list of the names of all existing classes.
         '''
@@ -555,7 +631,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             # calling code's node assumptions)
             node = node.copy()
             node['creation'] = node['activity'] = date.Date()
-            node['creator'] = self.getuid()
+            node['actor'] = node['creator'] = self.getuid()
 
         # default the non-multilink columns
         for col, prop in cl.properties.items():
@@ -611,6 +687,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         # add the special props
         values = values.copy()
         values['activity'] = date.Date()
+        values['actor'] = self.getuid()
 
         # make db-friendly
         values = self.serialise(classname, values)
@@ -925,6 +1002,8 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     def sql_commit(self):
         ''' Actually commit to the database.
         '''
+        if __debug__:
+            print >>hyperdb.DEBUG, '+++ commit database connection +++'
         self.conn.commit()
 
     def commit(self):
@@ -986,17 +1065,14 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         return (classname, nodeid)
 
     def sql_close(self):
+        if __debug__:
+            print >>hyperdb.DEBUG, '+++ close database connection +++'
         self.conn.close()
 
     def close(self):
         ''' Close off the connection.
         '''
         self.sql_close()
-        if self.lockfile is not None:
-            locking.release_lock(self.lockfile)
-        if self.lockfile is not None:
-            self.lockfile.close()
-            self.lockfile = None
 
 #
 # The base Class class
@@ -1015,10 +1091,10 @@ class Class(hyperdb.Class):
         or a ValueError is raised.  The keyword arguments in 'properties'
         must map names to property objects, or a TypeError is raised.
         '''
-        if (properties.has_key('creation') or properties.has_key('activity')
-                or properties.has_key('creator')):
-            raise ValueError, '"creation", "activity" and "creator" are '\
-                'reserved'
+        for name in 'creation activity creator actor'.split():
+            if properties.has_key(name):
+                raise ValueError, '"creation", "activity", "creator" and '\
+                    '"actor" are reserved'
 
         self.classname = classname
         self.properties = properties
@@ -1082,8 +1158,10 @@ class Class(hyperdb.Class):
         if self.db.journaltag is None:
             raise DatabaseError, 'Database open read-only'
 
-        if propvalues.has_key('creation') or propvalues.has_key('activity'):
-            raise KeyError, '"creation" and "activity" are reserved'
+        if propvalues.has_key('creator') or propvalues.has_key('actor') or \
+             propvalues.has_key('creation') or propvalues.has_key('activity'):
+            raise KeyError, '"creator", "actor", "creation" and '\
+                '"activity" are reserved'
 
         # new node's id
         newid = self.db.newid(self.classname)
@@ -1308,6 +1386,8 @@ class Class(hyperdb.Class):
             creation = None
         if d.has_key('activity'):
             del d['activity']
+        if d.has_key('actor'):
+            del d['actor']
         self.db.addjournal(self.classname, newid, 'create', {}, creator,
             creation)
         return newid
@@ -1343,6 +1423,11 @@ class Class(hyperdb.Class):
                 return d['creator']
             else:
                 return self.db.getuid()
+        if propname == 'actor':
+            if d.has_key('actor'):
+                return d['actor']
+            else:
+                return self.db.getuid()
 
         # get the property (raises KeyErorr if invalid)
         prop = self.properties[propname]
@@ -1383,8 +1468,10 @@ class Class(hyperdb.Class):
         if not propvalues:
             return propvalues
 
-        if propvalues.has_key('creation') or propvalues.has_key('activity'):
-            raise KeyError, '"creation" and "activity" are reserved'
+        if propvalues.has_key('creation') or propvalues.has_key('creator') or \
+                propvalues.has_key('actor') or propvalues.has_key('activity'):
+            raise KeyError, '"creation", "creator", "actor" and '\
+                '"activity" are reserved'
 
         if propvalues.has_key('id'):
             raise KeyError, '"id" is reserved'
@@ -2125,6 +2212,7 @@ class Class(hyperdb.Class):
             d['creation'] = hyperdb.Date()
             d['activity'] = hyperdb.Date()
             d['creator'] = hyperdb.Link('user')
+            d['actor'] = hyperdb.Link('user')
         return d
 
     def addprop(self, **properties):
@@ -2295,7 +2383,8 @@ class IssueClass(Class, roundupdb.IssueClass):
         '''The newly-created class automatically includes the "messages",
         "files", "nosy", and "superseder" properties.  If the 'properties'
         dictionary attempts to specify any of these properties or a
-        "creation" or "activity" property, a ValueError is raised.
+        "creation", "creator", "activity" or "actor" property, a ValueError
+        is raised.
         '''
         if not properties.has_key('title'):
             properties['title'] = hyperdb.String(indexme='yes')