# bugzilla.py -- Bugzilla interface. # Nicholas Barnes, Ravenbrook Limited, 2000-11-21. # $Id: //info.ravenbrook.com/project/p4dti/branch/2000-12-01/no-conflicts/code/replicator/bugzilla.py#1 $ # # Copyright 2000 Ravenbrook Limited. This document is provided "as is", # without any express or implied warranty. In no event will the authors # be held liable for any damages arising from the use of this document. # You may make and distribute copies and derivative works of this # document provided that (1) you do not charge a fee for this document or # for its distribution, and (2) you retain as they appear all copyright # and licence notices and document history entries, and (3) you append # descriptions of your modifications to the document history. import types error = 'P4DTI TeamTrack interface error' class bugzilla: config = {} db = None cursor = None rid = None sid = None replication = None def __init__(self, config, rid, sid): # Merge the supplied config with the default config (the former takes # precedence). for k in config.keys(): self.config[k] = config[k] self.db = self.config['db'](host=self.config['dbms-host'], port=self.config['dbms-port'], db=self.config['dbms-database'], user=self.config['dbms-user'], passwd=self.config['dbms-password']) self.cursor = self.db.cursor() self.rid = rid self.sid = sid def execute(self, sql): print ("SQL execute '%s'" % sql), rows = self.cursor.execute(sql) print "->", rows return rows def fetchone(self): row = self.cursor.fetchone() print "fetchone() ->", row return row def fetchall(self): rows = self.cursor.fetchall() print "fetchall() ->", rows return rows ### Tables. # is this table present? def table_present(self, tablename): rows = self.execute("show tables from bugs like %s;" % self.sqlquote(tablename)) return (rows == 1) # create the new tables that we need def create_p4dti_tables(self): if (not self.table_present('p4dti_bugs')) : self.execute("create table p4dti_bugs " " ( bug_id mediumint not null primary key, " " rid varchar(32) not null, " " sid varchar(32) not null, " " jobname text not null, " " replication datetime not null, " " action text not null " " );") if (not self.table_present('p4dti_changelists')) : self.execute("create table p4dti_changelists " " ( changelist int not null, " " rid varchar(32) not null, " " sid varchar(32) not null, " " user mediumint not null, " " flags int not null, " " description longtext not null, " " client text not null, " " p4date text not null, " " replication datetime not null, " " unique (changelist, rid, sid) " " );") if (not self.table_present('p4dti_fixes')) : self.execute("create table p4dti_fixes " " ( changelist int not null, " " bug_id mediumint not null, " " rid varchar(32) not null, " " sid varchar(32) not null, " " user mediumint not null, " " client text not null, " " status text not null, " " p4date text not null, " " replication datetime not null " " );") if (not self.table_present('p4dti_filespecs')) : self.execute("create table p4dti_filespecs " " ( bug_id mediumint not null, " " rid varchar(32) not null, " " sid varchar(32) not null, " " replication datetime not null, " " filespec longtext not null " " );") if (not self.table_present('p4dti_replications')) : self.execute("create table p4dti_replications " " ( id int auto_increment not null " " primary key," # for uniqueness " rid varchar(32) not null, " " sid varchar(32) not null, " " start datetime not null, " " end datetime not null " " );") # drop all our tables def drop_p4dti_tables(self): if (self.table_present('p4dti_bugs')) : self.execute("drop table p4dti_bugs;") if (self.table_present('p4dti_changelists')) : self.execute("drop table p4dti_changelists;") if (self.table_present('p4dti_fixes')) : self.execute("drop table p4dti_fixes;") if (self.table_present('p4dti_filespecs')) : self.execute("drop table p4dti_filespecs;") if (self.table_present('p4dti_replications')) : self.execute("drop table p4dti_replications;") ### Quotation. # Given a string, return an SQL representation # (e.g. "foo" -> "'foo'", "O'Malley" -> "'O\\'Malley'") def sqlquote(self, value): if (type(value) != types.StringType): raise error, ("sqlquote given non-string %s." % str(value)) return "'" + self.db.escape_string(value) + "'" # A version of str which takes self. def self_str(self,v) : return str(v) # A table of functions to translate from # Python values into SQL textual # representations. We don't have to # go the other way because SQL gives us Python # values. We have a default of sqlquote, # which handles string values. Any other # column which we might update has to # go in this table. quote_table = { 'bugs' : { 'assigned_to' : self_str, }, 'p4dti_bugs' : { 'bug_id' : self_str }, 'p4dti_changelists' : { 'changelist' : self_str, 'user' : self_str, 'flags' : self_str }, 'p4dti_fixes' : { 'changelist' : self_str, 'bug_id' : self_str, 'user' : self_str, }, 'p4dti_filespecs' : { 'bug_id' : self_str, }, 'p4dti_replications' : { 'id' : self_str, } } # quote a single value def quote(self, table, field, value) : if self.quote_table[table].has_key(field): return self.quote_table[table][field](self, value) else: return self.sqlquote(value) # quote all values in a dictionary def quote_dict(self, table, dict) : new_dict = {} for key in dict.keys() : new_dict[key] = self.quote(table, key, dict[key]) return new_dict ### Basic selects. # select a row and do error-checking. def select_one_row(self, select, description): rows = self.execute(select) if rows == 0 : raise error, ("select '%s' of %s returns no rows." % (select, description)) if rows > 1 : raise error, ("select '%s' of %s returns %d rows." % (select, description, rows)) if self.cursor.description == None : raise error, ("trying to fetch a row from non-select '%s'" % select) # get values values = self.fetchone() if values == None : raise error, ("Database error: select '%s'" " returned an unfetchable row." % select) return values # select many rows and do some error-checking. def select_rows(self, select, description): rows = self.execute(select) if self.cursor.description == None : raise error, ("trying to fetch rows from non-select '%s'" % select) # get values values = self.fetchall() if values == None : raise error, ("Database error: select '%s'" " returned unfetchable rows." % select) return values def select_at_most_one_row(self, select, description): rows = self.select_rows(select, description) if len(rows) == 0 : return None elif len(rows) == 1 : return rows[0] else : raise error, ("select '%s' of %s returns %d rows." % (select, description, rows)) def column_names(self): keys = [] for i in range(len(self.cursor.description)): keys.append(self.cursor.description[i][0]) return keys # select a row and return the result as a dictionary (column -> value) def fetch_one_row_as_dictionary(self, select, description): dict = {} values = self.select_one_row(select, description) keys = self.column_names() if len(keys) != len(values) : raise error, ("Database error: select '%s'" " returns %d keys but %d values." % (select, len(keys), len(values))) for i in range(len(keys)): dict[keys[i]] = values[i] return dict def fetch_at_most_one_row_as_dictionary(self, select, description): dict = {} values = self.select_at_most_one_row(select, description) if values == None : return None keys = self.column_names() if len(keys) != len(values) : raise error, ("Database error: select '%s'" " returns %d keys but %d values." % (select, len(keys), len(values))) for i in range(len(keys)): dict[keys[i]] = values[i] return dict # select a row and return the result as a list. def fetch_one_row_as_list(self, select, description): return self.select_one_row(select, description) # select many rows and return the result as a list of dictionaries def fetch_rows_as_list_of_dictionaries(self, select, description): results = [] values = self.select_rows(select, description) keys = self.column_names() for value in values: result={} if len(keys) != len(value) : raise error, ("Database error: select '%s'" " returns %d keys but %d columns." % (select, len(keys), len(value))) for j in range(len(keys)): result[keys[j]] = value[j] results.append(result) return results # select many rows and return the result as a list of lists def fetch_rows_as_list_of_lists(self, select, description): return self.select_rows(select, description) ### Other basic operations. # Insert a row. def insert_row(self, table, dict): columns = "" values = "" for key in dict.keys(): value = self.quote(table, key, dict[key]) columns = columns + key + ", " values = values + value + ', ' columns = columns[:-2] values = values[:-2] command = "insert %s ( %s ) values ( %s );" % (table, columns, values) rows = self.execute(command) if rows != 1 : raise error, ("Couldn't insert row in %s" % table) # Insert a row in a table with rid and sid columns. def insert_row_rid_sid(self, table, dict): dict['rid'] = self.rid dict['sid'] = self.sid self.insert_row(table, dict) # Update a row. def update_row(self, table, dict, where): updates = "" for key in dict.keys(): value = self.quote(table, key, dict[key]) updates = updates + key + ' = ' + value + ', ' updates = updates[:-2] command = "update %s set %s where %s;" % (table, updates, where) rows = self.execute(command) if rows != 1 : raise error, ("Couldn't update row in %s where %s" % (table, where)) # Update a row in a table with rid and sid columns def update_row_rid_sid(self, table, dict, where): self.update_row(table, dict, where + (' and rid = %s and sid = %s' % (self.sqlquote(self.rid), self.sqlquote(self.sid)))) # Delete rows def delete_rows(self, table, where): self.execute('delete from %s where %s;' % (table, where)) # Delete rows in a table with rid and sid columns def delete_rows_rid_sid(self, table, dict, where): self.delete_rows(table, where + (' and rid = %s and sid = %s' % (self.sqlquote(self.rid), self.sqlquote(self.sid)))) ### Particular operations which we use. # Note that update operations take a key argument. This means we can # restrict our update to a part-record by passing a small dictionary. def add_p4dti_bug(self, dict): dict['replication'] = self.replication self.insert_row_rid_sid('p4dti_bugs', dict) def update_p4dti_bug(self, dict, bug_id): dict['replication'] = self.replication self.update_row_rid_sid('p4dti_bugs', dict, 'bug_id = %d' % bug_id) def update_bug(self, dict, bug_id): self.update_row('bugs', dict, 'bug_id = %d' % bug_id) def add_changelist(self, dict): dict['replication'] = self.replication self.insert_row_rid_sid('p4dti_changelists', dict) def update_changelist(self, dict, number): dict['replication'] = self.replication self.update_row_rid_sid('p4dti_changelists', dict, 'changelist = %d' % number) def add_fix(self, fix): fix['replication'] = self.replication self.insert_row_rid_sid('p4dti_fixes', fix) def delete_fix(self, fix): self.delete_rows_rid_sid('p4dti_fixes', ('where bug_id = %d and changelist = %d ' % (fix['bug_id'], fix['changelist']))) def update_fix(self, fix, bug_id, number): fix['replication'] = self.replication self.update_row_rid_sid('p4dti_fixes', fix, ('bug_id = %d and changelist = %d ' % (bug_id, changelist))) def bug_p4dti_bug(self, bug): bug_id = bug['bug_id'] p4dti_bug = self.fetch_at_most_one_row_as_dictionary( 'select * from p4dti_bugs where bug_id = %d' % bug_id, 'p4dti_bug %d' % bug_id) return p4dti_bug def all_bugs(self): unmatched = self.fetch_rows_as_list_of_dictionaries( "select bugs.* from bugs" " left join p4dti_bugs on bugs.bug_id = p4dti_bugs.bug_id" " where p4dti_bugs.bug_id is null;", "non-matched bugs") matched = self.fetch_rows_as_list_of_dictionaries( ("select bugs.* from bugs, p4dti_bugs" " where bugs.bug_id = p4dti_bugs.bug_id " " and p4dti_bugs.rid = %s " " and p4dti_bugs.sid = %s " % (self.sqlquote(self.rid), self.sqlquote(self.sid))), "matched bugs") return unmatched + matched def all_bugs_since(self, replication): return self.fetch_rows_as_list_of_dictionaries( ("select * from bugs where delta_ts >= %s;" % self.sqlquote(replication)), "recent bugs") # some functions which get and return single rows as # dictionaries def bug_from_bug_id(self, bug_id): return self.fetch_one_row_as_dictionary( "select * from bugs where bug_id = %d;" % bug_id, "bug id %d" % bug_id) def fixes_from_bug_id(self, bug_id): return self.fetch_rows_as_list_of_dictionaries ( "select * from p4dti_fixes where bug_id = %d;" % bug_id, "fixes for bug %d" % bug_id) def user_id_and_email_list(self): return self.fetch_rows_as_list_of_lists ( "select userid, login_name from profiles;", "all user ids and emails") def changelists(self, number): return self.fetch_rows_as_list_of_dictionaries( "select * from p4dti_changelists " " where changelist = %d and " " rid = %s and " " sid = %s;" % (number, self.sqlquote(self.rid), self.sqlquote(self.sid)), "changelist %d" % number) ### Operations on replications (table p4dti_replications) def now(self): self.execute("select now();") value = self.fetchall() return value[0][0] def new_replication(self): now = self.now() self.insert_row_rid_sid('p4dti_replications', { 'start': now, 'end' : '0' } ) self.replication = now return now def end_replication(self): if self.replication != None: now = self.now() self.update_row_rid_sid('p4dti_replications', {'end' : now}, 'start = %s' % self.sqlquote(self.replication)) def delete_complete_replications(self) : self.delete_rows_rid_sid('p4dti_replications', ' end != 0') def incomplete_replications(self) : return self.fetch_rows_as_list_of_dictionaries( ("select * from p4dti_replications where " " rid = %s and " " sid = %s and " " end = 0;" % (self.sqlquote(self.rid), self.sqlquote(self.sid))), ("incomplete replications for (%s, %s)" % (self.rid, self.sid))) def latest_complete_replication(self) : self.execute("select max(start) from p4dti_replications where " " rid = %s and " " sid= %s and " " end != 0;" % (self.sqlquote(self.rid), self.sqlquote(self.sid))) result = self.fetchall()[0][0] if result == None : return '0' else : return result