blob: efc6062164212600be690820561c7a857a6bf5f6 [file] [log] [blame]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +00001#-----------------------------------------------------------------------
2#
3# Copyright (C) 2000, 2001 by Autonomous Zone Industries
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +00004# Copyright (C) 2002 Gregory P. Smith
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +00005#
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@electricrain.com>
14
15# This provides a simple database table interface built on top of
16# the Python BerkeleyDB 3 interface.
17#
18_cvsid = '$Id$'
19
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000020import re
Barry Warsawf71de3e2003-01-28 17:20:44 +000021import sys
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000022import copy
Barry Warsawf71de3e2003-01-28 17:20:44 +000023import xdrlib
Tim Peters95334a52004-08-08 00:54:21 +000024import random
Barry Warsawf71de3e2003-01-28 17:20:44 +000025import cPickle as pickle
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000026
Barry Warsawf71de3e2003-01-28 17:20:44 +000027try:
Gregory P. Smith41631e82003-09-21 00:08:14 +000028 # For Pythons w/distutils pybsddb
29 from bsddb3.db import *
30except ImportError:
Barry Warsawf71de3e2003-01-28 17:20:44 +000031 # For Python 2.3
32 from bsddb.db import *
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000033
Thomas Wouters0e3f5912006-08-11 14:57:12 +000034# XXX(nnorwitz): is this correct? DBIncompleteError is conditional in _bsddb.c
35try:
36 DBIncompleteError
37except NameError:
38 class DBIncompleteError(Exception):
39 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000040
Barry Warsawf71de3e2003-01-28 17:20:44 +000041class TableDBError(StandardError):
42 pass
43class TableAlreadyExists(TableDBError):
44 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000045
46
47class Cond:
48 """This condition matches everything"""
49 def __call__(self, s):
50 return 1
51
52class ExactCond(Cond):
53 """Acts as an exact match condition function"""
54 def __init__(self, strtomatch):
55 self.strtomatch = strtomatch
56 def __call__(self, s):
57 return s == self.strtomatch
58
59class PrefixCond(Cond):
60 """Acts as a condition function for matching a string prefix"""
61 def __init__(self, prefix):
62 self.prefix = prefix
63 def __call__(self, s):
64 return s[:len(self.prefix)] == self.prefix
65
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +000066class PostfixCond(Cond):
67 """Acts as a condition function for matching a string postfix"""
68 def __init__(self, postfix):
69 self.postfix = postfix
70 def __call__(self, s):
71 return s[-len(self.postfix):] == self.postfix
72
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000073class LikeCond(Cond):
74 """
75 Acts as a function that will match using an SQL 'LIKE' style
76 string. Case insensitive and % signs are wild cards.
77 This isn't perfect but it should work for the simple common cases.
78 """
79 def __init__(self, likestr, re_flags=re.IGNORECASE):
80 # escape python re characters
81 chars_to_escape = '.*+()[]?'
82 for char in chars_to_escape :
Barry Warsawf71de3e2003-01-28 17:20:44 +000083 likestr = likestr.replace(char, '\\'+char)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000084 # convert %s to wildcards
Barry Warsawf71de3e2003-01-28 17:20:44 +000085 self.likestr = likestr.replace('%', '.*')
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000086 self.re = re.compile('^'+self.likestr+'$', re_flags)
87 def __call__(self, s):
88 return self.re.match(s)
89
90#
91# keys used to store database metadata
92#
93_table_names_key = '__TABLE_NAMES__' # list of the tables in this db
94_columns = '._COLUMNS__' # table_name+this key contains a list of columns
Barry Warsawf71de3e2003-01-28 17:20:44 +000095
96def _columns_key(table):
97 return table + _columns
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000098
99#
100# these keys are found within table sub databases
101#
102_data = '._DATA_.' # this+column+this+rowid key contains table data
103_rowid = '._ROWID_.' # this+rowid+this key contains a unique entry for each
104 # row in the table. (no data is stored)
105_rowid_str_len = 8 # length in bytes of the unique rowid strings
Barry Warsawf71de3e2003-01-28 17:20:44 +0000106
107def _data_key(table, col, rowid):
108 return table + _data + col + _data + rowid
109
110def _search_col_data_key(table, col):
111 return table + _data + col + _data
112
113def _search_all_data_key(table):
114 return table + _data
115
116def _rowid_key(table, rowid):
117 return table + _rowid + rowid + _rowid
118
119def _search_rowid_key(table):
120 return table + _rowid
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000121
122def contains_metastrings(s) :
123 """Verify that the given string does not contain any
124 metadata strings that might interfere with dbtables database operation.
125 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000126 if (s.find(_table_names_key) >= 0 or
127 s.find(_columns) >= 0 or
128 s.find(_data) >= 0 or
129 s.find(_rowid) >= 0):
130 # Then
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000131 return 1
Barry Warsawf71de3e2003-01-28 17:20:44 +0000132 else:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000133 return 0
134
135
136class bsdTableDB :
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000137 def __init__(self, filename, dbhome, create=0, truncate=0, mode=0600,
Barry Warsawf71de3e2003-01-28 17:20:44 +0000138 recover=0, dbflags=0):
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000139 """bsdTableDB(filename, dbhome, create=0, truncate=0, mode=0600)
140
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000141 Open database name in the dbhome BerkeleyDB directory.
142 Use keyword arguments when calling this constructor.
143 """
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000144 self.db = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000145 myflags = DB_THREAD
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000146 if create:
147 myflags |= DB_CREATE
148 flagsforenv = (DB_INIT_MPOOL | DB_INIT_LOCK | DB_INIT_LOG |
149 DB_INIT_TXN | dbflags)
150 # DB_AUTO_COMMIT isn't a valid flag for env.open()
151 try:
152 dbflags |= DB_AUTO_COMMIT
153 except AttributeError:
154 pass
155 if recover:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000156 flagsforenv = flagsforenv | DB_RECOVER
157 self.env = DBEnv()
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000158 # enable auto deadlock avoidance
159 self.env.set_lk_detect(DB_LOCK_DEFAULT)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000160 self.env.open(dbhome, myflags | flagsforenv)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000161 if truncate:
162 myflags |= DB_TRUNCATE
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000163 self.db = DB(self.env)
Gregory P. Smith455d46f2003-07-09 04:45:59 +0000164 # this code relies on DBCursor.set* methods to raise exceptions
165 # rather than returning None
166 self.db.set_get_returns_none(1)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000167 # allow duplicate entries [warning: be careful w/ metadata]
168 self.db.set_flags(DB_DUP)
169 self.db.open(filename, DB_BTREE, dbflags | myflags, mode)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000170 self.dbfilename = filename
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000171 # Initialize the table names list if this is a new database
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000172 txn = self.env.txn_begin()
173 try:
174 if not self.db.has_key(_table_names_key, txn):
175 self.db.put(_table_names_key, pickle.dumps([], 1), txn=txn)
176 # Yes, bare except
177 except:
178 txn.abort()
179 raise
180 else:
181 txn.commit()
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000182 # TODO verify more of the database's metadata?
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000183 self.__tablecolumns = {}
184
185 def __del__(self):
186 self.close()
187
188 def close(self):
189 if self.db is not None:
190 self.db.close()
191 self.db = None
192 if self.env is not None:
193 self.env.close()
194 self.env = None
195
196 def checkpoint(self, mins=0):
197 try:
198 self.env.txn_checkpoint(mins)
199 except DBIncompleteError:
200 pass
201
202 def sync(self):
203 try:
204 self.db.sync()
205 except DBIncompleteError:
206 pass
207
208 def _db_print(self) :
209 """Print the database to stdout for debugging"""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000210 print("******** Printing raw database for debugging ********")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000211 cur = self.db.cursor()
212 try:
213 key, data = cur.first()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000214 while 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000215 print(repr({key: data}))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000216 next = cur.next()
217 if next:
218 key, data = next
219 else:
220 cur.close()
221 return
222 except DBNotFoundError:
223 cur.close()
224
225
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000226 def CreateTable(self, table, columns):
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000227 """CreateTable(table, columns) - Create a new table in the database.
228
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000229 raises TableDBError if it already exists or for other DB errors.
230 """
Guido van Rossum13257902007-06-07 23:15:56 +0000231 assert isinstance(columns, list)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000232 txn = None
233 try:
234 # checking sanity of the table and column names here on
235 # table creation will prevent problems elsewhere.
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000236 if contains_metastrings(table):
237 raise ValueError(
238 "bad table name: contains reserved metastrings")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000239 for column in columns :
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000240 if contains_metastrings(column):
241 raise ValueError(
242 "bad column name: contains reserved metastrings")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000243
244 columnlist_key = _columns_key(table)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000245 if self.db.has_key(columnlist_key):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000246 raise TableAlreadyExists, "table already exists"
247
248 txn = self.env.txn_begin()
249 # store the table's column info
250 self.db.put(columnlist_key, pickle.dumps(columns, 1), txn=txn)
251
252 # add the table name to the tablelist
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000253 tablelist = pickle.loads(self.db.get(_table_names_key, txn=txn,
254 flags=DB_RMW))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000255 tablelist.append(table)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000256 # delete 1st, in case we opened with DB_DUP
257 self.db.delete(_table_names_key, txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000258 self.db.put(_table_names_key, pickle.dumps(tablelist, 1), txn=txn)
259
260 txn.commit()
261 txn = None
Guido van Rossumb940e112007-01-10 16:19:56 +0000262 except DBError as dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000263 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000264 txn.abort()
Guido van Rossumd84da1b2007-03-28 21:03:48 +0000265 raise TableDBError, dberror.args[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000266
267
268 def ListTableColumns(self, table):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000269 """Return a list of columns in the given table.
270 [] if the table doesn't exist.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000271 """
Guido van Rossum13257902007-06-07 23:15:56 +0000272 assert isinstance(table, str)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000273 if contains_metastrings(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000274 raise ValueError, "bad table name: contains reserved metastrings"
275
276 columnlist_key = _columns_key(table)
277 if not self.db.has_key(columnlist_key):
278 return []
279 pickledcolumnlist = self.db.get(columnlist_key)
280 if pickledcolumnlist:
281 return pickle.loads(pickledcolumnlist)
282 else:
283 return []
284
285 def ListTables(self):
286 """Return a list of tables in this database."""
287 pickledtablelist = self.db.get(_table_names_key)
288 if pickledtablelist:
289 return pickle.loads(pickledtablelist)
290 else:
291 return []
292
293 def CreateOrExtendTable(self, table, columns):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000294 """CreateOrExtendTable(table, columns)
295
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000296 Create a new table in the database.
297
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000298 If a table of this name already exists, extend it to have any
299 additional columns present in the given list as well as
300 all of its current columns.
301 """
Guido van Rossum13257902007-06-07 23:15:56 +0000302 assert isinstance(columns, list)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000303 try:
304 self.CreateTable(table, columns)
305 except TableAlreadyExists:
306 # the table already existed, add any new columns
307 txn = None
308 try:
309 columnlist_key = _columns_key(table)
310 txn = self.env.txn_begin()
311
312 # load the current column list
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000313 oldcolumnlist = pickle.loads(
314 self.db.get(columnlist_key, txn=txn, flags=DB_RMW))
315 # create a hash table for fast lookups of column names in the
316 # loop below
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000317 oldcolumnhash = {}
318 for c in oldcolumnlist:
319 oldcolumnhash[c] = c
320
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000321 # create a new column list containing both the old and new
322 # column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000323 newcolumnlist = copy.copy(oldcolumnlist)
324 for c in columns:
Guido van Rossum20435132006-08-21 00:21:47 +0000325 if c not in oldcolumnhash:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000326 newcolumnlist.append(c)
327
328 # store the table's new extended column list
329 if newcolumnlist != oldcolumnlist :
330 # delete the old one first since we opened with DB_DUP
331 self.db.delete(columnlist_key, txn)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000332 self.db.put(columnlist_key,
333 pickle.dumps(newcolumnlist, 1),
334 txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000335
336 txn.commit()
337 txn = None
338
339 self.__load_column_info(table)
Guido van Rossumb940e112007-01-10 16:19:56 +0000340 except DBError as dberror:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000341 if txn:
342 txn.abort()
Guido van Rossumd84da1b2007-03-28 21:03:48 +0000343 raise TableDBError, dberror.args[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000344
345
346 def __load_column_info(self, table) :
347 """initialize the self.__tablecolumns dict"""
348 # check the column names
349 try:
350 tcolpickles = self.db.get(_columns_key(table))
351 except DBNotFoundError:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000352 raise TableDBError, "unknown table: %r" % (table,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000353 if not tcolpickles:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000354 raise TableDBError, "unknown table: %r" % (table,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000355 self.__tablecolumns[table] = pickle.loads(tcolpickles)
356
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000357 def __new_rowid(self, table, txn) :
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000358 """Create a new unique row identifier"""
359 unique = 0
Barry Warsawf71de3e2003-01-28 17:20:44 +0000360 while not unique:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000361 # Generate a random 64-bit row ID string
362 # (note: this code has <64 bits of randomness
363 # but it's plenty for our database id needs!)
364 p = xdrlib.Packer()
Tim Peters95334a52004-08-08 00:54:21 +0000365 p.pack_int(int(random.random()*2147483647))
366 p.pack_int(int(random.random()*2147483647))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000367 newid = p.get_buffer()
368
369 # Guarantee uniqueness by adding this key to the database
370 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000371 self.db.put(_rowid_key(table, newid), None, txn=txn,
372 flags=DB_NOOVERWRITE)
373 except DBKeyExistError:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000374 pass
375 else:
376 unique = 1
377
378 return newid
379
380
381 def Insert(self, table, rowdict) :
382 """Insert(table, datadict) - Insert a new row into the table
383 using the keys+values from rowdict as the column values.
384 """
385 txn = None
386 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000387 if not self.db.has_key(_columns_key(table)):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000388 raise TableDBError, "unknown table"
389
390 # check the validity of each column name
Guido van Rossum20435132006-08-21 00:21:47 +0000391 if table not in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000392 self.__load_column_info(table)
393 for column in rowdict.keys() :
Barry Warsawf71de3e2003-01-28 17:20:44 +0000394 if not self.__tablecolumns[table].count(column):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000395 raise TableDBError, "unknown column: %r" % (column,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000396
397 # get a unique row identifier for this row
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000398 txn = self.env.txn_begin()
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000399 rowid = self.__new_rowid(table, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000400
401 # insert the row values into the table database
Barry Warsawf71de3e2003-01-28 17:20:44 +0000402 for column, dataitem in rowdict.items():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000403 # store the value
404 self.db.put(_data_key(table, column, rowid), dataitem, txn=txn)
405
406 txn.commit()
407 txn = None
408
Guido van Rossumb940e112007-01-10 16:19:56 +0000409 except DBError as dberror:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000410 # WIBNI we could just abort the txn and re-raise the exception?
411 # But no, because TableDBError is not related to DBError via
412 # inheritance, so it would be backwards incompatible. Do the next
413 # best thing.
414 info = sys.exc_info()
415 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000416 txn.abort()
417 self.db.delete(_rowid_key(table, rowid))
Guido van Rossumd84da1b2007-03-28 21:03:48 +0000418 raise TableDBError, dberror.args[1], info[2]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000419
420
Barry Warsawf71de3e2003-01-28 17:20:44 +0000421 def Modify(self, table, conditions={}, mappings={}):
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000422 """Modify(table, conditions={}, mappings={}) - Modify items in rows matching 'conditions' using mapping functions in 'mappings'
423
424 * table - the table name
425 * conditions - a dictionary keyed on column names containing
426 a condition callable expecting the data string as an
427 argument and returning a boolean.
428 * mappings - a dictionary keyed on column names containing a
429 condition callable expecting the data string as an argument and
430 returning the new string for that column.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000431 """
432 try:
433 matching_rowids = self.__Select(table, [], conditions)
434
435 # modify only requested columns
436 columns = mappings.keys()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000437 for rowid in matching_rowids.keys():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000438 txn = None
439 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000440 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000441 txn = self.env.txn_begin()
442 # modify the requested column
443 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000444 dataitem = self.db.get(
445 _data_key(table, column, rowid),
446 txn)
447 self.db.delete(
448 _data_key(table, column, rowid),
449 txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000450 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000451 # XXXXXXX row key somehow didn't exist, assume no
452 # error
453 dataitem = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000454 dataitem = mappings[column](dataitem)
Guido van Rossumb053cd82006-08-24 03:53:23 +0000455 if dataitem != None:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000456 self.db.put(
457 _data_key(table, column, rowid),
458 dataitem, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000459 txn.commit()
460 txn = None
461
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000462 # catch all exceptions here since we call unknown callables
463 except:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000464 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000465 txn.abort()
466 raise
467
Guido van Rossumb940e112007-01-10 16:19:56 +0000468 except DBError as dberror:
Guido van Rossumd84da1b2007-03-28 21:03:48 +0000469 raise TableDBError, dberror.args[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000470
Barry Warsawf71de3e2003-01-28 17:20:44 +0000471 def Delete(self, table, conditions={}):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000472 """Delete(table, conditions) - Delete items matching the given
473 conditions from the table.
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000474
475 * conditions - a dictionary keyed on column names containing
476 condition functions expecting the data string as an
477 argument and returning a boolean.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000478 """
479 try:
480 matching_rowids = self.__Select(table, [], conditions)
481
482 # delete row data from all columns
483 columns = self.__tablecolumns[table]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000484 for rowid in matching_rowids.keys():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000485 txn = None
486 try:
487 txn = self.env.txn_begin()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000488 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000489 # delete the data key
490 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000491 self.db.delete(_data_key(table, column, rowid),
492 txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000493 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000494 # XXXXXXX column may not exist, assume no error
495 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000496
497 try:
498 self.db.delete(_rowid_key(table, rowid), txn)
499 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000500 # XXXXXXX row key somehow didn't exist, assume no error
501 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000502 txn.commit()
503 txn = None
Guido van Rossumb940e112007-01-10 16:19:56 +0000504 except DBError as dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000505 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000506 txn.abort()
507 raise
Guido van Rossumb940e112007-01-10 16:19:56 +0000508 except DBError as dberror:
Guido van Rossumd84da1b2007-03-28 21:03:48 +0000509 raise TableDBError, dberror.args[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000510
511
Barry Warsawf71de3e2003-01-28 17:20:44 +0000512 def Select(self, table, columns, conditions={}):
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000513 """Select(table, columns, conditions) - retrieve specific row data
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000514 Returns a list of row column->value mapping dictionaries.
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000515
516 * columns - a list of which column data to return. If
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000517 columns is None, all columns will be returned.
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000518 * conditions - a dictionary keyed on column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000519 containing callable conditions expecting the data string as an
520 argument and returning a boolean.
521 """
522 try:
Guido van Rossum20435132006-08-21 00:21:47 +0000523 if table not in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000524 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000525 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000526 columns = self.__tablecolumns[table]
527 matching_rowids = self.__Select(table, columns, conditions)
Guido van Rossumb940e112007-01-10 16:19:56 +0000528 except DBError as dberror:
Guido van Rossumd84da1b2007-03-28 21:03:48 +0000529 raise TableDBError, dberror.args[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000530 # return the matches as a list of dictionaries
531 return matching_rowids.values()
532
533
Barry Warsawf71de3e2003-01-28 17:20:44 +0000534 def __Select(self, table, columns, conditions):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000535 """__Select() - Used to implement Select and Delete (above)
536 Returns a dictionary keyed on rowids containing dicts
537 holding the row data for columns listed in the columns param
538 that match the given conditions.
539 * conditions is a dictionary keyed on column names
540 containing callable conditions expecting the data string as an
541 argument and returning a boolean.
542 """
543 # check the validity of each column name
Guido van Rossum20435132006-08-21 00:21:47 +0000544 if table not in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000545 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000546 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000547 columns = self.tablecolumns[table]
Brett Cannon0072e432007-02-22 06:40:59 +0000548 for column in (columns + list(conditions.keys())):
Barry Warsawf71de3e2003-01-28 17:20:44 +0000549 if not self.__tablecolumns[table].count(column):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000550 raise TableDBError, "unknown column: %r" % (column,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000551
552 # keyed on rows that match so far, containings dicts keyed on
553 # column names containing the data for that row and column.
554 matching_rowids = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000555 # keys are rowids that do not match
556 rejected_rowids = {}
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000557
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000558 # attempt to sort the conditions in such a way as to minimize full
559 # column lookups
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000560 def cmp_conditions(atuple, btuple):
561 a = atuple[1]
562 b = btuple[1]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000563 if type(a) is type(b):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000564 if isinstance(a, PrefixCond) and isinstance(b, PrefixCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000565 # longest prefix first
566 return cmp(len(b.prefix), len(a.prefix))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000567 if isinstance(a, LikeCond) and isinstance(b, LikeCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000568 # longest likestr first
569 return cmp(len(b.likestr), len(a.likestr))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000570 return 0
571 if isinstance(a, ExactCond):
572 return -1
573 if isinstance(b, ExactCond):
574 return 1
575 if isinstance(a, PrefixCond):
576 return -1
577 if isinstance(b, PrefixCond):
578 return 1
579 # leave all unknown condition callables alone as equals
580 return 0
581
Brett Cannon0072e432007-02-22 06:40:59 +0000582 conditionlist = list(conditions.items())
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000583 conditionlist.sort(cmp_conditions)
584
585 # Apply conditions to column data to find what we want
586 cur = self.db.cursor()
587 column_num = -1
Barry Warsawf71de3e2003-01-28 17:20:44 +0000588 for column, condition in conditionlist:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000589 column_num = column_num + 1
590 searchkey = _search_col_data_key(table, column)
591 # speedup: don't linear search columns within loop
Barry Warsawf71de3e2003-01-28 17:20:44 +0000592 if column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000593 savethiscolumndata = 1 # save the data for return
Barry Warsawf71de3e2003-01-28 17:20:44 +0000594 else:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000595 savethiscolumndata = 0 # data only used for selection
596
597 try:
598 key, data = cur.set_range(searchkey)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000599 while key[:len(searchkey)] == searchkey:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000600 # extract the rowid from the key
601 rowid = key[-_rowid_str_len:]
602
Guido van Rossum20435132006-08-21 00:21:47 +0000603 if rowid not in rejected_rowids:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000604 # if no condition was specified or the condition
605 # succeeds, add row to our match list.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000606 if not condition or condition(data):
Guido van Rossum20435132006-08-21 00:21:47 +0000607 if rowid not in matching_rowids:
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000608 matching_rowids[rowid] = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000609 if savethiscolumndata:
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000610 matching_rowids[rowid][column] = data
Barry Warsawf71de3e2003-01-28 17:20:44 +0000611 else:
Guido van Rossum20435132006-08-21 00:21:47 +0000612 if rowid in matching_rowids:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000613 del matching_rowids[rowid]
614 rejected_rowids[rowid] = rowid
615
616 key, data = cur.next()
617
Guido van Rossumb940e112007-01-10 16:19:56 +0000618 except DBError as dberror:
Guido van Rossumd84da1b2007-03-28 21:03:48 +0000619 if dberror.args[0] != DB_NOTFOUND:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000620 raise
621 continue
622
623 cur.close()
624
625 # we're done selecting rows, garbage collect the reject list
626 del rejected_rowids
627
628 # extract any remaining desired column data from the
629 # database for the matching rows.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000630 if len(columns) > 0:
631 for rowid, rowdata in matching_rowids.items():
632 for column in columns:
Guido van Rossum20435132006-08-21 00:21:47 +0000633 if column in rowdata:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000634 continue
635 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000636 rowdata[column] = self.db.get(
637 _data_key(table, column, rowid))
Guido van Rossumb940e112007-01-10 16:19:56 +0000638 except DBError as dberror:
Guido van Rossumd84da1b2007-03-28 21:03:48 +0000639 if dberror.args[0] != DB_NOTFOUND:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000640 raise
641 rowdata[column] = None
642
643 # return the matches
644 return matching_rowids
645
646
Barry Warsawf71de3e2003-01-28 17:20:44 +0000647 def Drop(self, table):
648 """Remove an entire table from the database"""
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000649 txn = None
650 try:
651 txn = self.env.txn_begin()
652
653 # delete the column list
654 self.db.delete(_columns_key(table), txn)
655
656 cur = self.db.cursor(txn)
657
658 # delete all keys containing this tables column and row info
659 table_key = _search_all_data_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000660 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000661 try:
662 key, data = cur.set_range(table_key)
663 except DBNotFoundError:
664 break
665 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000666 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000667 break
668 cur.delete()
669
670 # delete all rowids used by this table
671 table_key = _search_rowid_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000672 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000673 try:
674 key, data = cur.set_range(table_key)
675 except DBNotFoundError:
676 break
677 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000678 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000679 break
680 cur.delete()
681
682 cur.close()
683
684 # delete the tablename from the table name list
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000685 tablelist = pickle.loads(
686 self.db.get(_table_names_key, txn=txn, flags=DB_RMW))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000687 try:
688 tablelist.remove(table)
689 except ValueError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000690 # hmm, it wasn't there, oh well, that's what we want.
691 pass
692 # delete 1st, incase we opened with DB_DUP
693 self.db.delete(_table_names_key, txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000694 self.db.put(_table_names_key, pickle.dumps(tablelist, 1), txn=txn)
695
696 txn.commit()
697 txn = None
698
Guido van Rossum20435132006-08-21 00:21:47 +0000699 if table in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000700 del self.__tablecolumns[table]
701
Guido van Rossumb940e112007-01-10 16:19:56 +0000702 except DBError as dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000703 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000704 txn.abort()
Guido van Rossumd84da1b2007-03-28 21:03:48 +0000705 raise TableDBError, dberror.args[1]