2 Database Backend Interface
4 This module is intended to be a thin wrapper around CEO database operations.
5 Methods on the connection class correspond in a straightforward way to SQL
6 queries. These methods may restructure and clean up query output but may make
7 no other assumptions about its content or purpose.
9 This module makes use of the PyGreSQL Python bindings to libpq,
10 PostgreSQL's native C client library.
15 class DBException(Exception):
16 """Exception class for database-related errors."""
20 class DBConnection(object):
22 A connection to CEO's backend database. All database queries
23 and updates are made via this class.
25 Exceptions: (all methods)
26 DBException - on database query failure
28 Note: Updates will never take place until commit() is called.
30 Note: In the event that any method of this class raises a
31 DBException and that exception is caught, rollback()
32 must be called before further queries will be successful.
35 connection = DBConnection()
36 connection.connect("localhost", "ceo")
38 # make queries and updates, i.e.
39 connection.insert_member("Calum T. Dalek")
42 connection.disconnect()
50 def connect(self, hostname=None, database=None, username=None, password=None):
52 Establishes the connection to CEO's PostgreSQL database.
55 hostname - hostname:port to connect to
56 database - name of database
57 username - user to authenticate as
58 password - password of username
61 if self.cnx: raise DBException("unable to connect: already connected")
64 self.cnx = pgdb.connect(host=hostname, database=database,
65 user=username, password=password)
66 self.cursor = self.cnx.cursor()
68 raise DBException("unable to connect: %s" % e)
72 """Closes the connection to CEO's PostgreSQL database."""
84 """Determine whether the connection has been established."""
86 return self.cnx is not None
90 """Commits the current transaction and starts a new one."""
96 """Aborts the current transaction."""
102 ### Member-related methods ###
104 def select_members(self, sql, params=None):
106 Retrieves a list CSC members selected by given SQL statement.
108 This is a helper function that should generally not be called directly.
111 sql - the SELECT sql statement
112 params - parameters for the SQL statement
114 The sql statement must select the six columns
115 (memberid, name, studentid, program, type, userid)
116 from the members table in that order.
118 Returns: a memberid-keyed dictionary whose values are
119 column-keyed dictionaries with member attributes
122 # retrieve a list of all members
124 self.cursor.execute(sql, params)
125 members_list = self.cursor.fetchall()
126 except pgdb.Error, e:
127 raise DBException("SELECT statement failed: %s" % e)
129 # build a dictionary of dictionaries from the result (a list of lists)
131 for member in members_list:
132 members_dict[member[0]] = {
133 'memberid': member[0],
135 'studentid': member[2],
136 'program': member[3],
144 def select_single_member(self, sql, params=None):
146 Retrieves a single member by memberid.
148 This is a helper function that should generally not be called directly.
150 See: self.select_members()
152 Returns: a column-keyed dictionary with member attributes, or
153 None if no member matching member exists
156 # retrieve the member
157 results = self.select_members(sql, params)
159 # too many members returned
161 raise DBException("multiple members selected: sql='%s' params=%s" % (sql, repr(params)))
164 elif len(results) < 1:
167 # return the single match
168 memberid = results.keys()[0]
169 return results[memberid]
172 def select_all_members(self):
174 Retrieves a list of all CSC members (past and present).
176 See: self.select_members()
178 Example: connection.select_all_members() -> {
179 0: { 'memberid': 0, 'name': 'Calum T. Dalek' ...}
180 3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
184 sql = "SELECT memberid, name, studentid, program, type, userid FROM members"
185 return self.select_members(sql)
188 def select_members_by_name(self, name_re):
190 Retrieves a list of all CSC members whose name matches name_re.
192 See: self.select_members()
194 Example: connection.select_members_by_name('Michael') -> {
195 3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
199 sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE name ~* %s"
200 params = [ str(name_re) ]
202 return self.select_members(sql, params)
205 def select_members_by_term(self, term):
207 Retrieves a list of all CSC members who were members in the specified term.
209 See: self.select_members()
211 Example: connection.select_members_by_term('f2006') -> {
212 3349: { 'memberid': 3349, 'name': 'Michael Spang' ...}
216 sql = "SELECT members.memberid, name, studentid, program, type, userid FROM members JOIN terms ON members.memberid=terms.memberid WHERE term=%s"
217 params = [ str(term) ]
219 return self.select_members(sql, params)
222 def select_member_by_id(self, memberid):
224 Retrieves a single member by memberid.
226 See: self.select_single_member()
228 Example: connection.select_member_by_id(0) ->
229 { 'memberid': 0, 'name': 'Calum T. Dalek' ...}
231 sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE memberid=%d"
232 params = [ int(memberid) ]
234 return self.select_single_member(sql, params)
237 def select_member_by_userid(self, username):
239 Retrieves a single member by UNIX account username.
241 See: self.select_single_member()
243 Example: connection.select_member_by_userid('ctdalek') ->
244 { 'memberid': 0, 'name': 'Calum T. Dalek' ...}
246 sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE userid=%s"
247 params = [ username ]
249 return self.select_single_member(sql, params)
252 def select_member_by_studentid(self, studentid):
254 Retrieves a single member by student id number.
256 See: self.select_single_member()
258 Example: connection.select_member_by_studentid('nnnnnnnn') ->
259 { 'memberid': 3349, 'name': 'Michael Spang' ...}
261 sql = "SELECT memberid, name, studentid, program, type, userid FROM members WHERE studentid=%s"
262 params = [ studentid ]
264 return self.select_single_member(sql, params)
267 def insert_member(self, name, studentid=None, program=None, mtype='user', userid=None):
269 Creates a member with the specified attributes.
272 name - full name of member
273 studentid - student id number
274 program - program of study
278 Example: connection.insert_member('Michael Spang', '99999999', 'Math/CS') -> 3349
280 Returns: a memberid of created user
283 # retrieve the next memberid
284 sql = "SELECT nextval('memberid_seq')"
285 self.cursor.execute(sql)
286 result = self.cursor.fetchone()
290 sql = "INSERT INTO members (memberid, name, studentid, program, type, userid) VALUES (%d, %s, %s, %s, %s, %s)"
291 params = [ memberid, name, studentid, program, mtype, userid ]
292 self.cursor.execute(sql, params)
295 except pgdb.Error, e:
296 raise DBException("failed to create member: %s" % e)
299 def delete_member(self, memberid):
301 Deletes a member. Note that a member cannot
302 be deleted until it has been unregistered from
306 memberid - the member id number to delete
308 Example: connection.delete_member(3349)
310 sql = "DELETE FROM members WHERE memberid=%d"
311 params = [ memberid ]
314 self.cursor.execute(sql, params)
315 except pgdb.Error, e:
316 raise DBException("DELETE statement failed: %s" %e)
319 def update_member(self, member):
321 Modifies member attributes.
324 member - a column-keyed dictionary with the new state of the member.
325 member['memberid'] must be present. ommitted columns
326 will not be changed. None is NULL.
328 Returns: the full new state of the member as a column-keyed dictionary
330 Example: connection.update_member({
332 'name': 'Michael C. Spang',
336 'name': 'Michael C. Spang',
338 'studentid': '99999999' # unchanged
342 member = connection.select_member_by_id(3349)
343 member['name'] = 'Michael C. Spang'
344 member['program'] = 'CS!'
345 connection.update_member(member) -> { see above }
350 memberid = member['memberid']
352 # retrieve current state of member
353 member_state = self.select_member_by_id(memberid)
355 # build a list of changes to make
357 for column in member.keys():
358 if member[column] != member_state[column]:
360 # column's value must be updated
361 changes.append( (column, member[column]) )
362 member_state[column] = member[column]
368 # make the necessary changes in an update statement
369 changes = zip(*changes)
370 sql = "UPDATE members SET " + ", ".join(["%s=%%s"] * len(changes[0])) % changes[0] + " WHERE memberid=%d"
371 params = changes[1] + ( memberid, )
372 self.cursor.execute(sql, params)
375 except pgdb.Error, e:
376 raise DBException("member update failed: %s" % e)
380 ### Term-related methods ###
382 def select_term(self, memberid, term):
384 Determines whether a member is registered for a term.
387 memberid - the member id number
388 term - the term to check
390 Returns: a matching term, or None
392 Example: connection.select_term(3349, 'f2006') -> 'f2006'
394 sql = "SELECT term FROM terms WHERE memberid=%d AND term=%s"
395 params = [ memberid, term ]
399 self.cursor.execute(sql, params)
400 result = self.cursor.fetchall()
401 except pgdb.Error, e:
402 raise DBException("SELECT statement failed: %s" % e)
405 raise DBException("multiple rows in terms with memberid=%d term=%s" % (memberid, term))
406 elif len(result) == 0:
412 def select_terms(self, memberid):
414 Retrieves a list of terms a member is registered for.
417 memberid - the member id number
419 Returns: a sorted list of terms
421 Example: connection.select_terms(3349) -> ['f2006']
423 sql = "SELECT term FROM terms WHERE memberid=%d"
424 params = [ memberid ]
426 # retrieve the list of terms
428 self.cursor.execute(sql, params)
429 result = self.cursor.fetchall()
430 except pgdb.Error, e:
431 raise DBException("SELECT statement failed: %s" % e)
433 result = [ row[0] for row in result ]
438 def insert_term(self, memberid, term):
440 Registers a member for a term.
443 memberid - the member id number to register
444 term - string representation of the term
446 Example: connection.insert_term(3349, 'f2006')
448 sql = "INSERT INTO terms (memberid, term) VALUES (%d, %s)"
449 params = [ memberid, term ]
452 self.cursor.execute(sql, params)
453 except pgdb.Error, e:
454 raise DBException("INSERT statement failed: %s" % e)
457 def delete_term(self, memberid, term):
459 Unregisters a member for a term.
462 memberid - the member id number to register
463 term - string representation of the term
465 Example: connection.delete_term(3349, 'f2006')
467 sql = "DELETE FROM terms WHERE memberid=%d and term=%s"
468 params = [ memberid, term ]
471 self.cursor.execute(sql, params)
472 except pgdb.Error, e:
473 raise DBException("DELETE statement failed: %s" % e)
476 def delete_term_all(self, memberid):
478 Unregisters a member for all registered terms.
481 memberid - the member id number to unregister
483 Example: connection.delete_term_all(3349)
485 sql = "DELETE FROM terms WHERE memberid=%d"
486 params = [ memberid ]
488 # retrieve a list of terms
490 self.cursor.execute(sql, params)
491 except pgdb.Error, e:
492 raise DBException("DELETE statement failed: %s" % e)
495 ### Miscellaneous methods ###
497 def trim_memberid_sequence(self):
499 Sets the value of the member id sequence to the id of the newest
500 member. For use after testing to prevent large intervals of unused
501 memberids from developing.
503 Note: this does nothing unless the most recently added member(s) have been deleted
505 self.cursor.execute("SELECT setval('memberid_seq', (SELECT max(memberid) FROM members))")
511 if __name__ == '__main__':
513 from csc.common.test import *
515 conffile = "/etc/csc/pgsql.cf"
517 cfg = dict([map(str.strip, a.split("=", 1)) for a in map(str.strip, open(conffile).read().split("\n")) if "=" in a ])
518 hostnm = cfg['server'][1:-1]
519 dbase = cfg['database'][1:-1]
521 # t=test m=member s=student d=default e=expected u=updated
522 tmname = 'Test Member'
523 tmuname = 'Member Test'
526 tmprogram = 'Undecidable'
527 tmuprogram = 'Nondetermined'
531 tmuuserid = 'identifier'
532 tm2name = 'Test Member 2'
534 tm2program = 'Undeclared'
541 emdict = { 'name': tmname, 'program': tmprogram, 'studentid': tmsid, 'type': tmtype, 'userid': tmuserid }
542 emudict = { 'name': tmuname, 'program': tmuprogram, 'studentid': tmusid, 'type': tmutype, 'userid': tmuuserid }
543 em2dict = { 'name': tm2name, 'program': tm2program, 'studentid': tm2sid, 'type': dtype, 'userid': None }
544 em3dict = { 'name': tm3name, 'program': None, 'studentid': None, 'type': dtype, 'userid': None }
547 connection = DBConnection()
550 test(connection.connect)
551 connection.connect(hostnm, dbase)
554 test(connection.connected)
555 assert_equal(True, connection.connected())
558 test(connection.insert_member)
559 tmid = connection.insert_member(tmname, tmsid, tmprogram, tmtype, tmuserid)
560 tm2id = connection.insert_member(tm2name, tm2sid, tm2program)
561 tm3id = connection.insert_member(tm3name)
562 assert_equal(True, int(tmid) >= 0)
563 assert_equal(True, int(tmid) >= 0)
566 emdict['memberid'] = tmid
567 emudict['memberid'] = tmid
568 em2dict['memberid'] = tm2id
569 em3dict['memberid'] = tm3id
571 test(connection.select_member_by_id)
572 m1 = connection.select_member_by_id(tmid)
573 m2 = connection.select_member_by_id(tm2id)
574 m3 = connection.select_member_by_id(tm3id)
575 assert_equal(emdict, m1)
576 assert_equal(em2dict, m2)
577 assert_equal(em3dict, m3)
580 test(connection.select_all_members)
581 members = connection.select_all_members()
582 assert_equal(True, tmid in members)
583 assert_equal(True, tm2id in members)
584 assert_equal(True, tm3id in members)
585 assert_equal(emdict, members[tmid])
588 test(connection.select_members_by_name)
589 members = connection.select_members_by_name(tmname)
590 assert_equal(True, tmid in members)
591 assert_equal(False, tm3id in members)
592 assert_equal(emdict, members[tmid])
595 test(connection.select_member_by_userid)
596 assert_equal(emdict, connection.select_member_by_userid(tmuserid))
599 test(connection.insert_term)
600 connection.insert_term(tmid, tmterm)
601 connection.insert_term(tm3id, tm3term)
602 connection.insert_term(tm3id, tm3term2)
605 test(connection.select_members_by_term)
606 members = connection.select_members_by_term(tmterm)
607 assert_equal(True, tmid in members)
608 assert_equal(False, tm2id in members)
609 assert_equal(False, tm3id in members)
612 test(connection.select_term)
613 assert_equal(tmterm, connection.select_term(tmid, tmterm))
614 assert_equal(None, connection.select_term(tm2id, tmterm))
615 assert_equal(tm3term, connection.select_term(tm3id, tm3term))
616 assert_equal(tm3term2, connection.select_term(tm3id, tm3term2))
619 test(connection.select_terms)
620 trms = connection.select_terms(tmid)
621 trms2 = connection.select_terms(tm2id)
622 assert_equal([tmterm], trms)
623 assert_equal([], trms2)
626 test(connection.delete_term)
627 assert_equal(tm3term, connection.select_term(tm3id, tm3term))
628 connection.delete_term(tm3id, tm3term)
629 assert_equal(None, connection.select_term(tm3id, tm3term))
632 test(connection.update_member)
633 connection.update_member({'memberid': tmid, 'name': tmuname})
634 connection.update_member({'memberid': tmid, 'program': tmuprogram, 'studentid': tmusid })
635 connection.update_member({'memberid': tmid, 'userid': tmuuserid, 'type': tmutype })
636 assert_equal(emudict, connection.select_member_by_id(tmid))
637 connection.update_member(emdict)
638 assert_equal(emdict, connection.select_member_by_id(tmid))
641 test(connection.delete_term_all)
642 connection.delete_term_all(tm2id)
643 connection.delete_term_all(tm3id)
644 assert_equal([], connection.select_terms(tm2id))
645 assert_equal([], connection.select_terms(tm3id))
648 test(connection.delete_member)
649 connection.delete_member(tm3id)
650 assert_equal(None, connection.select_member_by_id(tm3id))
651 negative(connection.delete_member, (tmid,), DBException, "delete of term-registered member")
654 test(connection.rollback)
655 connection.rollback()
656 assert_equal(None, connection.select_member_by_id(tm2id))
659 test(connection.commit)
663 test(connection.trim_memberid_sequence)
664 connection.trim_memberid_sequence()
667 test(connection.disconnect)
668 connection.disconnect()
669 assert_equal(False, connection.connected())
670 connection.disconnect()