1#-----------------------------------------------------------------------
2#
3# Copyright (C) 2000, 2001 by Autonomous Zone Industries
4# Copyright (C) 2002 Gregory P. Smith
5#
6# License:      This is free software.  You may use this software for any
7#               purpose including modification/redistribution, so long as
8#               this header remains intact and that you do not claim any
9#               rights of ownership or authorship of this software.  This
10#               software has been tested, but no warranty is expressed or
11#               implied.
12#
13#   --  Gregory P. Smith <greg@krypto.org>
14
15# This provides a simple database table interface built on top of
16# the Python Berkeley DB 3 interface.
17#
18_cvsid = '$Id$'
19
20import re
21import sys
22import copy
23import random
24import struct
25
26
27if sys.version_info[0] >= 3 :
28    import pickle
29else :
30    if sys.version_info < (2, 6) :
31        import cPickle as pickle
32    else :
33        # When we drop support for python 2.4
34        # we could use: (in 2.5 we need a __future__ statement)
35        #
36        #    with warnings.catch_warnings():
37        #        warnings.filterwarnings(...)
38        #        ...
39        #
40        # We can not use "with" as is, because it would be invalid syntax
41        # in python 2.4 and (with no __future__) 2.5.
42        # Here we simulate "with" following PEP 343 :
43        import warnings
44        w = warnings.catch_warnings()
45        w.__enter__()
46        try :
47            warnings.filterwarnings('ignore',
48                message='the cPickle module has been removed in Python 3.0',
49                category=DeprecationWarning)
50            import cPickle as pickle
51        finally :
52            w.__exit__()
53        del w
54
55try:
56    # For Pythons w/distutils pybsddb
57    from bsddb3 import db
58except ImportError:
59    # For Python 2.3
60    from bsddb import db
61
62class TableDBError(StandardError):
63    pass
64class TableAlreadyExists(TableDBError):
65    pass
66
67
68class Cond:
69    """This condition matches everything"""
70    def __call__(self, s):
71        return 1
72
73class ExactCond(Cond):
74    """Acts as an exact match condition function"""
75    def __init__(self, strtomatch):
76        self.strtomatch = strtomatch
77    def __call__(self, s):
78        return s == self.strtomatch
79
80class PrefixCond(Cond):
81    """Acts as a condition function for matching a string prefix"""
82    def __init__(self, prefix):
83        self.prefix = prefix
84    def __call__(self, s):
85        return s[:len(self.prefix)] == self.prefix
86
87class PostfixCond(Cond):
88    """Acts as a condition function for matching a string postfix"""
89    def __init__(self, postfix):
90        self.postfix = postfix
91    def __call__(self, s):
92        return s[-len(self.postfix):] == self.postfix
93
94class LikeCond(Cond):
95    """
96    Acts as a function that will match using an SQL 'LIKE' style
97    string.  Case insensitive and % signs are wild cards.
98    This isn't perfect but it should work for the simple common cases.
99    """
100    def __init__(self, likestr, re_flags=re.IGNORECASE):
101        # escape python re characters
102        chars_to_escape = '.*+()[]?'
103        for char in chars_to_escape :
104            likestr = likestr.replace(char, '\\'+char)
105        # convert %s to wildcards
106        self.likestr = likestr.replace('%', '.*')
107        self.re = re.compile('^'+self.likestr+'$', re_flags)
108    def __call__(self, s):
109        return self.re.match(s)
110
111#
112# keys used to store database metadata
113#
114_table_names_key = '__TABLE_NAMES__'  # list of the tables in this db
115_columns = '._COLUMNS__'  # table_name+this key contains a list of columns
116
117def _columns_key(table):
118    return table + _columns
119
120#
121# these keys are found within table sub databases
122#
123_data =  '._DATA_.'  # this+column+this+rowid key contains table data
124_rowid = '._ROWID_.' # this+rowid+this key contains a unique entry for each
125                     # row in the table.  (no data is stored)
126_rowid_str_len = 8   # length in bytes of the unique rowid strings
127
128
129def _data_key(table, col, rowid):
130    return table + _data + col + _data + rowid
131
132def _search_col_data_key(table, col):
133    return table + _data + col + _data
134
135def _search_all_data_key(table):
136    return table + _data
137
138def _rowid_key(table, rowid):
139    return table + _rowid + rowid + _rowid
140
141def _search_rowid_key(table):
142    return table + _rowid
143
144def contains_metastrings(s) :
145    """Verify that the given string does not contain any
146    metadata strings that might interfere with dbtables database operation.
147    """
148    if (s.find(_table_names_key) >= 0 or
149        s.find(_columns) >= 0 or
150        s.find(_data) >= 0 or
151        s.find(_rowid) >= 0):
152        # Then
153        return 1
154    else:
155        return 0
156
157
158class bsdTableDB :
159    def __init__(self, filename, dbhome, create=0, truncate=0, mode=0600,
160                 recover=0, dbflags=0):
161        """bsdTableDB(filename, dbhome, create=0, truncate=0, mode=0600)
162
163        Open database name in the dbhome Berkeley DB directory.
164        Use keyword arguments when calling this constructor.
165        """
166        self.db = None
167        myflags = db.DB_THREAD
168        if create:
169            myflags |= db.DB_CREATE
170        flagsforenv = (db.DB_INIT_MPOOL | db.DB_INIT_LOCK | db.DB_INIT_LOG |
171                       db.DB_INIT_TXN | dbflags)
172        # DB_AUTO_COMMIT isn't a valid flag for env.open()
173        try:
174            dbflags |= db.DB_AUTO_COMMIT
175        except AttributeError:
176            pass
177        if recover:
178            flagsforenv = flagsforenv | db.DB_RECOVER
179        self.env = db.DBEnv()
180        # enable auto deadlock avoidance
181        self.env.set_lk_detect(db.DB_LOCK_DEFAULT)
182        self.env.open(dbhome, myflags | flagsforenv)
183        if truncate:
184            myflags |= db.DB_TRUNCATE
185        self.db = db.DB(self.env)
186        # this code relies on DBCursor.set* methods to raise exceptions
187        # rather than returning None
188        self.db.set_get_returns_none(1)
189        # allow duplicate entries [warning: be careful w/ metadata]
190        self.db.set_flags(db.DB_DUP)
191        self.db.open(filename, db.DB_BTREE, dbflags | myflags, mode)
192        self.dbfilename = filename
193
194        if sys.version_info[0] >= 3 :
195            class cursor_py3k(object) :
196                def __init__(self, dbcursor) :
197                    self._dbcursor = dbcursor
198
199                def close(self) :
200                    return self._dbcursor.close()
201
202                def set_range(self, search) :
203                    v = self._dbcursor.set_range(bytes(search, "iso8859-1"))
204                    if v is not None :
205                        v = (v[0].decode("iso8859-1"),
206                                v[1].decode("iso8859-1"))
207                    return v
208
209                def __next__(self) :
210                    v = getattr(self._dbcursor, "next")()
211                    if v is not None :
212                        v = (v[0].decode("iso8859-1"),
213                                v[1].decode("iso8859-1"))
214                    return v
215
216            class db_py3k(object) :
217                def __init__(self, db) :
218                    self._db = db
219
220                def cursor(self, txn=None) :
221                    return cursor_py3k(self._db.cursor(txn=txn))
222
223                def has_key(self, key, txn=None) :
224                    return getattr(self._db,"has_key")(bytes(key, "iso8859-1"),
225                            txn=txn)
226
227                def put(self, key, value, flags=0, txn=None) :
228                    key = bytes(key, "iso8859-1")
229                    if value is not None :
230                        value = bytes(value, "iso8859-1")
231                    return self._db.put(key, value, flags=flags, txn=txn)
232
233                def put_bytes(self, key, value, txn=None) :
234                    key = bytes(key, "iso8859-1")
235                    return self._db.put(key, value, txn=txn)
236
237                def get(self, key, txn=None, flags=0) :
238                    key = bytes(key, "iso8859-1")
239                    v = self._db.get(key, txn=txn, flags=flags)
240                    if v is not None :
241                        v = v.decode("iso8859-1")
242                    return v
243
244                def get_bytes(self, key, txn=None, flags=0) :
245                    key = bytes(key, "iso8859-1")
246                    return self._db.get(key, txn=txn, flags=flags)
247
248                def delete(self, key, txn=None) :
249                    key = bytes(key, "iso8859-1")
250                    return self._db.delete(key, txn=txn)
251
252                def close (self) :
253                    return self._db.close()
254
255            self.db = db_py3k(self.db)
256        else :  # Python 2.x
257            pass
258
259        # Initialize the table names list if this is a new database
260        txn = self.env.txn_begin()
261        try:
262            if not getattr(self.db, "has_key")(_table_names_key, txn):
263                getattr(self.db, "put_bytes", self.db.put) \
264                        (_table_names_key, pickle.dumps([], 1), txn=txn)
265        # Yes, bare except
266        except:
267            txn.abort()
268            raise
269        else:
270            txn.commit()
271        # TODO verify more of the database's metadata?
272        self.__tablecolumns = {}
273
274    def __del__(self):
275        self.close()
276
277    def close(self):
278        if self.db is not None:
279            self.db.close()
280            self.db = None
281        if self.env is not None:
282            self.env.close()
283            self.env = None
284
285    def checkpoint(self, mins=0):
286        self.env.txn_checkpoint(mins)
287
288    def sync(self):
289        self.db.sync()
290
291    def _db_print(self) :
292        """Print the database to stdout for debugging"""
293        print "******** Printing raw database for debugging ********"
294        cur = self.db.cursor()
295        try:
296            key, data = cur.first()
297            while 1:
298                print repr({key: data})
299                next = cur.next()
300                if next:
301                    key, data = next
302                else:
303                    cur.close()
304                    return
305        except db.DBNotFoundError:
306            cur.close()
307
308
309    def CreateTable(self, table, columns):
310        """CreateTable(table, columns) - Create a new table in the database.
311
312        raises TableDBError if it already exists or for other DB errors.
313        """
314        assert isinstance(columns, list)
315
316        txn = None
317        try:
318            # checking sanity of the table and column names here on
319            # table creation will prevent problems elsewhere.
320            if contains_metastrings(table):
321                raise ValueError(
322                    "bad table name: contains reserved metastrings")
323            for column in columns :
324                if contains_metastrings(column):
325                    raise ValueError(
326                        "bad column name: contains reserved metastrings")
327
328            columnlist_key = _columns_key(table)
329            if getattr(self.db, "has_key")(columnlist_key):
330                raise TableAlreadyExists, "table already exists"
331
332            txn = self.env.txn_begin()
333            # store the table's column info
334            getattr(self.db, "put_bytes", self.db.put)(columnlist_key,
335                    pickle.dumps(columns, 1), txn=txn)
336
337            # add the table name to the tablelist
338            tablelist = pickle.loads(getattr(self.db, "get_bytes",
339                self.db.get) (_table_names_key, txn=txn, flags=db.DB_RMW))
340            tablelist.append(table)
341            # delete 1st, in case we opened with DB_DUP
342            self.db.delete(_table_names_key, txn=txn)
343            getattr(self.db, "put_bytes", self.db.put)(_table_names_key,
344                    pickle.dumps(tablelist, 1), txn=txn)
345
346            txn.commit()
347            txn = None
348        except db.DBError, dberror:
349            if txn:
350                txn.abort()
351            if sys.version_info < (2, 6) :
352                raise TableDBError, dberror[1]
353            else :
354                raise TableDBError, dberror.args[1]
355
356
357    def ListTableColumns(self, table):
358        """Return a list of columns in the given table.
359        [] if the table doesn't exist.
360        """
361        assert isinstance(table, str)
362        if contains_metastrings(table):
363            raise ValueError, "bad table name: contains reserved metastrings"
364
365        columnlist_key = _columns_key(table)
366        if not getattr(self.db, "has_key")(columnlist_key):
367            return []
368        pickledcolumnlist = getattr(self.db, "get_bytes",
369                self.db.get)(columnlist_key)
370        if pickledcolumnlist:
371            return pickle.loads(pickledcolumnlist)
372        else:
373            return []
374
375    def ListTables(self):
376        """Return a list of tables in this database."""
377        pickledtablelist = self.db.get_get(_table_names_key)
378        if pickledtablelist:
379            return pickle.loads(pickledtablelist)
380        else:
381            return []
382
383    def CreateOrExtendTable(self, table, columns):
384        """CreateOrExtendTable(table, columns)
385
386        Create a new table in the database.
387
388        If a table of this name already exists, extend it to have any
389        additional columns present in the given list as well as
390        all of its current columns.
391        """
392        assert isinstance(columns, list)
393
394        try:
395            self.CreateTable(table, columns)
396        except TableAlreadyExists:
397            # the table already existed, add any new columns
398            txn = None
399            try:
400                columnlist_key = _columns_key(table)
401                txn = self.env.txn_begin()
402
403                # load the current column list
404                oldcolumnlist = pickle.loads(
405                    getattr(self.db, "get_bytes",
406                        self.db.get)(columnlist_key, txn=txn, flags=db.DB_RMW))
407                # create a hash table for fast lookups of column names in the
408                # loop below
409                oldcolumnhash = {}
410                for c in oldcolumnlist:
411                    oldcolumnhash[c] = c
412
413                # create a new column list containing both the old and new
414                # column names
415                newcolumnlist = copy.copy(oldcolumnlist)
416                for c in columns:
417                    if not c in oldcolumnhash:
418                        newcolumnlist.append(c)
419
420                # store the table's new extended column list
421                if newcolumnlist != oldcolumnlist :
422                    # delete the old one first since we opened with DB_DUP
423                    self.db.delete(columnlist_key, txn=txn)
424                    getattr(self.db, "put_bytes", self.db.put)(columnlist_key,
425                                pickle.dumps(newcolumnlist, 1),
426                                txn=txn)
427
428                txn.commit()
429                txn = None
430
431                self.__load_column_info(table)
432            except db.DBError, dberror:
433                if txn:
434                    txn.abort()
435                if sys.version_info < (2, 6) :
436                    raise TableDBError, dberror[1]
437                else :
438                    raise TableDBError, dberror.args[1]
439
440
441    def __load_column_info(self, table) :
442        """initialize the self.__tablecolumns dict"""
443        # check the column names
444        try:
445            tcolpickles = getattr(self.db, "get_bytes",
446                    self.db.get)(_columns_key(table))
447        except db.DBNotFoundError:
448            raise TableDBError, "unknown table: %r" % (table,)
449        if not tcolpickles:
450            raise TableDBError, "unknown table: %r" % (table,)
451        self.__tablecolumns[table] = pickle.loads(tcolpickles)
452
453    def __new_rowid(self, table, txn) :
454        """Create a new unique row identifier"""
455        unique = 0
456        while not unique:
457            # Generate a random 64-bit row ID string
458            # (note: might have <64 bits of true randomness
459            # but it's plenty for our database id needs!)
460            blist = []
461            for x in xrange(_rowid_str_len):
462                blist.append(random.randint(0,255))
463            newid = struct.pack('B'*_rowid_str_len, *blist)
464
465            if sys.version_info[0] >= 3 :
466                newid = newid.decode("iso8859-1")  # 8 bits
467
468            # Guarantee uniqueness by adding this key to the database
469            try:
470                self.db.put(_rowid_key(table, newid), None, txn=txn,
471                            flags=db.DB_NOOVERWRITE)
472            except db.DBKeyExistError:
473                pass
474            else:
475                unique = 1
476
477        return newid
478
479
480    def Insert(self, table, rowdict) :
481        """Insert(table, datadict) - Insert a new row into the table
482        using the keys+values from rowdict as the column values.
483        """
484
485        txn = None
486        try:
487            if not getattr(self.db, "has_key")(_columns_key(table)):
488                raise TableDBError, "unknown table"
489
490            # check the validity of each column name
491            if not table in self.__tablecolumns:
492                self.__load_column_info(table)
493            for column in rowdict.keys() :
494                if not self.__tablecolumns[table].count(column):
495                    raise TableDBError, "unknown column: %r" % (column,)
496
497            # get a unique row identifier for this row
498            txn = self.env.txn_begin()
499            rowid = self.__new_rowid(table, txn=txn)
500
501            # insert the row values into the table database
502            for column, dataitem in rowdict.items():
503                # store the value
504                self.db.put(_data_key(table, column, rowid), dataitem, txn=txn)
505
506            txn.commit()
507            txn = None
508
509        except db.DBError, dberror:
510            # WIBNI we could just abort the txn and re-raise the exception?
511            # But no, because TableDBError is not related to DBError via
512            # inheritance, so it would be backwards incompatible.  Do the next
513            # best thing.
514            info = sys.exc_info()
515            if txn:
516                txn.abort()
517                self.db.delete(_rowid_key(table, rowid))
518            if sys.version_info < (2, 6) :
519                raise TableDBError, dberror[1], info[2]
520            else :
521                raise TableDBError, dberror.args[1], info[2]
522
523
524    def Modify(self, table, conditions={}, mappings={}):
525        """Modify(table, conditions={}, mappings={}) - Modify items in rows matching 'conditions' using mapping functions in 'mappings'
526
527        * table - the table name
528        * conditions - a dictionary keyed on column names containing
529          a condition callable expecting the data string as an
530          argument and returning a boolean.
531        * mappings - a dictionary keyed on column names containing a
532          condition callable expecting the data string as an argument and
533          returning the new string for that column.
534        """
535
536        try:
537            matching_rowids = self.__Select(table, [], conditions)
538
539            # modify only requested columns
540            columns = mappings.keys()
541            for rowid in matching_rowids.keys():
542                txn = None
543                try:
544                    for column in columns:
545                        txn = self.env.txn_begin()
546                        # modify the requested column
547                        try:
548                            dataitem = self.db.get(
549                                _data_key(table, column, rowid),
550                                txn=txn)
551                            self.db.delete(
552                                _data_key(table, column, rowid),
553                                txn=txn)
554                        except db.DBNotFoundError:
555                             # XXXXXXX row key somehow didn't exist, assume no
556                             # error
557                            dataitem = None
558                        dataitem = mappings[column](dataitem)
559                        if dataitem is not None:
560                            self.db.put(
561                                _data_key(table, column, rowid),
562                                dataitem, txn=txn)
563                        txn.commit()
564                        txn = None
565
566                # catch all exceptions here since we call unknown callables
567                except:
568                    if txn:
569                        txn.abort()
570                    raise
571
572        except db.DBError, dberror:
573            if sys.version_info < (2, 6) :
574                raise TableDBError, dberror[1]
575            else :
576                raise TableDBError, dberror.args[1]
577
578    def Delete(self, table, conditions={}):
579        """Delete(table, conditions) - Delete items matching the given
580        conditions from the table.
581
582        * conditions - a dictionary keyed on column names containing
583          condition functions expecting the data string as an
584          argument and returning a boolean.
585        """
586
587        try:
588            matching_rowids = self.__Select(table, [], conditions)
589
590            # delete row data from all columns
591            columns = self.__tablecolumns[table]
592            for rowid in matching_rowids.keys():
593                txn = None
594                try:
595                    txn = self.env.txn_begin()
596                    for column in columns:
597                        # delete the data key
598                        try:
599                            self.db.delete(_data_key(table, column, rowid),
600                                           txn=txn)
601                        except db.DBNotFoundError:
602                            # XXXXXXX column may not exist, assume no error
603                            pass
604
605                    try:
606                        self.db.delete(_rowid_key(table, rowid), txn=txn)
607                    except db.DBNotFoundError:
608                        # XXXXXXX row key somehow didn't exist, assume no error
609                        pass
610                    txn.commit()
611                    txn = None
612                except db.DBError, dberror:
613                    if txn:
614                        txn.abort()
615                    raise
616        except db.DBError, dberror:
617            if sys.version_info < (2, 6) :
618                raise TableDBError, dberror[1]
619            else :
620                raise TableDBError, dberror.args[1]
621
622
623    def Select(self, table, columns, conditions={}):
624        """Select(table, columns, conditions) - retrieve specific row data
625        Returns a list of row column->value mapping dictionaries.
626
627        * columns - a list of which column data to return.  If
628          columns is None, all columns will be returned.
629        * conditions - a dictionary keyed on column names
630          containing callable conditions expecting the data string as an
631          argument and returning a boolean.
632        """
633        try:
634            if not table in self.__tablecolumns:
635                self.__load_column_info(table)
636            if columns is None:
637                columns = self.__tablecolumns[table]
638            matching_rowids = self.__Select(table, columns, conditions)
639        except db.DBError, dberror:
640            if sys.version_info < (2, 6) :
641                raise TableDBError, dberror[1]
642            else :
643                raise TableDBError, dberror.args[1]
644        # return the matches as a list of dictionaries
645        return matching_rowids.values()
646
647
648    def __Select(self, table, columns, conditions):
649        """__Select() - Used to implement Select and Delete (above)
650        Returns a dictionary keyed on rowids containing dicts
651        holding the row data for columns listed in the columns param
652        that match the given conditions.
653        * conditions is a dictionary keyed on column names
654        containing callable conditions expecting the data string as an
655        argument and returning a boolean.
656        """
657        # check the validity of each column name
658        if not table in self.__tablecolumns:
659            self.__load_column_info(table)
660        if columns is None:
661            columns = self.tablecolumns[table]
662        for column in (columns + conditions.keys()):
663            if not self.__tablecolumns[table].count(column):
664                raise TableDBError, "unknown column: %r" % (column,)
665
666        # keyed on rows that match so far, containings dicts keyed on
667        # column names containing the data for that row and column.
668        matching_rowids = {}
669        # keys are rowids that do not match
670        rejected_rowids = {}
671
672        # attempt to sort the conditions in such a way as to minimize full
673        # column lookups
674        def cmp_conditions(atuple, btuple):
675            a = atuple[1]
676            b = btuple[1]
677            if type(a) is type(b):
678
679                # Needed for python 3. "cmp" vanished in 3.0.1
680                def cmp(a, b) :
681                    if a==b : return 0
682                    if a<b : return -1
683                    return 1
684
685                if isinstance(a, PrefixCond) and isinstance(b, PrefixCond):
686                    # longest prefix first
687                    return cmp(len(b.prefix), len(a.prefix))
688                if isinstance(a, LikeCond) and isinstance(b, LikeCond):
689                    # longest likestr first
690                    return cmp(len(b.likestr), len(a.likestr))
691                return 0
692            if isinstance(a, ExactCond):
693                return -1
694            if isinstance(b, ExactCond):
695                return 1
696            if isinstance(a, PrefixCond):
697                return -1
698            if isinstance(b, PrefixCond):
699                return 1
700            # leave all unknown condition callables alone as equals
701            return 0
702
703        if sys.version_info < (2, 6) :
704            conditionlist = conditions.items()
705            conditionlist.sort(cmp_conditions)
706        else :  # Insertion Sort. Please, improve
707            conditionlist = []
708            for i in conditions.items() :
709                for j, k in enumerate(conditionlist) :
710                    r = cmp_conditions(k, i)
711                    if r == 1 :
712                        conditionlist.insert(j, i)
713                        break
714                else :
715                    conditionlist.append(i)
716
717        # Apply conditions to column data to find what we want
718        cur = self.db.cursor()
719        column_num = -1
720        for column, condition in conditionlist:
721            column_num = column_num + 1
722            searchkey = _search_col_data_key(table, column)
723            # speedup: don't linear search columns within loop
724            if column in columns:
725                savethiscolumndata = 1  # save the data for return
726            else:
727                savethiscolumndata = 0  # data only used for selection
728
729            try:
730                key, data = cur.set_range(searchkey)
731                while key[:len(searchkey)] == searchkey:
732                    # extract the rowid from the key
733                    rowid = key[-_rowid_str_len:]
734
735                    if not rowid in rejected_rowids:
736                        # if no condition was specified or the condition
737                        # succeeds, add row to our match list.
738                        if not condition or condition(data):
739                            if not rowid in matching_rowids:
740                                matching_rowids[rowid] = {}
741                            if savethiscolumndata:
742                                matching_rowids[rowid][column] = data
743                        else:
744                            if rowid in matching_rowids:
745                                del matching_rowids[rowid]
746                            rejected_rowids[rowid] = rowid
747
748                    key, data = cur.next()
749
750            except db.DBError, dberror:
751                if dberror.args[0] != db.DB_NOTFOUND:
752                    raise
753                continue
754
755        cur.close()
756
757        # we're done selecting rows, garbage collect the reject list
758        del rejected_rowids
759
760        # extract any remaining desired column data from the
761        # database for the matching rows.
762        if len(columns) > 0:
763            for rowid, rowdata in matching_rowids.items():
764                for column in columns:
765                    if column in rowdata:
766                        continue
767                    try:
768                        rowdata[column] = self.db.get(
769                            _data_key(table, column, rowid))
770                    except db.DBError, dberror:
771                        if sys.version_info < (2, 6) :
772                            if dberror[0] != db.DB_NOTFOUND:
773                                raise
774                        else :
775                            if dberror.args[0] != db.DB_NOTFOUND:
776                                raise
777                        rowdata[column] = None
778
779        # return the matches
780        return matching_rowids
781
782
783    def Drop(self, table):
784        """Remove an entire table from the database"""
785        txn = None
786        try:
787            txn = self.env.txn_begin()
788
789            # delete the column list
790            self.db.delete(_columns_key(table), txn=txn)
791
792            cur = self.db.cursor(txn)
793
794            # delete all keys containing this tables column and row info
795            table_key = _search_all_data_key(table)
796            while 1:
797                try:
798                    key, data = cur.set_range(table_key)
799                except db.DBNotFoundError:
800                    break
801                # only delete items in this table
802                if key[:len(table_key)] != table_key:
803                    break
804                cur.delete()
805
806            # delete all rowids used by this table
807            table_key = _search_rowid_key(table)
808            while 1:
809                try:
810                    key, data = cur.set_range(table_key)
811                except db.DBNotFoundError:
812                    break
813                # only delete items in this table
814                if key[:len(table_key)] != table_key:
815                    break
816                cur.delete()
817
818            cur.close()
819
820            # delete the tablename from the table name list
821            tablelist = pickle.loads(
822                getattr(self.db, "get_bytes", self.db.get)(_table_names_key,
823                    txn=txn, flags=db.DB_RMW))
824            try:
825                tablelist.remove(table)
826            except ValueError:
827                # hmm, it wasn't there, oh well, that's what we want.
828                pass
829            # delete 1st, incase we opened with DB_DUP
830            self.db.delete(_table_names_key, txn=txn)
831            getattr(self.db, "put_bytes", self.db.put)(_table_names_key,
832                    pickle.dumps(tablelist, 1), txn=txn)
833
834            txn.commit()
835            txn = None
836
837            if table in self.__tablecolumns:
838                del self.__tablecolumns[table]
839
840        except db.DBError, dberror:
841            if txn:
842                txn.abort()
843            raise TableDBError(dberror.args[1])
844