Project

General

Profile

1 11 aaronmk
# Database access
2
3
import random
4
import re
5
import sys
6
7
import ex_util
8
9 14 aaronmk
def _add_cursor_info(ex, cur): ex_util.add_msg(ex, 'query: '+cur.query)
10
11 11 aaronmk
class NameException(Exception): pass
12
13 14 aaronmk
class DbException(ex_util.ExceptionWithCause):
14
    def __init__(self, msg, cause=None, cur=None):
15
        ex_util.ExceptionWithCause.__init__(self, msg, cause)
16
        if cur != None: _add_cursor_info(self, cur)
17
18
class ExceptionWithColumn(DbException):
19 13 aaronmk
    def __init__(self, col, cause=None):
20 14 aaronmk
        DbException.__init__(self, 'column: '+col, cause)
21 13 aaronmk
        self.col = col
22 11 aaronmk
23 13 aaronmk
class DuplicateKeyException(ExceptionWithColumn): pass
24
25
class NullValueException(ExceptionWithColumn): pass
26
27 11 aaronmk
def check_name(name):
28
    if re.search(r'\W', name) != None: raise NameException('Name "'+name
29
        +'" may contain only alphanumeric characters and _')
30
31
def run_query(db, query, params=None):
32
    cur = db.cursor()
33
    try: cur.execute(query, params)
34
    except Exception, ex:
35 14 aaronmk
        _add_cursor_info(ex, cur)
36 11 aaronmk
        raise
37
    return cur
38
39 14 aaronmk
def row(cur): return iter(lambda: cur.fetchone(), None).next()
40 11 aaronmk
41
def value(cur): return row(cur)[0]
42
43
def with_savepoint(db, func):
44
    savepoint = 'savepoint_'+str(random.randint(0, sys.maxint)) # must be unique
45
    run_query(db, 'SAVEPOINT '+savepoint)
46
    try: return_val = func()
47
    except:
48
        run_query(db, 'ROLLBACK TO SAVEPOINT '+savepoint)
49
        raise
50
    else:
51
        run_query(db, 'RELEASE SAVEPOINT '+savepoint)
52
        return return_val
53
54
def select(db, table, fields, conds):
55
    for field in fields: check_name(field)
56 13 aaronmk
    for col in conds.keys(): check_name(col)
57 11 aaronmk
    def cond(entry):
58 13 aaronmk
        col, value = entry
59
        cond_ = col+' '
60 11 aaronmk
        if value == None: cond_ += 'IS'
61
        else: cond_ += '='
62
        cond_ += ' %s'
63
        return cond_
64
    return run_query(db, 'SELECT '+', '.join(fields)+' FROM '+table+' WHERE '
65
        +' AND '.join(map(cond, conds.iteritems())), conds.values())
66
67 13 aaronmk
def insert(db, table, row):
68 11 aaronmk
    check_name(table)
69 13 aaronmk
    cols = row.keys()
70
    for col in cols: check_name(col)
71
    return run_query(db, 'INSERT INTO '+table+' ('+', '.join(cols)
72
        +') VALUES ('+', '.join(['%s']*len(cols))+')', row.values())
73 11 aaronmk
74 13 aaronmk
def last_insert_id(db): return value(run_query(db, 'SELECT lastval()'))
75
76 14 aaronmk
def try_insert(db, table, row):
77 13 aaronmk
    try: return with_savepoint(db, lambda: insert(db, table, row))
78 11 aaronmk
    except Exception, ex:
79 13 aaronmk
        msg = str(ex)
80 11 aaronmk
        match = re.search(r'duplicate key value violates unique constraint "'
81 13 aaronmk
            +table+'_(\w+)_index"', msg)
82
        if match: raise DuplicateKeyException(match.group(1), ex)
83
        match = re.search(r'null value in column "(\w+)" violates not-null '
84
            'constraint', msg)
85
        if match: raise NullValueException(match.group(1), ex)
86
        raise # no specific exception raised
87 11 aaronmk
88 13 aaronmk
def insert_or_get(db, table, row, pkey, row_ct_ref=None):
89 14 aaronmk
    try: return value(select(db, table, [pkey], row))
90
    except StopIteration:
91
        try:
92
            row_ct = try_insert(db, table, row).rowcount
93
            if row_ct_ref != None and row_ct >= 0: row_ct_ref[0] += row_ct
94
            return last_insert_id(db)
95
        except DuplicateKeyException, ex:
96
            return value(select(db, table, [pkey], {ex.col: row[ex.col]}))