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