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