source: trunk/module/pg.py @ 434

Last change on this file since 434 was 434, checked in by cito, 7 years ago

Add sqlstate attribute to DatabaseError? instances.

  • Property svn:keywords set to Id
File size: 26.1 KB
Line 
1#!/usr/bin/env python
2#
3# pg.py
4#
5# Written by D'Arcy J.M. Cain
6# Improved by Christoph Zwerschke
7#
8# $Id: pg.py 434 2012-05-08 09:18:10Z cito $
9#
10
11"""PyGreSQL classic interface.
12
13This pg module implements some basic database management stuff.
14It includes the _pg module and builds on it, providing the higher
15level wrapper class named DB with addtional functionality.
16This is known as the "classic" ("old style") PyGreSQL interface.
17For a DB-API 2 compliant interface use the newer pgdb module.
18
19"""
20
21from _pg import *
22try:
23    frozenset
24except NameError: # Python < 2.4
25    from sets import ImmutableSet as frozenset
26try:
27    from decimal import Decimal
28    set_decimal(Decimal)
29except ImportError:
30    pass # Python < 2.4
31
32
33# Auxiliary functions which are independent from a DB connection:
34
35def _is_quoted(s):
36    """Check whether this string is a quoted identifier."""
37    s = s.replace('_', 'a')
38    return not s.isalnum() or s[:1].isdigit() or s != s.lower()
39
40def _is_unquoted(s):
41    """Check whether this string is an unquoted identifier."""
42    s = s.replace('_', 'a')
43    return s.isalnum() and not s[:1].isdigit()
44
45def _split_first_part(s):
46    """Split the first part of a dot separated string."""
47    s = s.lstrip()
48    if s[:1] == '"':
49        p = []
50        s = s.split('"', 3)[1:]
51        p.append(s[0])
52        while len(s) == 3 and s[1] == '':
53            p.append('"')
54            s = s[2].split('"', 2)
55            p.append(s[0])
56        p = [''.join(p)]
57        s = '"'.join(s[1:]).lstrip()
58        if s:
59            if s[:0] == '.':
60                p.append(s[1:])
61            else:
62                s = _split_first_part(s)
63                p[0] += s[0]
64                if len(s) > 1:
65                    p.append(s[1])
66    else:
67        p = s.split('.', 1)
68        s = p[0].rstrip()
69        if _is_unquoted(s):
70            s = s.lower()
71        p[0] = s
72    return p
73
74def _split_parts(s):
75    """Split all parts of a dot separated string."""
76    q = []
77    while s:
78        s = _split_first_part(s)
79        q.append(s[0])
80        if len(s) < 2:
81            break
82        s = s[1]
83    return q
84
85def _join_parts(s):
86    """Join all parts of a dot separated string."""
87    return '.'.join([_is_quoted(p) and '"%s"' % p or p for p in s])
88
89def _oid_key(qcl):
90    """Build oid key from qualified class name."""
91    return 'oid(%s)' % qcl
92
93def _db_error(msg, cls=DatabaseError):
94    """Returns DatabaseError with empty sqlstate attribute."""
95    error = cls(msg)
96    error.sqlstate = None
97    return error
98
99def _int_error(msg):
100    """Returns InternalError."""
101    return _db_error(msg, InternalError)
102
103def _prg_error(msg):
104    """Returns ProgrammingError."""
105    return _db_error(msg, ProgrammingError)
106
107
108# The PostGreSQL database connection interface:
109
110class DB(object):
111    """Wrapper class for the _pg connection type."""
112
113    def __init__(self, *args, **kw):
114        """Create a new connection.
115
116        You can pass either the connection parameters or an existing
117        _pg or pgdb connection. This allows you to use the methods
118        of the classic pg interface with a DB-API 2 pgdb connection.
119
120        """
121        if not args and len(kw) == 1:
122            db = kw.get('db')
123        elif not kw and len(args) == 1:
124            db = args[0]
125        else:
126            db = None
127        if db:
128            if isinstance(db, DB):
129                db = db.db
130            else:
131                try:
132                    db = db._cnx
133                except AttributeError:
134                    pass
135        if not db or not hasattr(db, 'db') or not hasattr(db, 'query'):
136            db = connect(*args, **kw)
137            self._closeable = 1
138        else:
139            self._closeable = 0
140        self.db = db
141        self.dbname = db.db
142        self._attnames = {}
143        self._pkeys = {}
144        self._privileges = {}
145        self._args = args, kw
146        self.debug = None # For debugging scripts, this can be set
147            # * to a string format specification (e.g. in CGI set to "%s<BR>"),
148            # * to a file object to write debug statements or
149            # * to a callable object which takes a string argument.
150
151    def __getattr__(self, name):
152        # All undefined members are the same as in the underlying pg connection:
153        if self.db:
154            return getattr(self.db, name)
155        else:
156            raise _int_error('Connection is not valid')
157
158    # Auxiliary methods
159
160    def _do_debug(self, s):
161        """Print a debug message."""
162        if self.debug:
163            if isinstance(self.debug, basestring):
164                sys.stdout.write((self.debug % s) + '\n')
165            elif isinstance(self.debug, file):
166                file.write(s + '\n')
167            elif callable(self.debug):
168                self.debug(s)
169
170    def _quote_text(self, d):
171        """Quote text value."""
172        if not isinstance(d, basestring):
173            d = str(d)
174        return "'%s'" % self.escape_string(d)
175
176    _bool_true = frozenset('t true 1 y yes on'.split())
177
178    def _quote_bool(self, d):
179        """Quote boolean value."""
180        if isinstance(d, basestring):
181            if not d:
182                return 'NULL'
183            d = d.lower() in self._bool_true
184        else:
185            d = bool(d)
186        return ("'f'", "'t'")[d]
187
188    _date_literals = frozenset('current_date current_time'
189        ' current_timestamp localtime localtimestamp'.split())
190
191    def _quote_date(self, d):
192        """Quote date value."""
193        if not d:
194            return 'NULL'
195        if isinstance(d, basestring) and d.lower() in self._date_literals:
196            return d
197        return self._quote_text(d)
198
199    def _quote_num(self, d):
200        """Quote numeric value."""
201        if not d and d != 0:
202            return 'NULL'
203        return str(d)
204
205    def _quote_money(self, d):
206        """Quote money value."""
207        if d is None or d == '':
208            return 'NULL'
209        return "'%.2f'" % float(d)
210
211    _quote_funcs = dict( # quote methods for each type
212        text=_quote_text, bool=_quote_bool, date=_quote_date,
213        int=_quote_num, num=_quote_num, float=_quote_num,
214        money=_quote_money)
215
216    def _quote(self, d, t):
217        """Return quotes if needed."""
218        if d is None:
219            return 'NULL'
220        try:
221            quote_func = self._quote_funcs[t]
222        except KeyError:
223            quote_func = self._quote_funcs['text']
224        return quote_func(self, d)
225
226    def _split_schema(self, cl):
227        """Return schema and name of object separately.
228
229        This auxiliary function splits off the namespace (schema)
230        belonging to the class with the name cl. If the class name
231        is not qualified, the function is able to determine the schema
232        of the class, taking into account the current search path.
233
234        """
235        s = _split_parts(cl)
236        if len(s) > 1: # name already qualfied?
237            # should be database.schema.table or schema.table
238            if len(s) > 3:
239                raise _prg_error('Too many dots in class name %s' % cl)
240            schema, cl = s[-2:]
241        else:
242            cl = s[0]
243            # determine search path
244            q = 'SELECT current_schemas(TRUE)'
245            schemas = self.db.query(q).getresult()[0][0][1:-1].split(',')
246            if schemas: # non-empty path
247                # search schema for this object in the current search path
248                q = ' UNION '.join(
249                    ["SELECT %d::integer AS n, '%s'::name AS nspname"
250                        % s for s in enumerate(schemas)])
251                q = ("SELECT nspname FROM pg_class"
252                    " JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid"
253                    " JOIN (%s) AS p USING (nspname)"
254                    " WHERE pg_class.relname = '%s'"
255                    " ORDER BY n LIMIT 1" % (q, cl))
256                schema = self.db.query(q).getresult()
257                if schema: # schema found
258                    schema = schema[0][0]
259                else: # object not found in current search path
260                    schema = 'public'
261            else: # empty path
262                schema = 'public'
263        return schema, cl
264
265    def _add_schema(self, cl):
266        """Ensure that the class name is prefixed with a schema name."""
267        return _join_parts(self._split_schema(cl))
268
269    # Public methods
270
271    # escape_string and escape_bytea exist as methods,
272    # so we define unescape_bytea as a method as well
273    unescape_bytea = staticmethod(unescape_bytea)
274
275    def close(self):
276        """Close the database connection."""
277        # Wraps shared library function so we can track state.
278        if self._closeable:
279            if self.db:
280                self.db.close()
281                self.db = None
282            else:
283                raise _int_error('Connection already closed')
284
285    def reset(self):
286        """Reset connection with current parameters.
287
288        All derived queries and large objects derived from this connection
289        will not be usable after this call.
290
291        """
292        self.db.reset()
293
294    def reopen(self):
295        """Reopen connection to the database.
296
297        Used in case we need another connection to the same database.
298        Note that we can still reopen a database that we have closed.
299
300        """
301        # There is no such shared library function.
302        if self._closeable:
303            db = connect(*self._args[0], **self._args[1])
304            if self.db:
305                self.db.close()
306            self.db = db
307
308    def query(self, qstr):
309        """Executes a SQL command string.
310
311        This method simply sends a SQL query to the database. If the query is
312        an insert statement that inserted exactly one row into a table that
313        has OIDs, the return value is the OID of the newly inserted row.
314        If the query is an update or delete statement, or an insert statement
315        that did not insert exactly one row in a table with OIDs, then the
316        numer of rows affected is returned as a string. If it is a statement
317        that returns rows as a result (usually a select statement, but maybe
318        also an "insert/update ... returning" statement), this method returns
319        a pgqueryobject that can be accessed via getresult() or dictresult()
320        or simply printed. Otherwise, it returns `None`.
321
322        """
323        # Wraps shared library function for debugging.
324        if not self.db:
325            raise _int_error('Connection is not valid')
326        self._do_debug(qstr)
327        return self.db.query(qstr)
328
329    def pkey(self, cl, newpkey=None):
330        """This method gets or sets the primary key of a class.
331
332        Composite primary keys are represented as frozensets. Note that
333        this raises an exception if the table does not have a primary key.
334
335        If newpkey is set and is not a dictionary then set that
336        value as the primary key of the class.  If it is a dictionary
337        then replace the _pkeys dictionary with a copy of it.
338
339        """
340        # First see if the caller is supplying a dictionary
341        if isinstance(newpkey, dict):
342            # make sure that all classes have a namespace
343            self._pkeys = dict([
344                ('.' in cl and cl or 'public.' + cl, pkey)
345                for cl, pkey in newpkey.iteritems()])
346            return self._pkeys
347
348        qcl = self._add_schema(cl) # build fully qualified class name
349        # Check if the caller is supplying a new primary key for the class
350        if newpkey:
351            self._pkeys[qcl] = newpkey
352            return newpkey
353
354        # Get all the primary keys at once
355        if qcl not in self._pkeys:
356            # if not found, check again in case it was added after we started
357            self._pkeys = {}
358            if self.server_version >= 80200:
359                # the ANY syntax works correctly only with PostgreSQL >= 8.2
360                any_indkey = "= ANY (pg_index.indkey)"
361            else:
362                any_indkey = "IN (%s)" % ', '.join(
363                    ['pg_index.indkey[%d]' % i for i in range(16)])
364            for r in self.db.query(
365                "SELECT pg_namespace.nspname, pg_class.relname,"
366                    " pg_attribute.attname FROM pg_class"
367                " JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace"
368                    " AND pg_namespace.nspname NOT LIKE 'pg_%'"
369                " JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid"
370                    " AND pg_attribute.attisdropped = 'f'"
371                " JOIN pg_index ON pg_index.indrelid = pg_class.oid"
372                    " AND pg_index.indisprimary = 't'"
373                    " AND pg_attribute.attnum " + any_indkey).getresult():
374                cl, pkey = _join_parts(r[:2]), r[2]
375                self._pkeys.setdefault(cl, []).append(pkey)
376            # (only) for composite primary keys, the values will be frozensets
377            for cl, pkey in self._pkeys.iteritems():
378                self._pkeys[cl] = len(pkey) > 1 and frozenset(pkey) or pkey[0]
379            self._do_debug(self._pkeys)
380
381        # will raise an exception if primary key doesn't exist
382        return self._pkeys[qcl]
383
384    def get_databases(self):
385        """Get list of databases in the system."""
386        return [s[0] for s in
387            self.db.query('SELECT datname FROM pg_database').getresult()]
388
389    def get_relations(self, kinds=None):
390        """Get list of relations in connected database of specified kinds.
391
392            If kinds is None or empty, all kinds of relations are returned.
393            Otherwise kinds can be a string or sequence of type letters
394            specifying which kind of relations you want to list.
395
396        """
397        where = kinds and "pg_class.relkind IN (%s) AND" % ','.join(
398            ["'%s'" % x for x in kinds]) or ''
399        return map(_join_parts, self.db.query(
400            "SELECT pg_namespace.nspname, pg_class.relname "
401            "FROM pg_class "
402            "JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace "
403            "WHERE %s pg_class.relname !~ '^Inv' AND "
404                "pg_class.relname !~ '^pg_' "
405            "ORDER BY 1, 2" % where).getresult())
406
407    def get_tables(self):
408        """Return list of tables in connected database."""
409        return self.get_relations('r')
410
411    def get_attnames(self, cl, newattnames=None):
412        """Given the name of a table, digs out the set of attribute names.
413
414        Returns a dictionary of attribute names (the names are the keys,
415        the values are the names of the attributes' types).
416        If the optional newattnames exists, it must be a dictionary and
417        will become the new attribute names dictionary.
418
419        """
420        if isinstance(newattnames, dict):
421            self._attnames = newattnames
422            return
423        elif newattnames:
424            raise _prg_error('If supplied, newattnames must be a dictionary')
425        cl = self._split_schema(cl) # split into schema and class
426        qcl = _join_parts(cl) # build fully qualified name
427        # May as well cache them:
428        if qcl in self._attnames:
429            return self._attnames[qcl]
430        if qcl not in self.get_relations('rv'):
431            raise _prg_error('Class %s does not exist' % qcl)
432        t = {}
433        for att, typ in self.db.query("SELECT pg_attribute.attname,"
434            " pg_type.typname FROM pg_class"
435            " JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid"
436            " JOIN pg_attribute ON pg_attribute.attrelid = pg_class.oid"
437            " JOIN pg_type ON pg_type.oid = pg_attribute.atttypid"
438            " WHERE pg_namespace.nspname = '%s' AND pg_class.relname = '%s'"
439            " AND (pg_attribute.attnum > 0 or pg_attribute.attname = 'oid')"
440            " AND pg_attribute.attisdropped = 'f'"
441                % cl).getresult():
442            if typ.startswith('bool'):
443                t[att] = 'bool'
444            elif typ.startswith('abstime'):
445                t[att] = 'date'
446            elif typ.startswith('date'):
447                t[att] = 'date'
448            elif typ.startswith('interval'):
449                t[att] = 'date'
450            elif typ.startswith('timestamp'):
451                t[att] = 'date'
452            elif typ.startswith('oid'):
453                t[att] = 'int'
454            elif typ.startswith('int'):
455                t[att] = 'int'
456            elif typ.startswith('float'):
457                t[att] = 'float'
458            elif typ.startswith('numeric'):
459                t[att] = 'num'
460            elif typ.startswith('money'):
461                t[att] = 'money'
462            else:
463                t[att] = 'text'
464        self._attnames[qcl] = t # cache it
465        return self._attnames[qcl]
466
467    def has_table_privilege(self, cl, privilege='select'):
468        """Check whether current user has specified table privilege."""
469        qcl = self._add_schema(cl)
470        privilege = privilege.lower()
471        try:
472            return self._privileges[(qcl, privilege)]
473        except KeyError:
474            q = "SELECT has_table_privilege('%s', '%s')" % (qcl, privilege)
475            ret = self.db.query(q).getresult()[0][0] == 't'
476            self._privileges[(qcl, privilege)] = ret
477            return ret
478
479    def get(self, cl, arg, keyname=None):
480        """Get a tuple from a database table or view.
481
482        This method is the basic mechanism to get a single row.  The keyname
483        that the key specifies a unique row.  If keyname is not specified
484        then the primary key for the table is used.  If arg is a dictionary
485        then the value for the key is taken from it and it is modified to
486        include the new values, replacing existing values where necessary.
487        For a composite key, keyname can also be a sequence of key names.
488        The OID is also put into the dictionary if the table has one, but
489        in order to allow the caller to work with multiple tables, it is
490        munged as oid(schema.table).
491
492        """
493        if cl.endswith('*'): # scan descendant tables?
494            cl = cl[:-1].rstrip() # need parent table name
495        # build qualified class name
496        qcl = self._add_schema(cl)
497        # To allow users to work with multiple tables,
498        # we munge the name of the "oid" the key
499        qoid = _oid_key(qcl)
500        if not keyname:
501            # use the primary key by default
502            try:
503                keyname = self.pkey(qcl)
504            except KeyError:
505                raise _prg_error('Class %s has no primary key' % qcl)
506        # We want the oid for later updates if that isn't the key
507        if keyname == 'oid':
508            if isinstance(arg, dict):
509                if qoid not in arg:
510                    raise _db_error('%s not in arg' % qoid)
511            else:
512                arg = {qoid: arg}
513            where = 'oid = %s' % arg[qoid]
514            attnames = '*'
515        else:
516            attnames = self.get_attnames(qcl)
517            if isinstance(keyname, basestring):
518                keyname = (keyname,)
519            if not isinstance(arg, dict):
520                if len(keyname) > 1:
521                    raise _prg_error('Composite key needs dict as arg')
522                arg = dict([(k, arg) for k in keyname])
523            where = ' AND '.join(['%s = %s'
524                % (k, self._quote(arg[k], attnames[k])) for k in keyname])
525            attnames = ', '.join(attnames)
526        q = 'SELECT %s FROM %s WHERE %s LIMIT 1' % (attnames, qcl, where)
527        self._do_debug(q)
528        res = self.db.query(q).dictresult()
529        if not res:
530            raise _db_error('No such record in %s where %s' % (qcl, where))
531        for att, value in res[0].iteritems():
532            arg[att == 'oid' and qoid or att] = value
533        return arg
534
535    def insert(self, cl, d=None, **kw):
536        """Insert a tuple into a database table.
537
538        This method inserts a row into a table.  If a dictionary is
539        supplied it starts with that.  Otherwise it uses a blank dictionary.
540        Either way the dictionary is updated from the keywords.
541
542        The dictionary is then, if possible, reloaded with the values actually
543        inserted in order to pick up values modified by rules, triggers, etc.
544
545        Note: The method currently doesn't support insert into views
546        although PostgreSQL does.
547
548        """
549        qcl = self._add_schema(cl)
550        qoid = _oid_key(qcl)
551        if d is None:
552            d = {}
553        d.update(kw)
554        attnames = self.get_attnames(qcl)
555        names, values = [], []
556        for n in attnames:
557            if n != 'oid' and n in d:
558                names.append('"%s"' % n)
559                values.append(self._quote(d[n], attnames[n]))
560        names, values = ', '.join(names), ', '.join(values)
561        selectable = self.has_table_privilege(qcl)
562        if selectable and self.server_version >= 80200:
563            ret = ' RETURNING %s*' % ('oid' in attnames and 'oid, ' or '')
564        else:
565            ret = ''
566        q = 'INSERT INTO %s (%s) VALUES (%s)%s' % (qcl, names, values, ret)
567        self._do_debug(q)
568        res = self.db.query(q)
569        if ret:
570            res = res.dictresult()
571            for att, value in res[0].iteritems():
572                d[att == 'oid' and qoid or att] = value
573        elif isinstance(res, int):
574            d[qoid] = res
575            if selectable:
576                self.get(qcl, d, 'oid')
577        elif selectable:
578            if qoid in d:
579                self.get(qcl, d, 'oid')
580            else:
581                try:
582                    self.get(qcl, d)
583                except ProgrammingError:
584                    pass # table has no primary key
585        return d
586
587    def update(self, cl, d=None, **kw):
588        """Update an existing row in a database table.
589
590        Similar to insert but updates an existing row.  The update is based
591        on the OID value as munged by get or passed as keyword, or on the
592        primary key of the table.  The dictionary is modified, if possible,
593        to reflect any changes caused by the update due to triggers, rules,
594        default values, etc.
595
596        """
597        # Update always works on the oid which get returns if available,
598        # otherwise use the primary key.  Fail if neither.
599        # Note that we only accept oid key from named args for safety
600        qcl = self._add_schema(cl)
601        qoid = _oid_key(qcl)
602        if 'oid' in kw:
603            kw[qoid] = kw['oid']
604            del kw['oid']
605        if d is None:
606            d = {}
607        d.update(kw)
608        attnames = self.get_attnames(qcl)
609        if qoid in d:
610            where = 'oid = %s' % d[qoid]
611            keyname = ()
612        else:
613            try:
614                keyname = self.pkey(qcl)
615            except KeyError:
616                raise _prg_error('Class %s has no primary key' % qcl)
617            if isinstance(keyname, basestring):
618                keyname = (keyname,)
619            try:
620                where = ' AND '.join(['%s = %s'
621                    % (k, self._quote(d[k], attnames[k])) for k in keyname])
622            except KeyError:
623                raise _prg_error('Update needs primary key or oid.')
624        values = []
625        for n in attnames:
626            if n in d and n not in keyname:
627                values.append('%s = %s' % (n, self._quote(d[n], attnames[n])))
628        if not values:
629            return d
630        values = ', '.join(values)
631        selectable = self.has_table_privilege(qcl)
632        if selectable and self.server_version >= 880200:
633            ret = ' RETURNING %s*' % ('oid' in attnames and 'oid, ' or '')
634        else:
635            ret = ''
636        q = 'UPDATE %s SET %s WHERE %s%s' % (qcl, values, where, ret)
637        self._do_debug(q)
638        res = self.db.query(q)
639        if ret:
640            res = res.dictresult()[0]
641            for att, value in res.iteritems():
642                d[att == 'oid' and qoid or att] = value
643        else:
644            if selectable:
645                if qoid in d:
646                    self.get(qcl, d, 'oid')
647                else:
648                    self.get(qcl, d)
649        return d
650
651    def clear(self, cl, a=None):
652        """
653
654        This method clears all the attributes to values determined by the types.
655        Numeric types are set to 0, Booleans are set to 'f', and everything
656        else is set to the empty string.  If the array argument is present,
657        it is used as the array and any entries matching attribute names are
658        cleared with everything else left unchanged.
659
660        """
661        # At some point we will need a way to get defaults from a table.
662        qcl = self._add_schema(cl)
663        if a is None:
664            a = {} # empty if argument is not present
665        attnames = self.get_attnames(qcl)
666        for n, t in attnames.iteritems():
667            if n == 'oid':
668                continue
669            if t in ('int', 'float', 'num', 'money'):
670                a[n] = 0
671            elif t == 'bool':
672                a[n] = 'f'
673            else:
674                a[n] = ''
675        return a
676
677    def delete(self, cl, d=None, **kw):
678        """Delete an existing row in a database table.
679
680        This method deletes the row from a table.  It deletes based on the
681        OID value as munged by get or passed as keyword, or on the primary
682        key of the table.  The return value is the number of deleted rows
683        (i.e. 0 if the row did not exist and 1 if the row was deleted).
684
685        """
686        # Like update, delete works on the oid.
687        # One day we will be testing that the record to be deleted
688        # isn't referenced somewhere (or else PostgreSQL will).
689        # Note that we only accept oid key from named args for safety
690        qcl = self._add_schema(cl)
691        qoid = _oid_key(qcl)
692        if 'oid' in kw:
693            kw[qoid] = kw['oid']
694            del kw['oid']
695        if d is None:
696            d = {}
697        d.update(kw)
698        if qoid in d:
699            where = 'oid = %s' % d[qoid]
700        else:
701            try:
702                keyname = self.pkey(qcl)
703            except KeyError:
704                raise _prg_error('Class %s has no primary key' % qcl)
705            if isinstance(keyname, basestring):
706                keyname = (keyname,)
707            attnames = self.get_attnames(qcl)
708            try:
709                where = ' AND '.join(['%s = %s'
710                    % (k, self._quote(d[k], attnames[k])) for k in keyname])
711            except KeyError:
712                raise _prg_error('Delete needs primary key or oid.')
713        q = 'DELETE FROM %s WHERE %s' % (qcl, where)
714        self._do_debug(q)
715        return int(self.db.query(q))
716
717
718# if run as script, print some information
719
720if __name__ == '__main__':
721    print('PyGreSQL version' + version)
722    print('')
723    print(__doc__)
Note: See TracBrowser for help on using the repository browser.