"""Module implementing a SQL based repository for cfvers""" # Copyright 2003-2005 Iustin Pop # # This file is part of cfvers. # # cfvers is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # cfvers is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with cfvers; if not, write to the Free Software Foundation, # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # $Id: sql.py 218 2005-10-30 09:26:23Z iusty $ import base64 import quopri import os import re import bz2 import zlib import sys import time __all__ = ["RSql", "VERSION", "Param"] VERSION = 1 import cfvers from cfvers.main import * class Param: def __init__(self, value): self.value = value def __repr__(self): return 'Param(%r)' % (self.value,) class RSql(object): """Abstract class for all SQL-bases backends""" def __init__(self, create=False, cnxargs=None, createopts=None): if type(self) is RSql: raise TypeError, "You can't instantiate a RSql!" return def _create(self, createopts=None): """Creates the database structure""" cursor = self.conn.cursor() if createopts is not None and createopts.force: cursor2 = self.conn.cursor() self._delete_schema(cursor2) self.commit() cursor2.close() self._init_schema(cursor) if createopts is None or createopts.doarea: self._init_data(cursor) self.conn.commit() cursor.close() return def _init_schema(self, cursor): """Creates the database objects""" cursor.execute("CREATE TABLE areas ( " \ "name TEXT PRIMARY KEY, " \ "root TEXT, " \ "ctime TIMESTAMP, " \ "description TEXT)") cursor.execute("CREATE TABLE revisions (" \ "area TEXT, " \ "revno INTEGER CHECK(revno > 0), " \ "server TEXT, " \ "logmsg TEXT, " \ "ctime TIMESTAMP, " \ "uid INTEGER, " \ "uname TEXT, " \ "gid INTEGER, " \ "gname TEXT, " \ "commiter TEXT, " \ "PRIMARY KEY(area, revno))") cursor.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, " \ "area TEXT NOT NULL, " \ "name TEXT NOT NULL, " \ "ctime TIMESTAMP NOT NULL, " \ "flags INTEGER NOT NULL, "\ "command TEXT, "\ "dirname TEXT)") cursor.execute("CREATE UNIQUE INDEX items_an_idx ON " \ "items (area, name)") cursor.execute("CREATE INDEX items_dir_idx ON " \ "items (dirname)") cursor.execute("CREATE TABLE entries (item INTEGER, " \ "revno INTEGER, filename TEXT, status \"char\", " \ "filetype INTEGER, filetype_c CHAR(1), " \ "filecontents BYTEA, sha1sum TEXT, " \ "size INTEGER, " \ "mode INTEGER, " \ "mtime INTEGER, " \ "atime INTEGER, " \ "ctime INTEGER, " \ "inode INT8, " \ "device INTEGER, " \ "nlink INTEGER, " \ "uid INTEGER, gid INTEGER, " \ "uname TEXT, gname TEXT, " \ "rdev INTEGER, " \ "blocks INTEGER, " \ "blksize INTEGER, " \ "encoding TEXT, " \ "exitcode INTEGER," \ "PRIMARY KEY(item, revno))") cursor.execute("CREATE TABLE config (" \ "page TEXT NOT NULL, " \ "key TEXT NOT NULL, " \ "value TEXT NOT NULL, " \ "PRIMARY KEY(page, key))" \ ) self.set_param("repository", "version", 1) self.set_param("repository", "creation date", time.strftime("%Y-%m-%d %T %Z")) return def _delete_schema(self, cursor): """Deletes the database objects""" for i in "entries", "revisions", "items", "areas", "config": self._try_stm(cursor, "DROP TABLE %s" % i, quiet=True) return def _try_stm(self, cursor, stm, vals={}, quiet=False): """Tryes to execute an statement and commit otherwise rollbacks""" try: cursor.execute(stm, vals) self.commit() except Exception, e: if not quiet: print >>sys.stderr, "Info: An error has occured: '%s'. Continuing" % e self.rollback() return def _init_data(self, cursor): """Creates a standard repository""" a = Area(name="default", description="Default area", root="/") self.addArea(a) return def _check_schema(self, cursor): """Checks the database for compatibility""" try: value = self.get_param('repository', 'version') except Exception, e: raise cfvers.ConfigException, "Incompatible repository (%s)" % e if value is None: raise cfvers.ConfigException, "Incompatible repository (missing version information)" try: value = int(value) except ValueError, e: raise cfvers.ConfigException, "Incompatible repository (invalid version information)" if value != VERSION: raise cfvers.ConfigException, \ "Incompatible repository (expected [%d] != present [%d])" \ % (VERSION, value) return def get_param(self, page, key, defa=None): """Gets a configuration parameter""" cursor = self.conn.cursor() self._exec(cursor, "select value from config where page = ", Param(page), " and key = ", Param(key)) arr = cursor.fetchall() cursor.close() if len(arr) == 0: return defa return arr[0][0] def set_param(self, page, key, value): """Sets a configuration parameter""" if value is None: raise ValueError, "Configuration values must not be null!" cursor = self.conn.cursor() self._exec(cursor, "select value from config where page = ", Param(page), " and key = ", Param(key)) arr = cursor.fetchall() if len(arr) == 0: oldval = None self._exec(cursor, "insert into config (page, key, value) values (", Param(page), ",", Param(key), ",", Param(value), ")") else: self._exec(cursor, "update config set value=", Param(value), " where page=", Param(page), " and key=", Param(key)) oldval = arr[0][0] cursor.close() return def close(self): """Closes the database connection""" self.conn.close() return def commit(self): """Executes a commit on the database link""" self.conn.commit() return def rollback(self): """Executes a rollback on the database link""" self.conn.rollback() return def getItemByID(self, id): """Returns an item by ID. Since the item ID is unique across all areas, it does not need the area name as a parameter. Parameters: - id: integer, item ID; """ cursor = self.conn.cursor() self._exec(cursor, "select id, area, name, ctime, dirname, flags, command "\ "from items where id = ", Param(id)) row = cursor.fetchone() cursor.close() if row is None: return None i = Item(id=row[0], area=row[1], name=row[2], ctime=row[3], dirname=row[4], flags=row[5], command=row[6]) return i def getItemByName(self, areaname, name): """Returns an item by area name and item name. Parameters: - area: the area name; - name: the item name; """ cursor = self.conn.cursor() self._exec(cursor, "select id, area, name, ctime, dirname, flags, command "\ "from items where area = ", Param(areaname), " and name = ", Param(name)) row = cursor.fetchone() cursor.close() if row is None: return None i = Item(id=row[0], area=row[1], name=row[2], ctime=row[3], dirname=row[4], flags=row[5], command=row[6]) return i def getItemsByDirname(self, areaname, dirname): """Returns all items locate under a directory Returns the list of all items which are located under a (fully qualified) directory. Parameters: - areaname: the area name; - dirname: the directory name; """ cursor = self.conn.cursor() self._exec(cursor, "select id, name, ctime, flags, command from items "\ "where area = ", Param(areaname), " and dirname = ", Param(dirname)) ret = [Item(id=row[0], area=areaname, name=row[1], ctime=row[2], dirname=dirname, flags=row[3], command=row[4]) for row in cursor.fetchall()] cursor.close() return ret def addItem(self, item): """Adds a new item Given an item instance, it adds it to the database. Parameters: - item: Item instance; """ cursor = self.conn.cursor() self._exec(cursor, "insert into items (area, name, ctime, dirname, "\ "flags, command) values (", Param(item.area), ",", Param(item.name), ",", Param(self.TimestampFromMx(item.ctime)), ",", Param(item.dirname), ",", Param(item.flags), ",", Param(item.command), ")") self._exec(cursor, "select id from items where area = ", Param(item.area), " and name = ", Param(item.name)) id = cursor.fetchone()[0] item.id = id cursor.close() return item def updItem(self, item): pass def addEntry(self, entry): """Adds a new revision entry Given a Entry instance, it adds it to the database. Parameters: - entry: Entry instance; """ cursor = self.conn.cursor() payload, encoding = self._encode_payload(entry.filecontents) if entry.filetype is None: filetype_c = None else: filetype_c = entry.modemap[entry.filetype][1] self._exec(cursor, """insert into entries (item, revno, filename, filetype, filetype_c, filecontents, mode, mtime, atime, uid, gid, uname, gname, rdev, encoding, sha1sum, size, ctime, inode, device, nlink, blocks, blksize, status, exitcode) values (""", Param(entry.item), ", ", Param(entry.revno), ", ", Param(entry.filename), ", ", Param(entry.filetype), ",", Param(filetype_c), ",", Param(payload), ", ", Param(entry.mode), ", ", Param(entry.mtime), ", ", Param(entry.atime), ", ", Param(entry.uid), ", ", Param(entry.gid), ", ", Param(entry.uname), ", ", Param(entry.gname), ", ", Param(entry.rdev), ", ", Param(encoding), ", ", Param(entry.sha1sum), ", ", Param(entry.size), ", ", Param(entry.ctime), ", ", Param(entry.inode), ", ", Param(entry.device), ", ", Param(entry.nlink), ", ", Param(entry.blocks), ", ", Param(entry.blksize), ", ", Param(entry.status), ", ", Param(entry.exitcode), ")") cursor.close() return def getItems(self, areaname): """Returns the list of items in an area Parameters: - area: the area name; """ cursor = self.conn.cursor() if areaname is None: raise ValueError, "Area not given" self._exec(cursor, "select id, area, name, ctime, dirname, flags, command "\ "from items where area = ", Param(areaname), " order by id") while True: row = cursor.fetchone() if row is None: break yield Item(id=row[0], area=areaname, name=row[2], ctime=row[3], dirname=row[4], flags=row[5], command=row[6]) cursor.close() return def getAreas(self): """Returns the list of areas""" cursor = self.conn.cursor() cursor.execute("select name, root, ctime, (select count(*) from items where items.area = areas.name) as nitems, (select max(revno) from revisions where revisions.area = areas.name) as revno, description from areas") areas = [Area(name=row[0], root=row[1], ctime=row[2], numitems=row[3], revno=row[4], description=row[5]) for row in cursor.fetchall()] cursor.close() return areas def getEntry(self, itemid, revno, do_payload=True): """Returns a Entry for an item Parameters: - item: Item id; - revno: the revision number which to retrieve; - do_payload: whether to retrieve the payload also or only the metadata; """ cursor = self.conn.cursor() if do_payload: pfield = 'filecontents' else: pfield = 'Null' sqlist = [ "select e.item, e.revno, e.filename, %s, e.filetype, "\ "e.mode, e.mtime, e.atime, e.uid, e.gid, e.uname, "\ "e.gname, e.rdev, e.encoding, e.sha1sum, e.size, "\ "e.ctime, e.inode, e.device, e.nlink, e.blocks, "\ "e.blksize, e.status, i.area, e.exitcode "\ "from entries e join items i on e.item = i.id "\ "where item = " % pfield, Param(itemid) ] if revno is not None: sqlist.append(" and revno <= ") sqlist.append(Param(revno)) sqlist.append(" order by revno desc limit 1") self._exec(cursor, *sqlist) row = cursor.fetchone() cursor.close() if row is None: return None e = Entry() (e.item, e.revno, e.filename, payload, e.filetype, e.mode, e.mtime, e.atime, e.uid, e.gid, e.uname, e.gname, e.rdev, encoding, e.sha1sum, e.size, e.ctime, e.inode, e.device, e.nlink, e.blocks, e.blksize, e.status, e.areaname, e.exitcode ) = row e.filecontents = self._decode_payload(payload, encoding) return e def _encode_payload(self, payload, dobzip2=False): """Encodes the data of a file for storage in the database Parameters: - payload: the string containing the data; - dobzip2: whether do bzip2-compress the data Returns: - payload: the encoded payload; - encoding: a string representing the methods applied; The string will be binary-encoded with a driver-specific encoding/decoding method pair. Currently the encoding can only be bzip2, since custom encoding formats have been abandoned. """ if payload is None: return None, None encoding = "" if dobzip2: ndata = bz2.compress(payload) if len(ndata) < len(payload): payload = ndata encoding = "bzip2:%s" % encoding payload = self._encode_binary(payload) return payload, encoding def _decode_payload(self, payload, encoding): """Decodes an encoded-payload Parameters: - payload: the string containing the data; - encoding: the string containing the encoding methods applied on the original data; """ if payload is None: return None payload = self._decode_binary(payload) for enc in encoding.split(":"): if enc is None or enc == "": break elif enc == "base64": payload = base64.decodestring(payload) elif enc == "quoted-printable": payload = quopri.decodestring(payload) elif enc == "bzip2": payload = bz2.decompress(payload) elif enc == "gzip": payload = zlib.decompress(payload) else: raise ValueError, "Unknown encoding '%s'!" % enc return payload def _encode_binary(value): raise NotImplementedError _encode_binary = staticmethod(_encode_binary) def _decode_binary(value): raise NotImplementedError _decode_binary = staticmethod(_decode_binary) def getEntryList(self, itemid): """Returns the Entry list for an item""" entries = [] cursor = self.conn.cursor() self._exec(cursor,"select e.item, e.revno, e.filename, e.filetype, "\ "e.mode, e.mtime, e.atime, e.uid, e.gid, e.uname, "\ "e.gname, e.rdev, e.size, e.status, i.area, e.exitcode "\ "from entries e join items i on e.item = i.id "\ "where item = ", Param(itemid), " order by revno desc") for row in cursor.fetchall(): e = Entry() (e.item, e.revno, e.filename, e.filetype, e.mode, e.mtime, e.atime, e.uid, e.gid, e.uname, e.gname, e.rdev, e.size, e.status, e.areaname, e.exitcode) = row entries.append(e) cursor.close() return entries def getRevNumbers(self, item): """Returns the revision numbers for an item""" cursor = self.conn.cursor() self._exec(cursor, "select revno from entries where item = ", Param(item), " order by revno") ret = [x[0] for x in cursor.fetchall()] cursor.close() return ret def addArea(self, a): """Adds an area to the database""" self._exec(None, "insert into areas (name, root, ctime, description) values (", Param(a.name), ",", Param(a.root), ",", Param(self.TimestampFromMx(a.ctime)), ",", Param(a.description), ")") return def updArea(self, a): """Updates an area in the database""" self._exec(None, "update areas set name = ", Param(a.name), ", description = ", Param(a.description), ", root = ", Param(a.root), " where id = ", Param(a.name)) return def getArea(self, name): """Returns an area Parameters: - area: the name of the area Returns: - Area instance """ cursor = self.conn.cursor() self._exec(cursor, "select name, root, ctime, (select count(*) from items where items.area = areas.name) as nitems, (select max(revno) from revisions where revisions.area = areas.name) as revno, description from areas where name = ", Param(name)) row = cursor.fetchone() cursor.close() if row is None: return None a = Area(name=row[0], root=row[1], ctime=row[2], numitems=row[3], revno=row[4], description=row[5]) return a def getRevisions(self, areaname): """Returns the Revision list for an area""" cursor = self.conn.cursor() sel._exec(cursor, "select area, revno, logmsg, ctime, uid, gid, uname, gname, commiter, server from revisions where area = ", Param(areaname), " order by revno desc") rs = [] for row in cursor.fetchall(): r = Revision() (r.area, r.revno, r.logmsg, r.ctime, r.uid, r.gid, r.uname, r.gname, r.commiter, r.server) = row rs.append(r) cursor.close() return rs def getRevisionItems(self, areaname, revno): """Returns the list of item IDs for an Revision Parameters: - areaname: the area name; - revno: the revision number """ cursor = self.conn.cursor() self._exec(cursor, "select items.id, items.name, entries.status from items, entries where items.area = ", Param(areaname), " and entries.revno = ", Param(revno), " and entries.item = items.id") ret = list(cursor.fetchall()) cursor.close() return ret def putRevision(self, r): """Adds a Revision to the database and fills its revno It is important to note that only this function can return a valid new revision number. Until stored in the repository, the new revision number cannot be safely determined. """ raise NotImplementedError def numAreas(self): """Returns the number of areas""" cursor = self.conn.cursor() cursor.execute("select count(1) from areas") row = cursor.fetchone() cursor.close() return int(row[0]) def _check_op_comparison(op): op = str(op).lower() cdict = { 'lt': '<', 'le': '<=', 'eq': '=', 'ne': '!=', 'ge': '>=', 'gt': '>', } if op not in cdict: raise ParsingException("Invalid comparison operator %s" % op) return cdict[op] _check_op_comparison = staticmethod(_check_op_comparison) def _check_op_text(op): op = str(op).lower() cdict = { 'eq': '=', 'ne': '!=', 'regex': '~', 'iregex': '~*', } if op not in cdict: raise ParsingException("Invalid text operator %s" % op) return cdict[op] _check_op_text = staticmethod(_check_op_text) def _check_op_qual(op): op = str(op).lower() qdict = { 'eq': '=', 'ne': '!=', } if op not in cdict: raise ParsingException("Invalid equality operator %s" % op) return qdict[op] _check_op_qual = staticmethod(_check_op_qual) def _check_arg_int(arg): try: ret = int(arg) except ValueError: raise ParsingException("Invalid integer value %s" % arg) return ret _check_arg_int = staticmethod(_check_arg_int) def _parseSearch(self, options): mlist = [] if not options.allareas: # no operand # string arg mlist.append(["i.area = ", Param(options.area)]) if options.find_logmsg: # no operand # string arg mlist.append(["r.logmsg ~ ", Param(options.find_logmsg)]) if options.find_revno: # comparison operator # int arg for op, revno in options.find_revno: op = self._check_op_comparison(op) revno = self._check_arg_int(revno) mlist.append(["r.revno ", op, Param(revno)]) if options.find_empty: # no operand # no arg mlist.append(["e.size = 0 and e.filetype_c in ('d','f')"]) if options.find_gid is not None: # comparison operator # int arg for op, gid in options.find_gid: op = self._check_op_comparison(op) gid = self._check_arg_int(gid) mlist.append(["e.gid is not Null and e.gid ", op, Param(gid)]) if options.find_group is not None: # text operator # string arg for op, group in options.find_group: op = self._check_op_text(op) mlist.append(["e.gname ", op, Param(group)]) if options.find_uid is not None: # comparison operator # int arg for op, uid in options.find_uid: op = self._check_op_comparison(op) uid = self._check_arg_int(uid) mlist.append(["e.uid ", op, Param(uid)]) if options.find_user is not None: # text operator # string arg for op, user in options.find_user: op = self._check_op_text(op) mlist.append(["e.uname ", op, Param(user)]) if options.find_regex is not None: # no operator # string arg for r in options.find_regex: mlist.append(["e.filename ~ ", Param(r)]) if options.find_iregex is not None: # no operator # string arg for r in options.find_iregex: mlist.append(["e.filename ~* ", Param(r)]) if options.find_size is not None: # comparison operator # int+unit arg for op, size in options.find_size: op = self._check_op_comparison(op) unit = size[-1].upper() size = size[:-1] if unit == "G": multiplier = 1073741824 elif unit == "M": multiplier = 1048576 elif unit == "K": multiplier = 1024 else: multiplier = 1 size += unit size = self._check_arg_int(size) * multiplier mlist.append(['e.size ', op, Param(size)]) if options.find_type is not None: # boolean operator # special string arg, manual check for op, arg in options.find_type: op = self._check_op_qual(op) if arg not in ('d', 'f', 'l', 'b', 'c', 'p', 's', 'v'): raise ParsingError("Invalid file type for --type: %s" % op) mlist.append(["e.filetype_c ", op, Param(arg)]) if options.find_nouser: # no operator # no arg mlist.append("e.uname is Null") if options.find_nogroup: # no operator # no arg mlist.append("e.gname is Null") if options.find_name is not None: # no operator # string arg for glob in options.find_name: mlist.append(["fnmatch(basename(e.filename), ", Param(glob), ")"]) if options.find_iname is not None: # no operator # string arg for glob in options.find_iname: mlist.append(["fnmatch(basename(upper(e.filename)), ", Param(glob.upper()), ")"]) if options.find_path is not None: # no operator # string arg for glob in options.find_path: mlist.append(["fnmatch(e.filename, ", Param(glob), ")"]) if options.find_ipath is not None: # no operator # string arg for glob in options.find_ipath: mlist.append(["fnmatch(upper(e.filename), ", Param(glob.upper()), ")"]) if options.find_links is not None: # comparison operator # integer arg for op, n in options.find_links: op = self._check_op_comparison(op) n = self._check_arg_int(n) mlist.append(["e.nlink ", op, Param(n)]) if options.find_inum is not None: # comparison operator # integer arg for op, n in options.find_inum: op = self._check_op_comparison(op) n = self._check_arg_int(n) mlist.append(["e.inode ", op, Param(n)]) if options.find_lname is not None: # no operator # string arg for glob in options.find_lname: mlist.append(["fnmatch(e.filecontents, ", Param(glob), ") and e.filetype_c = 'l'"]) if options.find_ilname is not None: # no operator # string arg for glob in options.find_ilname: mlist.append(["fnmatch(upper(e.filecontents), ", Param(glob), ") and e.filetype_c = 'l'"]) if options.find_perm is not None: # special operator, manual check # integer arg, manual check for op, perms in options.find_perm: op = str(op).upper() mask = "(e.mode & %d)" % int(07777) try: match = int(perms, 8) except ValueError: raise ParsingException("Invalid integer %s" % perms) if op == "EQ": mlist.append([mask, " = ", Param(match)]) elif op == "LT": mlist.append(["(", mask, " & ", Param(match), ") > 0"]) elif op == "GT": mlist.append(["(", mask, " & ", Param(match), ") = ", Param(match)]) else: raise ParsingException("Invalid operand for --perm: %s" % op) return mlist def getEntries(self, options): """Returns all RevEntries for an area and a revision number""" cursor = self.conn.cursor() sql = ["select"] if not getattr(options, "allentries", False): sql.append(" distinct on (e.item)") sql.append(" e.item, e.revno, e.filename, e.filetype") if getattr(options, "do_payload", True): sql.append(', e.filecontents') else: sql.append(', Null') sql.append(", e.mode, e.mtime, e.atime, e.uid, e.gid, e.uname, e.gname, "\ "e.rdev, e.encoding, e.sha1sum, e.size, e.ctime, e.inode, "\ "e.device, e.nlink, e.blocks, e.blksize, e.status, i.area, "\ "e.exitcode "\ "from entries e, items i, revisions r "\ "where i.id = e.item AND e.revno = r.revno AND "\ "i.area = r.area AND e.status = 'M'") if getattr(options, "match_and", True): mioper = ' AND ' else: mioper = ' OR ' mlist = self._parseSearch(options) if len(mlist) > 0: sql.append(" AND (") sql.extend(mlist[0]) for pair in mlist[1:]: sql.append(mioper) sql.append("(") sql.extend(pair) sql.append(")") sql.append(")") #print sql self._exec(cursor, *sql) while True: row = cursor.fetchone() if row is None: break e = Entry() (e.item, e.revno, e.filename, e.filetype, payload, e.mode, e.mtime, e.atime, e.uid, e.gid, e.uname, e.gname, e.rdev, encoding, e.sha1sum, e.size, e.ctime, e.inode, e.device, e.nlink, e.blocks, e.blksize, e.status, e.areaname, e.exitcode, ) = row e.filecontents = self._decode_payload(payload, encoding) yield e cursor.close() return def _quote_qmark(chunks): query_parts = [] params = [] for chunk in chunks: if isinstance(chunk, Param): params.append(chunk.value) query_parts.append('?') else: query_parts.append(chunk) return ''.join(query_parts), params _quote_qmark = staticmethod(_quote_qmark) def _quote_numeric(chunks): query_parts = [] params = [] for chunk in chunks: if isinstance(chunk, Param): params.append(chunk.value) query_parts.append(':%d' % len(params)) else: query_parts.append(chunk) return ''.join(query_parts), tuple(params) # DCOracle2 has broken support # for sequences of other types _quote_numeric = staticmethod(_quote_numeric) def _quote_named(chunks): query_parts = [] params = {} for chunk in chunks: if isinstance(chunk, Param): name = 'p%d' % len(params) # Are numbers in name allowed? params[name] = chunk.value query_parts.append(':%s' % name) else: query_parts.append(chunk) return ''.join(query_parts), params _quote_named = staticmethod(_quote_named) def _quote_format(chunks): query_parts = [] params = [] for chunk in chunks: if isinstance(chunk, Param): params.append(chunk.value) query_parts.append('%s') else: query_parts.append(chunk.replace('%', '%%')) return ''.join(query_parts), params _quote_format = staticmethod(_quote_format) def _quote_pyformat(chunks): query_parts = [] params = {} for chunk in chunks: if isinstance(chunk, Param): name = '%d' % len(params) params[name] = chunk.value query_parts.append('%%(%s)s' % name) else: query_parts.append(chunk.replace('%', '%%')) return ''.join(query_parts), params _quote_pyformat = staticmethod(_quote_pyformat) def _exec(self, cursor, *chunks): close = False if cursor is None: cursor = self.conn.cursor() close = True query, parms = self._quote(chunks) #print query, parms ret = cursor.execute(query, parms) if close: cursor.close() return ret