2003-01-08 13:42:16 +00:00

453 lines
10 KiB
Python

""" pgdb - DB-SIG compliant module for PygreSQL.
(c) 1999, Pascal Andre <andre@via.ecp.fr>.
See package documentation for further information on copyright.
Inline documentation is sparse. See DB-SIG 2.0 specification for
usage information.
basic usage:
pgdb.connect(connect_string) -> connection
connect_string = 'host:database:user:password:opt:tty'
All parts are optional. You may also pass host through
password as keyword arguments. To pass a port, pass it in
the host keyword parameter:
pgdb.connect(host='localhost:5432')
connection.cursor() -> cursor
connection.commit()
connection.close()
connection.rollback()
cursor.execute(query[, params])
execute a query, binding params (a dictionary) if it is
passed. The binding syntax is the same as the % operator
for dictionaries, and no quoting is done.
cursor.executemany(query, list of params)
execute a query many times, binding each param dictionary
from the list.
cursor.fetchone() -> [value, value, ...]
cursor.fetchall() -> [[value, value, ...], ...]
cursor.fetchmany([size]) -> [[value, value, ...], ...]
returns size or cursor.arraysize number of rows from result
set. Default cursor.arraysize is 1.
cursor.description -> [(column_name, type_name, display_size,
internal_size, precision, scale, null_ok), ...]
Note that precision, scale and null_ok are not implemented.
cursor.rowcount
number of rows available in the result set. Available after
a call to execute.
cursor.close()
"""
import _pg
import string
import exceptions
import types
import time
import types
# Marc-Andre is changing where DateTime goes. This handles it either way.
try: from mx import DateTime
except ImportError: import DateTime
### module constants
# compliant with DB SIG 2.0
apilevel = '2.0'
# module may be shared, but not connections
threadsafety = 1
# this module use extended python format codes
paramstyle = 'pyformat'
### exception hierarchy
class Warning(StandardError):
pass
class Error(StandardError):
pass
class InterfaceError(Error):
pass
class DatabaseError(Error):
pass
class DataError(DatabaseError):
pass
class OperationalError(DatabaseError):
pass
class IntegrityError(DatabaseError):
pass
class InternalError(DatabaseError):
pass
class ProgrammingError(DatabaseError):
pass
class NotSupportedError(DatabaseError):
pass
### internal type handling class
class pgdbTypeCache:
def __init__(self, cnx):
self.__source = cnx.source()
self.__type_cache = {}
def typecast(self, typ, value):
# for NULL values, no typecast is necessary
if value == None:
return value
if typ == STRING:
pass
elif typ == BINARY:
pass
elif typ == BOOL:
value = (value[:1] in ['t','T'])
elif typ == INTEGER:
value = int(value)
elif typ == LONG:
value = long(value)
elif typ == FLOAT:
value = float(value)
elif typ == MONEY:
value = string.replace(value, "$", "")
value = string.replace(value, ",", "")
value = float(value)
elif typ == DATETIME:
# format may differ ... we'll give string
pass
elif typ == ROWID:
value = long(value)
return value
def getdescr(self, oid):
try:
return self.__type_cache[oid]
except:
self.__source.execute(
"SELECT typname, typlen "
"FROM pg_type WHERE oid = %s" % oid
)
res = self.__source.fetch(1)[0]
# column name is omitted from the return value. It will
# have to be prepended by the caller.
res = (
res[0],
None, string.atoi(res[1]),
None, None, None
)
self.__type_cache[oid] = res
return res
### cursor object
class pgdbCursor:
def __init__(self, src, cache):
self.__cache = cache
self.__source = src
self.description = None
self.rowcount = -1
self.arraysize = 1
def close(self):
self.__source.close()
self.description = None
self.rowcount = -1
def execute(self, operation, params = None):
# "The parameters may also be specified as list of
# tuples to e.g. insert multiple rows in a single
# operation, but this kind of usage is deprecated:
if params and type(params) == types.ListType and \
type(params[0]) == types.TupleType:
self.executemany(operation, params)
else:
# not a list of tuples
self.executemany(operation, (params,))
def executemany(self, operation, param_seq):
self.description = None
self.rowcount = -1
# first try to execute all queries
totrows = 0
sql = "INIT"
try:
for params in param_seq:
if params != None:
sql = _quoteparams(operation, params)
else:
sql = operation
rows = self.__source.execute(sql)
if rows != None: # true is __source is NOT a DQL
totrows = totrows + rows
except _pg.error, msg:
raise DatabaseError, "error '%s' in '%s'" % ( msg, sql )
except:
raise OperationalError, "internal error in '%s'" % sql
# then initialize result raw count and description
if self.__source.resulttype == _pg.RESULT_DQL:
self.rowcount = self.__source.ntuples
d = []
for typ in self.__source.listinfo():
# listinfo is a sequence of
# (index, column_name, type_oid)
# getdescr returns all items needed for a
# description tuple except the column_name.
desc = typ[1:2]+self.__cache.getdescr(typ[2])
d.append(desc)
self.description = d
else:
self.rowcount = totrows
self.description = None
def fetchone(self):
res = self.fetchmany(1, 0)
try:
return res[0]
except:
return None
def fetchall(self):
return self.fetchmany(-1, 0)
def fetchmany(self, size = None, keep = 1):
if size == None:
size = self.arraysize
if keep == 1:
self.arraysize = size
try: res = self.__source.fetch(size)
except _pg.error, e: raise DatabaseError, str(e)
result = []
for r in res:
row = []
for i in range(len(r)):
row.append(self.__cache.typecast(
self.description[i][1],
r[i]
)
)
result.append(row)
return result
def nextset(self):
raise NotSupportedError, "nextset() is not supported"
def setinputsizes(self, sizes):
pass
def setoutputsize(self, size, col = 0):
pass
try:
_quote = _pg.quote_fast
_quoteparams = _pg.quoteparams_fast
except (NameError, AttributeError):
def _quote(x):
if type(x) == DateTime.DateTimeType:
x = str(x)
if type(x) == types.StringType:
x = "'" + string.replace(
string.replace(str(x), '\\', '\\\\'), "'", "''") + "'"
elif type(x) in (types.IntType, types.LongType, types.FloatType):
pass
elif x is None:
x = 'NULL'
elif type(x) in (types.ListType, types.TupleType):
x = '(%s)' % string.join(map(lambda x: str(_quote(x)), x), ',')
elif hasattr(x, '__pg_repr__'):
x = x.__pg_repr__()
else:
raise InterfaceError, 'do not know how to handle type %s' % type(x)
return x
def _quoteparams(s, params):
if hasattr(params, 'has_key'):
x = {}
for k, v in params.items():
x[k] = _quote(v)
params = x
else:
params = tuple(map(_quote, params))
return s % params
### connection object
class pgdbCnx:
def __init__(self, cnx):
self.__cnx = cnx
self.__cache = pgdbTypeCache(cnx)
try:
src = self.__cnx.source()
src.execute("BEGIN")
except:
raise OperationalError, "invalid connection."
def close(self):
self.__cnx.close()
def commit(self):
try:
src = self.__cnx.source()
src.execute("COMMIT")
src.execute("BEGIN")
except:
raise OperationalError, "can't commit."
def rollback(self):
try:
src = self.__cnx.source()
src.execute("ROLLBACK")
src.execute("BEGIN")
except:
raise OperationalError, "can't rollback."
def cursor(self):
try:
src = self.__cnx.source()
return pgdbCursor(src, self.__cache)
except:
raise pgOperationalError, "invalid connection."
### module interface
# connects to a database
def connect(dsn = None, user = None, password = None, host = None, database = None):
# first get params from DSN
dbport = -1
dbhost = ""
dbbase = ""
dbuser = ""
dbpasswd = ""
dbopt = ""
dbtty = ""
try:
params = string.split(dsn, ":")
dbhost = params[0]
dbbase = params[1]
dbuser = params[2]
dbpasswd = params[3]
dbopt = params[4]
dbtty = params[5]
except:
pass
# override if necessary
if user != None:
dbuser = user
if password != None:
dbpasswd = password
if database != None:
dbbase = database
if host != None:
try:
params = string.split(host, ":")
dbhost = params[0]
dbport = int(params[1])
except:
pass
# empty host is localhost
if dbhost == "":
dbhost = None
if dbuser == "":
dbuser = None
# open the connection
cnx = _pg.connect(dbbase, dbhost, dbport, dbopt,
dbtty, dbuser, dbpasswd)
return pgdbCnx(cnx)
### types handling
# PostgreSQL is object-oriented: types are dynamic. We must thus use type names
# as internal type codes.
class pgdbType:
def __init__(self, *values):
self.values= values
def __cmp__(self, other):
if other in self.values:
return 0
if other < self.values:
return 1
else:
return -1
STRING = pgdbType(
'char', 'bpchar', 'name', 'text', 'varchar'
)
# BLOB support is pg specific
BINARY = pgdbType()
INTEGER = pgdbType('int2', 'int4', 'serial')
LONG = pgdbType('int8')
FLOAT = pgdbType('float4', 'float8', 'numeric')
BOOL = pgdbType('bool')
MONEY = pgdbType('money')
# this may be problematic as type are quite different ... I hope it won't hurt
DATETIME = pgdbType(
'abstime', 'reltime', 'tinterval', 'date', 'time', 'timespan', 'timestamp', 'timestamptz', 'interval'
)
# OIDs are used for everything (types, tables, BLOBs, rows, ...). This may cause
# confusion, but we are unable to find out what exactly is behind the OID (at
# least not easily enough). Should this be undefined as BLOBs ?
ROWID = pgdbType(
'oid', 'oid8'
)
# mandatory type helpers
def Date(year, month, day):
return DateTime.DateTime(year, month, day)
def Time(hour, minute, second):
return DateTime.TimeDelta(hour, minute, second)
def Timestamp(year, month, day, hour, minute, second):
return DateTime.DateTime(year, month, day, hour, minute, second)
def DateFromTicks(ticks):
return apply(Date, time.localtime(ticks)[:3])
def TimeFromTicks(ticks):
return apply(Time, time.localtime(ticks)[3:6])
def TimestampFromTicks(ticks):
return apply(Timestamp, time.localtime(ticks)[:6])