blob: 369db43abe9d3bd13ab9f5033f99e343d477464e [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 +000025from types import ListType, StringType
26import cPickle as pickle
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000027
Barry Warsawf71de3e2003-01-28 17:20:44 +000028try:
Gregory P. Smith41631e82003-09-21 00:08:14 +000029 # For Pythons w/distutils pybsddb
30 from bsddb3.db import *
31except ImportError:
Barry Warsawf71de3e2003-01-28 17:20:44 +000032 # For Python 2.3
33 from bsddb.db import *
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000034
35
Barry Warsawf71de3e2003-01-28 17:20:44 +000036class TableDBError(StandardError):
37 pass
38class TableAlreadyExists(TableDBError):
39 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000040
41
42class Cond:
43 """This condition matches everything"""
44 def __call__(self, s):
45 return 1
46
47class ExactCond(Cond):
48 """Acts as an exact match condition function"""
49 def __init__(self, strtomatch):
50 self.strtomatch = strtomatch
51 def __call__(self, s):
52 return s == self.strtomatch
53
54class PrefixCond(Cond):
55 """Acts as a condition function for matching a string prefix"""
56 def __init__(self, prefix):
57 self.prefix = prefix
58 def __call__(self, s):
59 return s[:len(self.prefix)] == self.prefix
60
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +000061class PostfixCond(Cond):
62 """Acts as a condition function for matching a string postfix"""
63 def __init__(self, postfix):
64 self.postfix = postfix
65 def __call__(self, s):
66 return s[-len(self.postfix):] == self.postfix
67
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000068class LikeCond(Cond):
69 """
70 Acts as a function that will match using an SQL 'LIKE' style
71 string. Case insensitive and % signs are wild cards.
72 This isn't perfect but it should work for the simple common cases.
73 """
74 def __init__(self, likestr, re_flags=re.IGNORECASE):
75 # escape python re characters
76 chars_to_escape = '.*+()[]?'
77 for char in chars_to_escape :
Barry Warsawf71de3e2003-01-28 17:20:44 +000078 likestr = likestr.replace(char, '\\'+char)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000079 # convert %s to wildcards
Barry Warsawf71de3e2003-01-28 17:20:44 +000080 self.likestr = likestr.replace('%', '.*')
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000081 self.re = re.compile('^'+self.likestr+'$', re_flags)
82 def __call__(self, s):
83 return self.re.match(s)
84
85#
86# keys used to store database metadata
87#
88_table_names_key = '__TABLE_NAMES__' # list of the tables in this db
89_columns = '._COLUMNS__' # table_name+this key contains a list of columns
Barry Warsawf71de3e2003-01-28 17:20:44 +000090
91def _columns_key(table):
92 return table + _columns
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000093
94#
95# these keys are found within table sub databases
96#
97_data = '._DATA_.' # this+column+this+rowid key contains table data
98_rowid = '._ROWID_.' # this+rowid+this key contains a unique entry for each
99 # row in the table. (no data is stored)
100_rowid_str_len = 8 # length in bytes of the unique rowid strings
Barry Warsawf71de3e2003-01-28 17:20:44 +0000101
102def _data_key(table, col, rowid):
103 return table + _data + col + _data + rowid
104
105def _search_col_data_key(table, col):
106 return table + _data + col + _data
107
108def _search_all_data_key(table):
109 return table + _data
110
111def _rowid_key(table, rowid):
112 return table + _rowid + rowid + _rowid
113
114def _search_rowid_key(table):
115 return table + _rowid
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000116
117def contains_metastrings(s) :
118 """Verify that the given string does not contain any
119 metadata strings that might interfere with dbtables database operation.
120 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000121 if (s.find(_table_names_key) >= 0 or
122 s.find(_columns) >= 0 or
123 s.find(_data) >= 0 or
124 s.find(_rowid) >= 0):
125 # Then
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000126 return 1
Barry Warsawf71de3e2003-01-28 17:20:44 +0000127 else:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000128 return 0
129
130
131class bsdTableDB :
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000132 def __init__(self, filename, dbhome, create=0, truncate=0, mode=0600,
Barry Warsawf71de3e2003-01-28 17:20:44 +0000133 recover=0, dbflags=0):
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000134 """bsdTableDB(filename, dbhome, create=0, truncate=0, mode=0600)
135
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000136 Open database name in the dbhome BerkeleyDB directory.
137 Use keyword arguments when calling this constructor.
138 """
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000139 self.db = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000140 myflags = DB_THREAD
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000141 if create:
142 myflags |= DB_CREATE
143 flagsforenv = (DB_INIT_MPOOL | DB_INIT_LOCK | DB_INIT_LOG |
144 DB_INIT_TXN | dbflags)
145 # DB_AUTO_COMMIT isn't a valid flag for env.open()
146 try:
147 dbflags |= DB_AUTO_COMMIT
148 except AttributeError:
149 pass
150 if recover:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000151 flagsforenv = flagsforenv | DB_RECOVER
152 self.env = DBEnv()
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000153 # enable auto deadlock avoidance
154 self.env.set_lk_detect(DB_LOCK_DEFAULT)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000155 self.env.open(dbhome, myflags | flagsforenv)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000156 if truncate:
157 myflags |= DB_TRUNCATE
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000158 self.db = DB(self.env)
Gregory P. Smith455d46f2003-07-09 04:45:59 +0000159 # this code relies on DBCursor.set* methods to raise exceptions
160 # rather than returning None
161 self.db.set_get_returns_none(1)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000162 # allow duplicate entries [warning: be careful w/ metadata]
163 self.db.set_flags(DB_DUP)
164 self.db.open(filename, DB_BTREE, dbflags | myflags, mode)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000165 self.dbfilename = filename
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000166 # Initialize the table names list if this is a new database
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000167 txn = self.env.txn_begin()
168 try:
169 if not self.db.has_key(_table_names_key, txn):
170 self.db.put(_table_names_key, pickle.dumps([], 1), txn=txn)
171 # Yes, bare except
172 except:
173 txn.abort()
174 raise
175 else:
176 txn.commit()
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000177 # TODO verify more of the database's metadata?
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000178 self.__tablecolumns = {}
179
180 def __del__(self):
181 self.close()
182
183 def close(self):
184 if self.db is not None:
185 self.db.close()
186 self.db = None
187 if self.env is not None:
188 self.env.close()
189 self.env = None
190
191 def checkpoint(self, mins=0):
192 try:
193 self.env.txn_checkpoint(mins)
194 except DBIncompleteError:
195 pass
196
197 def sync(self):
198 try:
199 self.db.sync()
200 except DBIncompleteError:
201 pass
202
203 def _db_print(self) :
204 """Print the database to stdout for debugging"""
205 print "******** Printing raw database for debugging ********"
206 cur = self.db.cursor()
207 try:
208 key, data = cur.first()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000209 while 1:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000210 print repr({key: data})
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000211 next = cur.next()
212 if next:
213 key, data = next
214 else:
215 cur.close()
216 return
217 except DBNotFoundError:
218 cur.close()
219
220
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000221 def CreateTable(self, table, columns):
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000222 """CreateTable(table, columns) - Create a new table in the database.
223
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000224 raises TableDBError if it already exists or for other DB errors.
225 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000226 assert isinstance(columns, ListType)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000227 txn = None
228 try:
229 # checking sanity of the table and column names here on
230 # table creation will prevent problems elsewhere.
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000231 if contains_metastrings(table):
232 raise ValueError(
233 "bad table name: contains reserved metastrings")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000234 for column in columns :
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000235 if contains_metastrings(column):
236 raise ValueError(
237 "bad column name: contains reserved metastrings")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000238
239 columnlist_key = _columns_key(table)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000240 if self.db.has_key(columnlist_key):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000241 raise TableAlreadyExists, "table already exists"
242
243 txn = self.env.txn_begin()
244 # store the table's column info
245 self.db.put(columnlist_key, pickle.dumps(columns, 1), txn=txn)
246
247 # add the table name to the tablelist
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000248 tablelist = pickle.loads(self.db.get(_table_names_key, txn=txn,
249 flags=DB_RMW))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000250 tablelist.append(table)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000251 # delete 1st, in case we opened with DB_DUP
252 self.db.delete(_table_names_key, txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000253 self.db.put(_table_names_key, pickle.dumps(tablelist, 1), txn=txn)
254
255 txn.commit()
256 txn = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000257 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000258 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000259 txn.abort()
260 raise TableDBError, dberror[1]
261
262
263 def ListTableColumns(self, table):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000264 """Return a list of columns in the given table.
265 [] if the table doesn't exist.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000266 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000267 assert isinstance(table, StringType)
268 if contains_metastrings(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000269 raise ValueError, "bad table name: contains reserved metastrings"
270
271 columnlist_key = _columns_key(table)
272 if not self.db.has_key(columnlist_key):
273 return []
274 pickledcolumnlist = self.db.get(columnlist_key)
275 if pickledcolumnlist:
276 return pickle.loads(pickledcolumnlist)
277 else:
278 return []
279
280 def ListTables(self):
281 """Return a list of tables in this database."""
282 pickledtablelist = self.db.get(_table_names_key)
283 if pickledtablelist:
284 return pickle.loads(pickledtablelist)
285 else:
286 return []
287
288 def CreateOrExtendTable(self, table, columns):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000289 """CreateOrExtendTable(table, columns)
290
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000291 Create a new table in the database.
292
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000293 If a table of this name already exists, extend it to have any
294 additional columns present in the given list as well as
295 all of its current columns.
296 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000297 assert isinstance(columns, ListType)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000298 try:
299 self.CreateTable(table, columns)
300 except TableAlreadyExists:
301 # the table already existed, add any new columns
302 txn = None
303 try:
304 columnlist_key = _columns_key(table)
305 txn = self.env.txn_begin()
306
307 # load the current column list
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000308 oldcolumnlist = pickle.loads(
309 self.db.get(columnlist_key, txn=txn, flags=DB_RMW))
310 # create a hash table for fast lookups of column names in the
311 # loop below
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000312 oldcolumnhash = {}
313 for c in oldcolumnlist:
314 oldcolumnhash[c] = c
315
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000316 # create a new column list containing both the old and new
317 # column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000318 newcolumnlist = copy.copy(oldcolumnlist)
319 for c in columns:
320 if not oldcolumnhash.has_key(c):
321 newcolumnlist.append(c)
322
323 # store the table's new extended column list
324 if newcolumnlist != oldcolumnlist :
325 # delete the old one first since we opened with DB_DUP
326 self.db.delete(columnlist_key, txn)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000327 self.db.put(columnlist_key,
328 pickle.dumps(newcolumnlist, 1),
329 txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000330
331 txn.commit()
332 txn = None
333
334 self.__load_column_info(table)
335 except DBError, dberror:
336 if txn:
337 txn.abort()
338 raise TableDBError, dberror[1]
339
340
341 def __load_column_info(self, table) :
342 """initialize the self.__tablecolumns dict"""
343 # check the column names
344 try:
345 tcolpickles = self.db.get(_columns_key(table))
346 except DBNotFoundError:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000347 raise TableDBError, "unknown table: %r" % (table,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000348 if not tcolpickles:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000349 raise TableDBError, "unknown table: %r" % (table,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000350 self.__tablecolumns[table] = pickle.loads(tcolpickles)
351
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000352 def __new_rowid(self, table, txn) :
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000353 """Create a new unique row identifier"""
354 unique = 0
Barry Warsawf71de3e2003-01-28 17:20:44 +0000355 while not unique:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000356 # Generate a random 64-bit row ID string
357 # (note: this code has <64 bits of randomness
358 # but it's plenty for our database id needs!)
359 p = xdrlib.Packer()
Tim Peters95334a52004-08-08 00:54:21 +0000360 p.pack_int(int(random.random()*2147483647))
361 p.pack_int(int(random.random()*2147483647))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000362 newid = p.get_buffer()
363
364 # Guarantee uniqueness by adding this key to the database
365 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000366 self.db.put(_rowid_key(table, newid), None, txn=txn,
367 flags=DB_NOOVERWRITE)
368 except DBKeyExistError:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000369 pass
370 else:
371 unique = 1
372
373 return newid
374
375
376 def Insert(self, table, rowdict) :
377 """Insert(table, datadict) - Insert a new row into the table
378 using the keys+values from rowdict as the column values.
379 """
380 txn = None
381 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000382 if not self.db.has_key(_columns_key(table)):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000383 raise TableDBError, "unknown table"
384
385 # check the validity of each column name
Barry Warsawf71de3e2003-01-28 17:20:44 +0000386 if not self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000387 self.__load_column_info(table)
388 for column in rowdict.keys() :
Barry Warsawf71de3e2003-01-28 17:20:44 +0000389 if not self.__tablecolumns[table].count(column):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000390 raise TableDBError, "unknown column: %r" % (column,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000391
392 # get a unique row identifier for this row
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000393 txn = self.env.txn_begin()
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000394 rowid = self.__new_rowid(table, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000395
396 # insert the row values into the table database
Barry Warsawf71de3e2003-01-28 17:20:44 +0000397 for column, dataitem in rowdict.items():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000398 # store the value
399 self.db.put(_data_key(table, column, rowid), dataitem, txn=txn)
400
401 txn.commit()
402 txn = None
403
404 except DBError, dberror:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000405 # WIBNI we could just abort the txn and re-raise the exception?
406 # But no, because TableDBError is not related to DBError via
407 # inheritance, so it would be backwards incompatible. Do the next
408 # best thing.
409 info = sys.exc_info()
410 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000411 txn.abort()
412 self.db.delete(_rowid_key(table, rowid))
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000413 raise TableDBError, dberror[1], info[2]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000414
415
Barry Warsawf71de3e2003-01-28 17:20:44 +0000416 def Modify(self, table, conditions={}, mappings={}):
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000417 """Modify(table, conditions={}, mappings={}) - Modify items in rows matching 'conditions' using mapping functions in 'mappings'
418
419 * table - the table name
420 * conditions - a dictionary keyed on column names containing
421 a condition callable expecting the data string as an
422 argument and returning a boolean.
423 * mappings - a dictionary keyed on column names containing a
424 condition callable expecting the data string as an argument and
425 returning the new string for that column.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000426 """
427 try:
428 matching_rowids = self.__Select(table, [], conditions)
429
430 # modify only requested columns
431 columns = mappings.keys()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000432 for rowid in matching_rowids.keys():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000433 txn = None
434 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000435 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000436 txn = self.env.txn_begin()
437 # modify the requested column
438 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000439 dataitem = self.db.get(
440 _data_key(table, column, rowid),
441 txn)
442 self.db.delete(
443 _data_key(table, column, rowid),
444 txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000445 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000446 # XXXXXXX row key somehow didn't exist, assume no
447 # error
448 dataitem = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000449 dataitem = mappings[column](dataitem)
450 if dataitem <> None:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000451 self.db.put(
452 _data_key(table, column, rowid),
453 dataitem, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000454 txn.commit()
455 txn = None
456
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000457 # catch all exceptions here since we call unknown callables
458 except:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000459 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000460 txn.abort()
461 raise
462
463 except DBError, dberror:
464 raise TableDBError, dberror[1]
465
Barry Warsawf71de3e2003-01-28 17:20:44 +0000466 def Delete(self, table, conditions={}):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000467 """Delete(table, conditions) - Delete items matching the given
468 conditions from the table.
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000469
470 * conditions - a dictionary keyed on column names containing
471 condition functions expecting the data string as an
472 argument and returning a boolean.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000473 """
474 try:
475 matching_rowids = self.__Select(table, [], conditions)
476
477 # delete row data from all columns
478 columns = self.__tablecolumns[table]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000479 for rowid in matching_rowids.keys():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000480 txn = None
481 try:
482 txn = self.env.txn_begin()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000483 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000484 # delete the data key
485 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000486 self.db.delete(_data_key(table, column, rowid),
487 txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000488 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000489 # XXXXXXX column may not exist, assume no error
490 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000491
492 try:
493 self.db.delete(_rowid_key(table, rowid), txn)
494 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000495 # XXXXXXX row key somehow didn't exist, assume no error
496 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000497 txn.commit()
498 txn = None
499 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000500 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000501 txn.abort()
502 raise
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000503 except DBError, dberror:
504 raise TableDBError, dberror[1]
505
506
Barry Warsawf71de3e2003-01-28 17:20:44 +0000507 def Select(self, table, columns, conditions={}):
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000508 """Select(table, columns, conditions) - retrieve specific row data
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000509 Returns a list of row column->value mapping dictionaries.
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000510
511 * columns - a list of which column data to return. If
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000512 columns is None, all columns will be returned.
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000513 * conditions - a dictionary keyed on column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000514 containing callable conditions expecting the data string as an
515 argument and returning a boolean.
516 """
517 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000518 if not self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000519 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000520 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000521 columns = self.__tablecolumns[table]
522 matching_rowids = self.__Select(table, columns, conditions)
523 except DBError, dberror:
524 raise TableDBError, dberror[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000525 # return the matches as a list of dictionaries
526 return matching_rowids.values()
527
528
Barry Warsawf71de3e2003-01-28 17:20:44 +0000529 def __Select(self, table, columns, conditions):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000530 """__Select() - Used to implement Select and Delete (above)
531 Returns a dictionary keyed on rowids containing dicts
532 holding the row data for columns listed in the columns param
533 that match the given conditions.
534 * conditions is a dictionary keyed on column names
535 containing callable conditions expecting the data string as an
536 argument and returning a boolean.
537 """
538 # check the validity of each column name
Barry Warsawf71de3e2003-01-28 17:20:44 +0000539 if not self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000540 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000541 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000542 columns = self.tablecolumns[table]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000543 for column in (columns + conditions.keys()):
544 if not self.__tablecolumns[table].count(column):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000545 raise TableDBError, "unknown column: %r" % (column,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000546
547 # keyed on rows that match so far, containings dicts keyed on
548 # column names containing the data for that row and column.
549 matching_rowids = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000550 # keys are rowids that do not match
551 rejected_rowids = {}
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000552
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000553 # attempt to sort the conditions in such a way as to minimize full
554 # column lookups
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000555 def cmp_conditions(atuple, btuple):
556 a = atuple[1]
557 b = btuple[1]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000558 if type(a) is type(b):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000559 if isinstance(a, PrefixCond) and isinstance(b, PrefixCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000560 # longest prefix first
561 return cmp(len(b.prefix), len(a.prefix))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000562 if isinstance(a, LikeCond) and isinstance(b, LikeCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000563 # longest likestr first
564 return cmp(len(b.likestr), len(a.likestr))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000565 return 0
566 if isinstance(a, ExactCond):
567 return -1
568 if isinstance(b, ExactCond):
569 return 1
570 if isinstance(a, PrefixCond):
571 return -1
572 if isinstance(b, PrefixCond):
573 return 1
574 # leave all unknown condition callables alone as equals
575 return 0
576
577 conditionlist = conditions.items()
578 conditionlist.sort(cmp_conditions)
579
580 # Apply conditions to column data to find what we want
581 cur = self.db.cursor()
582 column_num = -1
Barry Warsawf71de3e2003-01-28 17:20:44 +0000583 for column, condition in conditionlist:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000584 column_num = column_num + 1
585 searchkey = _search_col_data_key(table, column)
586 # speedup: don't linear search columns within loop
Barry Warsawf71de3e2003-01-28 17:20:44 +0000587 if column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000588 savethiscolumndata = 1 # save the data for return
Barry Warsawf71de3e2003-01-28 17:20:44 +0000589 else:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000590 savethiscolumndata = 0 # data only used for selection
591
592 try:
593 key, data = cur.set_range(searchkey)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000594 while key[:len(searchkey)] == searchkey:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000595 # extract the rowid from the key
596 rowid = key[-_rowid_str_len:]
597
Barry Warsawf71de3e2003-01-28 17:20:44 +0000598 if not rejected_rowids.has_key(rowid):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000599 # if no condition was specified or the condition
600 # succeeds, add row to our match list.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000601 if not condition or condition(data):
602 if not matching_rowids.has_key(rowid):
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000603 matching_rowids[rowid] = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000604 if savethiscolumndata:
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000605 matching_rowids[rowid][column] = data
Barry Warsawf71de3e2003-01-28 17:20:44 +0000606 else:
607 if matching_rowids.has_key(rowid):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000608 del matching_rowids[rowid]
609 rejected_rowids[rowid] = rowid
610
611 key, data = cur.next()
612
613 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000614 if dberror[0] != DB_NOTFOUND:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000615 raise
616 continue
617
618 cur.close()
619
620 # we're done selecting rows, garbage collect the reject list
621 del rejected_rowids
622
623 # extract any remaining desired column data from the
624 # database for the matching rows.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000625 if len(columns) > 0:
626 for rowid, rowdata in matching_rowids.items():
627 for column in columns:
628 if rowdata.has_key(column):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000629 continue
630 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000631 rowdata[column] = self.db.get(
632 _data_key(table, column, rowid))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000633 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000634 if dberror[0] != DB_NOTFOUND:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000635 raise
636 rowdata[column] = None
637
638 # return the matches
639 return matching_rowids
640
641
Barry Warsawf71de3e2003-01-28 17:20:44 +0000642 def Drop(self, table):
643 """Remove an entire table from the database"""
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000644 txn = None
645 try:
646 txn = self.env.txn_begin()
647
648 # delete the column list
649 self.db.delete(_columns_key(table), txn)
650
651 cur = self.db.cursor(txn)
652
653 # delete all keys containing this tables column and row info
654 table_key = _search_all_data_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000655 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000656 try:
657 key, data = cur.set_range(table_key)
658 except DBNotFoundError:
659 break
660 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000661 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000662 break
663 cur.delete()
664
665 # delete all rowids used by this table
666 table_key = _search_rowid_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000667 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000668 try:
669 key, data = cur.set_range(table_key)
670 except DBNotFoundError:
671 break
672 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000673 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000674 break
675 cur.delete()
676
677 cur.close()
678
679 # delete the tablename from the table name list
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000680 tablelist = pickle.loads(
681 self.db.get(_table_names_key, txn=txn, flags=DB_RMW))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000682 try:
683 tablelist.remove(table)
684 except ValueError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000685 # hmm, it wasn't there, oh well, that's what we want.
686 pass
687 # delete 1st, incase we opened with DB_DUP
688 self.db.delete(_table_names_key, txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000689 self.db.put(_table_names_key, pickle.dumps(tablelist, 1), txn=txn)
690
691 txn.commit()
692 txn = None
693
Barry Warsawf71de3e2003-01-28 17:20:44 +0000694 if self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000695 del self.__tablecolumns[table]
696
697 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000698 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000699 txn.abort()
700 raise TableDBError, dberror[1]