import pg import re def quote(s): return re.sub("'", r"\'", s) class QueryResult: def __init__(self, result): self.__result = result self.__tuple = None def getresult(self): if self.__tuple is None: self.__tuple = self.__result.getresult() return self.__tuple def __len__(self): return len(self.getresult()) def __getattr__(self, attr): return getattr(self.__result, attr) class SQLMapping: """Decode a simple mapping from an SQL table An interface for a simple SQL table of the following sort: CREATE TABLE bug_group_t ( bug_group_id int PRIMARY KEY, name text NOT NULL ); The chief requirements are that the types of the two fields match the example above. Assumes that the keys and values are disjoint, so that a single interface can resolve in either direction. """ def __init__(self, db, table, fields="*"): self.dict1 = {} self.dict2 = {} self.__db = db self.__table = table r = db.query("SELECT %s FROM %s" % (fields, table)).getresult() for key, val in r: assert None not in (key, val) self.dict1[key] = val self.dict2[val] = key def lookup(self, kv): r = self.dict1.get(kv) if r is None: r = self.dict2.get(kv) return r def get_dict(self): """Return dict mapping key to value""" return self.dict2 __insert_q = "INSERT INTO %(table)s VALUES (%(key)s, '%(value)s')" def insert(self, key, value): table = self.__table value = quote(value) query = self.__insert_q % locals() self.__db.query(query) self.dict1[key] = value self.dict2[value] = key class PatchDBInterface: """Interface between the PatchManager and the SQL database Scheme for the patches table is: CREATE TABLE patches_t ( patch_id int PRIMARY KEY, summary text, status int REFERENCES status_t, category text, date text, submitted_by int REFERENCES users_t, assigned_to int REFERENCES users_t, summary_url text ); """ def __init__(self, db=None): db = db or pg.connect() self.db = db self.users = SQLMapping(db, 'users_t') self.status = SQLMapping(db, 'status_t') self.num_deletes = 0 def update(self, patch_id, attrs): # resolve REFERENCES status = self.status.lookup(attrs['Status']) submitted_by = self.users.lookup(attrs['Submitted By']) if submitted_by is None: submitted_by = 0 assigned_to = self.users.lookup(attrs['Assigned To']) if assigned_to is None: assigned_to = 100 # delete old version if necessary if self.has_patch(patch_id): q = "DELETE FROM patches_t WHERE patch_id = %(patch_id)d" self.db.query(q % locals()) self.num_deletes = self.num_deletes + 1 d = locals() del d['attrs'] # just to make debugging prints clearer for k, v in attrs.items(): d[k] = pg._quote(v, 0) q = "INSERT INTO patches_t VALUES (%(patch_id)d," \ " %(Summary)s, %(status)d, %(Category)s, %(Date)s," \ " %(submitted_by)d, %(assigned_to)d)" self.db.query(q % d) def has_patch(self, patch_id): r = self.db.query("SELECT * FROM patches_t" \ " WHERE patch_id = %d" % patch_id).getresult() if r: return 1 else: return 0 def query(self, query): return self.db.query(query) class Bug: """Interface to bug_t row""" def __init__(self, bug_info, db): self.__db = db self.__dict = bug_info self.__clean = 1 # Keep this one around solely for the benefit of the SF Web # interface; the local database ignores it. self.__details = None def get(self, attr): """Get an attribute of the bug This method understands a few different kinds of keys: names of bug_t columns, e.g. summary, status_id, etc. names of bug_t references, e.g. status returns the value referred to by status_id """ if self.__dict.has_key(attr): return self.__dict[attr] ref = attr + "_id" if self.__dict.has_key(ref): return self.__get_ref(attr, ref) if attr == "details": return self.__details raise KeyError, "no attribute: %s" % attr def __get_mapping(self, table): try: mapping = getattr(self.__db, table) except AttributeError: raise KeyError, "no table for attribute: %s" % table return mapping def __get_ref(self, table, id): mapping = self.__get_mapping(table) return mapping.lookup(self.__dict[id]) def set(self, attr, value): if attr == 'details': self.__details = value return if self.__dict.has_key(attr): self.__clean = 0 self.__dict[attr] = value return ref = attr + "_id" if self.__dict.has_key(ref): self.__set_ref(attr, ref, value) return raise KeyError, "no attribute: %s" % attr def __set_ref(self, table, id, value): mapping = self.__get_mapping(table) key = mapping.lookup(value) if key is None: raise ValueError, "invalid attribute for table %s: %s" % \ (table, value) # __dict holds keys to values in the mapping self.__clean = 0 self.__dict[id] = key def assign(self, username): # the set interface does not work well here self.__set_ref("users", "assigned_to", username) _update_ids = ('bug_group_id', 'status_id', 'category_id', 'resolution_id') def get_update_data(self): """Return data in the format expected by db update method""" d = {} for attr in Bug._update_ids: attr_value = attr[:-3] key = self.get(attr_value) if key is not None: d[attr] = key, self.get(attr) d['priority'] = self.get('priority'), self.get('priority') user = self.__get_ref('users', 'assigned_to') if user is not None: d['assigned_to'] = user, self.get('assigned_to') for attr, (k, v) in d.items(): d[attr] = k, str(v) return self.__dict['bug_id'], self.__dict['summary'], d _form_keys = ('bug_id', 'summary', 'category_id', 'priority', 'bug_group_id', 'resolution_id', 'assigned_to', 'status_id') def get_form_data(self): d = {} for name in Bug._form_keys: val = self.get(name) if val is None: val = 100 s = str(val) d[name] = s if self.__details is not None: d['details'] = self.__details return d def flush_sf(self): # XXX not sure what to do here... pass def flush_local(self): args = self.get_update_data() self.__db.update(*args) def flush(self): if not self.__clean: self.flush_sf() self.flush_local() self.__clean = 1 class BugDBInterface: """Interface to bug_t""" def __init__(self, db=None): db = db or pg.connect() self.__db = db self.bug_group = SQLMapping(db, "bug_group_t") self.category = SQLMapping(db, "category_t") self.status = SQLMapping(db, "status_t") self.users = SQLMapping(db, "users_t") self.resolution = SQLMapping(db, "resolution_t") def __query(self, query): print query return QueryResult(self.__db.query(query)) def get(self, bug_id): r = self.__query('select * from bug_t where bug_id = %s' % bug_id) if len(r) == 0: return None assert len(r) == 1 return Bug(r.dictresult()[0], self) def insert(self, bug_id, summary, attrs): """Load bug using the info from the buglig BugParser""" self._do_bug_query(bug_id, summary, attrs, self.__insert_q) def update(self, bug_id, summary, attrs): """Update existing bug using info from buglib BugParser""" self.__query("BEGIN WORK") sql = 'DELETE FROM bug_t WHERE bug_id = %(bug_id)s' % locals() self.__query(sql) self.insert(bug_id, summary, attrs) self.__query("COMMIT WORK") __insert_q = "INSERT INTO bug_t (bug_id, summary, " \ "%(optional_keys)s, priority) VALUES " \ "(%(bug_id)s, '%(summary)s', %(optional_values)s, " \ "%(priority)s)" def _do_bug_query(self, bug_id, summary, attrs, query): summary = quote(summary) priority = int(attrs["priority"][1]) optional_keys, optional_values = self._prep_query_refs(attrs) sql = self.__insert_q % locals() self.__query(sql) def _prep_query_refs(self, attrs): self._new_query() self._lookup_ref(self.bug_group, attrs, "bug_group_id") self._lookup_ref(self.category, attrs, "category_id") self._lookup_ref(self.resolution, attrs, "resolution_id") self._lookup_ref(self.status, attrs, "status_id") self._lookup_ref(self.users, attrs, "assigned_to") # now figure out which of the optional fields have values (yuck) for k, v in self.refs.items(): if v is None: del self.refs[k] optional_keys = ", ".join(self.refs.keys()) optional_values = ", ".join(map(str, self.refs.values())) return optional_keys, optional_values __exists_q = "SELECT * FROM bug_t WHERE bug_id = %(bug_id)s" def exists(self, bug_id): if self.__query(self.__exists_q % locals()): return 1 else: return 0 def _new_query(self): self.refs = {} def _lookup_ref(self, table, attrs, attrname): pair = attrs.get(attrname) if pair is None: self.refs[attrname] = None return None value, key = pair id = table.lookup(value) if id is None: id = int(key) table.insert(id, value) self.refs[attrname] = id return id