blob: daf6a9e462586c6e5cde730265d70ae2eaab0e0d [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#
Gregory P. Smithf8057852007-09-09 20:25:00 +000013# -- Gregory P. Smith <greg@krypto.org>
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000014
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
Tim Peters95334a52004-08-08 00:54:21 +000023import random
Gregory P. Smithafed3a42007-10-18 07:56:54 +000024import struct
25import base64
Barry Warsawf71de3e2003-01-28 17:20:44 +000026from types import ListType, StringType
27import cPickle as pickle
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000028
Barry Warsawf71de3e2003-01-28 17:20:44 +000029try:
Gregory P. Smith41631e82003-09-21 00:08:14 +000030 # For Pythons w/distutils pybsddb
31 from bsddb3.db import *
32except ImportError:
Barry Warsawf71de3e2003-01-28 17:20:44 +000033 # For Python 2.3
34 from bsddb.db import *
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000035
Neal Norwitz6aaccc62006-06-11 08:35:14 +000036# XXX(nnorwitz): is this correct? DBIncompleteError is conditional in _bsddb.c
37try:
38 DBIncompleteError
39except NameError:
40 class DBIncompleteError(Exception):
41 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000042
Barry Warsawf71de3e2003-01-28 17:20:44 +000043class TableDBError(StandardError):
44 pass
45class TableAlreadyExists(TableDBError):
46 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000047
48
49class Cond:
50 """This condition matches everything"""
51 def __call__(self, s):
52 return 1
53
54class ExactCond(Cond):
55 """Acts as an exact match condition function"""
56 def __init__(self, strtomatch):
57 self.strtomatch = strtomatch
58 def __call__(self, s):
59 return s == self.strtomatch
60
61class PrefixCond(Cond):
62 """Acts as a condition function for matching a string prefix"""
63 def __init__(self, prefix):
64 self.prefix = prefix
65 def __call__(self, s):
66 return s[:len(self.prefix)] == self.prefix
67
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +000068class PostfixCond(Cond):
69 """Acts as a condition function for matching a string postfix"""
70 def __init__(self, postfix):
71 self.postfix = postfix
72 def __call__(self, s):
73 return s[-len(self.postfix):] == self.postfix
74
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000075class LikeCond(Cond):
76 """
77 Acts as a function that will match using an SQL 'LIKE' style
78 string. Case insensitive and % signs are wild cards.
79 This isn't perfect but it should work for the simple common cases.
80 """
81 def __init__(self, likestr, re_flags=re.IGNORECASE):
82 # escape python re characters
83 chars_to_escape = '.*+()[]?'
84 for char in chars_to_escape :
Barry Warsawf71de3e2003-01-28 17:20:44 +000085 likestr = likestr.replace(char, '\\'+char)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000086 # convert %s to wildcards
Barry Warsawf71de3e2003-01-28 17:20:44 +000087 self.likestr = likestr.replace('%', '.*')
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000088 self.re = re.compile('^'+self.likestr+'$', re_flags)
89 def __call__(self, s):
90 return self.re.match(s)
91
92#
93# keys used to store database metadata
94#
95_table_names_key = '__TABLE_NAMES__' # list of the tables in this db
96_columns = '._COLUMNS__' # table_name+this key contains a list of columns
Barry Warsawf71de3e2003-01-28 17:20:44 +000097
98def _columns_key(table):
99 return table + _columns
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000100
101#
102# these keys are found within table sub databases
103#
104_data = '._DATA_.' # this+column+this+rowid key contains table data
105_rowid = '._ROWID_.' # this+rowid+this key contains a unique entry for each
106 # row in the table. (no data is stored)
107_rowid_str_len = 8 # length in bytes of the unique rowid strings
Barry Warsawf71de3e2003-01-28 17:20:44 +0000108
109def _data_key(table, col, rowid):
110 return table + _data + col + _data + rowid
111
112def _search_col_data_key(table, col):
113 return table + _data + col + _data
114
115def _search_all_data_key(table):
116 return table + _data
117
118def _rowid_key(table, rowid):
119 return table + _rowid + rowid + _rowid
120
121def _search_rowid_key(table):
122 return table + _rowid
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000123
124def contains_metastrings(s) :
125 """Verify that the given string does not contain any
126 metadata strings that might interfere with dbtables database operation.
127 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000128 if (s.find(_table_names_key) >= 0 or
129 s.find(_columns) >= 0 or
130 s.find(_data) >= 0 or
131 s.find(_rowid) >= 0):
132 # Then
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000133 return 1
Barry Warsawf71de3e2003-01-28 17:20:44 +0000134 else:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000135 return 0
136
137
138class bsdTableDB :
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000139 def __init__(self, filename, dbhome, create=0, truncate=0, mode=0600,
Barry Warsawf71de3e2003-01-28 17:20:44 +0000140 recover=0, dbflags=0):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000141 """bsdTableDB(filename, dbhome, create=0, truncate=0, mode=0600)
142
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000143 Open database name in the dbhome BerkeleyDB directory.
144 Use keyword arguments when calling this constructor.
145 """
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000146 self.db = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000147 myflags = DB_THREAD
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000148 if create:
149 myflags |= DB_CREATE
150 flagsforenv = (DB_INIT_MPOOL | DB_INIT_LOCK | DB_INIT_LOG |
151 DB_INIT_TXN | dbflags)
152 # DB_AUTO_COMMIT isn't a valid flag for env.open()
153 try:
154 dbflags |= DB_AUTO_COMMIT
155 except AttributeError:
156 pass
157 if recover:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000158 flagsforenv = flagsforenv | DB_RECOVER
159 self.env = DBEnv()
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000160 # enable auto deadlock avoidance
161 self.env.set_lk_detect(DB_LOCK_DEFAULT)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000162 self.env.open(dbhome, myflags | flagsforenv)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000163 if truncate:
164 myflags |= DB_TRUNCATE
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000165 self.db = DB(self.env)
Gregory P. Smith455d46f2003-07-09 04:45:59 +0000166 # this code relies on DBCursor.set* methods to raise exceptions
167 # rather than returning None
168 self.db.set_get_returns_none(1)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000169 # allow duplicate entries [warning: be careful w/ metadata]
170 self.db.set_flags(DB_DUP)
171 self.db.open(filename, DB_BTREE, dbflags | myflags, mode)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000172 self.dbfilename = filename
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000173 # Initialize the table names list if this is a new database
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000174 txn = self.env.txn_begin()
175 try:
176 if not self.db.has_key(_table_names_key, txn):
177 self.db.put(_table_names_key, pickle.dumps([], 1), txn=txn)
178 # Yes, bare except
179 except:
180 txn.abort()
181 raise
182 else:
183 txn.commit()
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000184 # TODO verify more of the database's metadata?
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000185 self.__tablecolumns = {}
186
187 def __del__(self):
188 self.close()
189
190 def close(self):
191 if self.db is not None:
192 self.db.close()
193 self.db = None
194 if self.env is not None:
195 self.env.close()
196 self.env = None
197
198 def checkpoint(self, mins=0):
199 try:
200 self.env.txn_checkpoint(mins)
201 except DBIncompleteError:
202 pass
203
204 def sync(self):
205 try:
206 self.db.sync()
207 except DBIncompleteError:
208 pass
209
210 def _db_print(self) :
211 """Print the database to stdout for debugging"""
212 print "******** Printing raw database for debugging ********"
213 cur = self.db.cursor()
214 try:
215 key, data = cur.first()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000216 while 1:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000217 print repr({key: data})
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000218 next = cur.next()
219 if next:
220 key, data = next
221 else:
222 cur.close()
223 return
224 except DBNotFoundError:
225 cur.close()
226
227
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000228 def CreateTable(self, table, columns):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000229 """CreateTable(table, columns) - Create a new table in the database.
230
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000231 raises TableDBError if it already exists or for other DB errors.
232 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000233 assert isinstance(columns, ListType)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000234 txn = None
235 try:
236 # checking sanity of the table and column names here on
237 # table creation will prevent problems elsewhere.
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000238 if contains_metastrings(table):
239 raise ValueError(
240 "bad table name: contains reserved metastrings")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000241 for column in columns :
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000242 if contains_metastrings(column):
243 raise ValueError(
244 "bad column name: contains reserved metastrings")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000245
246 columnlist_key = _columns_key(table)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000247 if self.db.has_key(columnlist_key):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000248 raise TableAlreadyExists, "table already exists"
249
250 txn = self.env.txn_begin()
251 # store the table's column info
252 self.db.put(columnlist_key, pickle.dumps(columns, 1), txn=txn)
253
254 # add the table name to the tablelist
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000255 tablelist = pickle.loads(self.db.get(_table_names_key, txn=txn,
256 flags=DB_RMW))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000257 tablelist.append(table)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000258 # delete 1st, in case we opened with DB_DUP
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000259 self.db.delete(_table_names_key, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000260 self.db.put(_table_names_key, pickle.dumps(tablelist, 1), txn=txn)
261
262 txn.commit()
263 txn = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000264 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000265 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000266 txn.abort()
267 raise TableDBError, dberror[1]
268
269
270 def ListTableColumns(self, table):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000271 """Return a list of columns in the given table.
272 [] if the table doesn't exist.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000273 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000274 assert isinstance(table, StringType)
275 if contains_metastrings(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000276 raise ValueError, "bad table name: contains reserved metastrings"
277
278 columnlist_key = _columns_key(table)
279 if not self.db.has_key(columnlist_key):
280 return []
281 pickledcolumnlist = self.db.get(columnlist_key)
282 if pickledcolumnlist:
283 return pickle.loads(pickledcolumnlist)
284 else:
285 return []
286
287 def ListTables(self):
288 """Return a list of tables in this database."""
289 pickledtablelist = self.db.get(_table_names_key)
290 if pickledtablelist:
291 return pickle.loads(pickledtablelist)
292 else:
293 return []
294
295 def CreateOrExtendTable(self, table, columns):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000296 """CreateOrExtendTable(table, columns)
297
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000298 Create a new table in the database.
299
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000300 If a table of this name already exists, extend it to have any
301 additional columns present in the given list as well as
302 all of its current columns.
303 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000304 assert isinstance(columns, ListType)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000305 try:
306 self.CreateTable(table, columns)
307 except TableAlreadyExists:
308 # the table already existed, add any new columns
309 txn = None
310 try:
311 columnlist_key = _columns_key(table)
312 txn = self.env.txn_begin()
313
314 # load the current column list
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000315 oldcolumnlist = pickle.loads(
316 self.db.get(columnlist_key, txn=txn, flags=DB_RMW))
317 # create a hash table for fast lookups of column names in the
318 # loop below
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000319 oldcolumnhash = {}
320 for c in oldcolumnlist:
321 oldcolumnhash[c] = c
322
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000323 # create a new column list containing both the old and new
324 # column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000325 newcolumnlist = copy.copy(oldcolumnlist)
326 for c in columns:
327 if not oldcolumnhash.has_key(c):
328 newcolumnlist.append(c)
329
330 # store the table's new extended column list
331 if newcolumnlist != oldcolumnlist :
332 # delete the old one first since we opened with DB_DUP
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000333 self.db.delete(columnlist_key, txn=txn)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000334 self.db.put(columnlist_key,
335 pickle.dumps(newcolumnlist, 1),
336 txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000337
338 txn.commit()
339 txn = None
340
341 self.__load_column_info(table)
342 except DBError, dberror:
343 if txn:
344 txn.abort()
345 raise TableDBError, dberror[1]
346
347
348 def __load_column_info(self, table) :
349 """initialize the self.__tablecolumns dict"""
350 # check the column names
351 try:
352 tcolpickles = self.db.get(_columns_key(table))
353 except DBNotFoundError:
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 if not tcolpickles:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000356 raise TableDBError, "unknown table: %r" % (table,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000357 self.__tablecolumns[table] = pickle.loads(tcolpickles)
358
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000359 def __new_rowid(self, table, txn) :
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000360 """Create a new unique row identifier"""
361 unique = 0
Barry Warsawf71de3e2003-01-28 17:20:44 +0000362 while not unique:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000363 # Generate a random 64-bit row ID string
364 # (note: this code has <64 bits of randomness
365 # but it's plenty for our database id needs!)
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000366 newid = struct.pack('ll',
367 random.randint(0, 2147483647),
368 random.randint(0, 2147483647))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000369
370 # Guarantee uniqueness by adding this key to the database
371 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000372 self.db.put(_rowid_key(table, newid), None, txn=txn,
373 flags=DB_NOOVERWRITE)
374 except DBKeyExistError:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000375 pass
376 else:
377 unique = 1
378
379 return newid
380
381
382 def Insert(self, table, rowdict) :
383 """Insert(table, datadict) - Insert a new row into the table
384 using the keys+values from rowdict as the column values.
385 """
386 txn = None
387 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000388 if not self.db.has_key(_columns_key(table)):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000389 raise TableDBError, "unknown table"
390
391 # check the validity of each column name
Barry Warsawf71de3e2003-01-28 17:20:44 +0000392 if not self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000393 self.__load_column_info(table)
394 for column in rowdict.keys() :
Barry Warsawf71de3e2003-01-28 17:20:44 +0000395 if not self.__tablecolumns[table].count(column):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000396 raise TableDBError, "unknown column: %r" % (column,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000397
398 # get a unique row identifier for this row
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000399 txn = self.env.txn_begin()
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000400 rowid = self.__new_rowid(table, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000401
402 # insert the row values into the table database
Barry Warsawf71de3e2003-01-28 17:20:44 +0000403 for column, dataitem in rowdict.items():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000404 # store the value
405 self.db.put(_data_key(table, column, rowid), dataitem, txn=txn)
406
407 txn.commit()
408 txn = None
409
410 except DBError, dberror:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000411 # WIBNI we could just abort the txn and re-raise the exception?
412 # But no, because TableDBError is not related to DBError via
413 # inheritance, so it would be backwards incompatible. Do the next
414 # best thing.
415 info = sys.exc_info()
416 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000417 txn.abort()
418 self.db.delete(_rowid_key(table, rowid))
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000419 raise TableDBError, dberror[1], info[2]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000420
421
Barry Warsawf71de3e2003-01-28 17:20:44 +0000422 def Modify(self, table, conditions={}, mappings={}):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000423 """Modify(table, conditions={}, mappings={}) - Modify items in rows matching 'conditions' using mapping functions in 'mappings'
424
425 * table - the table name
426 * conditions - a dictionary keyed on column names containing
427 a condition callable expecting the data string as an
428 argument and returning a boolean.
429 * mappings - a dictionary keyed on column names containing a
430 condition callable expecting the data string as an argument and
431 returning the new string for that column.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000432 """
433 try:
434 matching_rowids = self.__Select(table, [], conditions)
435
436 # modify only requested columns
437 columns = mappings.keys()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000438 for rowid in matching_rowids.keys():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000439 txn = None
440 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000441 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000442 txn = self.env.txn_begin()
443 # modify the requested column
444 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000445 dataitem = self.db.get(
446 _data_key(table, column, rowid),
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000447 txn=txn)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000448 self.db.delete(
449 _data_key(table, column, rowid),
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000450 txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000451 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000452 # XXXXXXX row key somehow didn't exist, assume no
453 # error
454 dataitem = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000455 dataitem = mappings[column](dataitem)
456 if dataitem <> None:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000457 self.db.put(
458 _data_key(table, column, rowid),
459 dataitem, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000460 txn.commit()
461 txn = None
462
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000463 # catch all exceptions here since we call unknown callables
464 except:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000465 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000466 txn.abort()
467 raise
468
469 except DBError, dberror:
470 raise TableDBError, dberror[1]
471
Barry Warsawf71de3e2003-01-28 17:20:44 +0000472 def Delete(self, table, conditions={}):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000473 """Delete(table, conditions) - Delete items matching the given
474 conditions from the table.
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000475
476 * conditions - a dictionary keyed on column names containing
477 condition functions expecting the data string as an
478 argument and returning a boolean.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000479 """
480 try:
481 matching_rowids = self.__Select(table, [], conditions)
482
483 # delete row data from all columns
484 columns = self.__tablecolumns[table]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000485 for rowid in matching_rowids.keys():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000486 txn = None
487 try:
488 txn = self.env.txn_begin()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000489 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000490 # delete the data key
491 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000492 self.db.delete(_data_key(table, column, rowid),
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000493 txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000494 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000495 # XXXXXXX column may not exist, assume no error
496 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000497
498 try:
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000499 self.db.delete(_rowid_key(table, rowid), txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000500 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000501 # XXXXXXX row key somehow didn't exist, assume no error
502 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000503 txn.commit()
504 txn = None
505 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000506 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000507 txn.abort()
508 raise
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000509 except DBError, dberror:
510 raise TableDBError, dberror[1]
511
512
Barry Warsawf71de3e2003-01-28 17:20:44 +0000513 def Select(self, table, columns, conditions={}):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000514 """Select(table, columns, conditions) - retrieve specific row data
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000515 Returns a list of row column->value mapping dictionaries.
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000516
517 * columns - a list of which column data to return. If
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000518 columns is None, all columns will be returned.
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000519 * conditions - a dictionary keyed on column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000520 containing callable conditions expecting the data string as an
521 argument and returning a boolean.
522 """
523 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000524 if not self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000525 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000526 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000527 columns = self.__tablecolumns[table]
528 matching_rowids = self.__Select(table, columns, conditions)
529 except DBError, dberror:
530 raise TableDBError, dberror[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000531 # return the matches as a list of dictionaries
532 return matching_rowids.values()
533
534
Barry Warsawf71de3e2003-01-28 17:20:44 +0000535 def __Select(self, table, columns, conditions):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000536 """__Select() - Used to implement Select and Delete (above)
537 Returns a dictionary keyed on rowids containing dicts
538 holding the row data for columns listed in the columns param
539 that match the given conditions.
540 * conditions is a dictionary keyed on column names
541 containing callable conditions expecting the data string as an
542 argument and returning a boolean.
543 """
544 # check the validity of each column name
Barry Warsawf71de3e2003-01-28 17:20:44 +0000545 if not self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000546 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000547 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000548 columns = self.tablecolumns[table]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000549 for column in (columns + conditions.keys()):
550 if not self.__tablecolumns[table].count(column):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000551 raise TableDBError, "unknown column: %r" % (column,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000552
553 # keyed on rows that match so far, containings dicts keyed on
554 # column names containing the data for that row and column.
555 matching_rowids = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000556 # keys are rowids that do not match
557 rejected_rowids = {}
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000558
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000559 # attempt to sort the conditions in such a way as to minimize full
560 # column lookups
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000561 def cmp_conditions(atuple, btuple):
562 a = atuple[1]
563 b = btuple[1]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000564 if type(a) is type(b):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000565 if isinstance(a, PrefixCond) and isinstance(b, PrefixCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000566 # longest prefix first
567 return cmp(len(b.prefix), len(a.prefix))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000568 if isinstance(a, LikeCond) and isinstance(b, LikeCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000569 # longest likestr first
570 return cmp(len(b.likestr), len(a.likestr))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000571 return 0
572 if isinstance(a, ExactCond):
573 return -1
574 if isinstance(b, ExactCond):
575 return 1
576 if isinstance(a, PrefixCond):
577 return -1
578 if isinstance(b, PrefixCond):
579 return 1
580 # leave all unknown condition callables alone as equals
581 return 0
582
583 conditionlist = conditions.items()
584 conditionlist.sort(cmp_conditions)
585
586 # Apply conditions to column data to find what we want
587 cur = self.db.cursor()
588 column_num = -1
Barry Warsawf71de3e2003-01-28 17:20:44 +0000589 for column, condition in conditionlist:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000590 column_num = column_num + 1
591 searchkey = _search_col_data_key(table, column)
592 # speedup: don't linear search columns within loop
Barry Warsawf71de3e2003-01-28 17:20:44 +0000593 if column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000594 savethiscolumndata = 1 # save the data for return
Barry Warsawf71de3e2003-01-28 17:20:44 +0000595 else:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000596 savethiscolumndata = 0 # data only used for selection
597
598 try:
599 key, data = cur.set_range(searchkey)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000600 while key[:len(searchkey)] == searchkey:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000601 # extract the rowid from the key
602 rowid = key[-_rowid_str_len:]
603
Barry Warsawf71de3e2003-01-28 17:20:44 +0000604 if not rejected_rowids.has_key(rowid):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000605 # if no condition was specified or the condition
606 # succeeds, add row to our match list.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000607 if not condition or condition(data):
608 if not matching_rowids.has_key(rowid):
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000609 matching_rowids[rowid] = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000610 if savethiscolumndata:
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000611 matching_rowids[rowid][column] = data
Barry Warsawf71de3e2003-01-28 17:20:44 +0000612 else:
613 if matching_rowids.has_key(rowid):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000614 del matching_rowids[rowid]
615 rejected_rowids[rowid] = rowid
616
617 key, data = cur.next()
618
619 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000620 if dberror[0] != DB_NOTFOUND:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000621 raise
622 continue
623
624 cur.close()
625
626 # we're done selecting rows, garbage collect the reject list
627 del rejected_rowids
628
629 # extract any remaining desired column data from the
630 # database for the matching rows.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000631 if len(columns) > 0:
632 for rowid, rowdata in matching_rowids.items():
633 for column in columns:
634 if rowdata.has_key(column):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000635 continue
636 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000637 rowdata[column] = self.db.get(
638 _data_key(table, column, rowid))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000639 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000640 if dberror[0] != DB_NOTFOUND:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000641 raise
642 rowdata[column] = None
643
644 # return the matches
645 return matching_rowids
646
647
Barry Warsawf71de3e2003-01-28 17:20:44 +0000648 def Drop(self, table):
649 """Remove an entire table from the database"""
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000650 txn = None
651 try:
652 txn = self.env.txn_begin()
653
654 # delete the column list
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000655 self.db.delete(_columns_key(table), txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000656
657 cur = self.db.cursor(txn)
658
659 # delete all keys containing this tables column and row info
660 table_key = _search_all_data_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000661 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000662 try:
663 key, data = cur.set_range(table_key)
664 except DBNotFoundError:
665 break
666 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000667 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000668 break
669 cur.delete()
670
671 # delete all rowids used by this table
672 table_key = _search_rowid_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000673 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000674 try:
675 key, data = cur.set_range(table_key)
676 except DBNotFoundError:
677 break
678 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000679 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000680 break
681 cur.delete()
682
683 cur.close()
684
685 # delete the tablename from the table name list
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000686 tablelist = pickle.loads(
687 self.db.get(_table_names_key, txn=txn, flags=DB_RMW))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000688 try:
689 tablelist.remove(table)
690 except ValueError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000691 # hmm, it wasn't there, oh well, that's what we want.
692 pass
693 # delete 1st, incase we opened with DB_DUP
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000694 self.db.delete(_table_names_key, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000695 self.db.put(_table_names_key, pickle.dumps(tablelist, 1), txn=txn)
696
697 txn.commit()
698 txn = None
699
Barry Warsawf71de3e2003-01-28 17:20:44 +0000700 if self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000701 del self.__tablecolumns[table]
702
703 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000704 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000705 txn.abort()
706 raise TableDBError, dberror[1]