"""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


syntax highlighted by Code2HTML, v. 0.9.1