blob: f6c7f02cfe32bdbc68dcda9a2030cf47de95c8a4 [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
Gregory P. Smith0dcc3cc2007-10-18 17:15:20 +000023import struct
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
Neal Norwitz6aaccc62006-06-11 08:35:14 +000035# XXX(nnorwitz): is this correct? DBIncompleteError is conditional in _bsddb.c
36try:
37 DBIncompleteError
38except NameError:
39 class DBIncompleteError(Exception):
40 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000041
Barry Warsawf71de3e2003-01-28 17:20:44 +000042class TableDBError(StandardError):
43 pass
44class TableAlreadyExists(TableDBError):
45 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000046
47
48class Cond:
49 """This condition matches everything"""
50 def __call__(self, s):
51 return 1
52
53class ExactCond(Cond):
54 """Acts as an exact match condition function"""
55 def __init__(self, strtomatch):
56 self.strtomatch = strtomatch
57 def __call__(self, s):
58 return s == self.strtomatch
59
60class PrefixCond(Cond):
61 """Acts as a condition function for matching a string prefix"""
62 def __init__(self, prefix):
63 self.prefix = prefix
64 def __call__(self, s):
65 return s[:len(self.prefix)] == self.prefix
66
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +000067class PostfixCond(Cond):
68 """Acts as a condition function for matching a string postfix"""
69 def __init__(self, postfix):
70 self.postfix = postfix
71 def __call__(self, s):
72 return s[-len(self.postfix):] == self.postfix
73
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000074class LikeCond(Cond):
75 """
76 Acts as a function that will match using an SQL 'LIKE' style
77 string. Case insensitive and % signs are wild cards.
78 This isn't perfect but it should work for the simple common cases.
79 """
80 def __init__(self, likestr, re_flags=re.IGNORECASE):
81 # escape python re characters
82 chars_to_escape = '.*+()[]?'
83 for char in chars_to_escape :
Barry Warsawf71de3e2003-01-28 17:20:44 +000084 likestr = likestr.replace(char, '\\'+char)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000085 # convert %s to wildcards
Barry Warsawf71de3e2003-01-28 17:20:44 +000086 self.likestr = likestr.replace('%', '.*')
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000087 self.re = re.compile('^'+self.likestr+'$', re_flags)
88 def __call__(self, s):
89 return self.re.match(s)
90
91#
92# keys used to store database metadata
93#
94_table_names_key = '__TABLE_NAMES__' # list of the tables in this db
95_columns = '._COLUMNS__' # table_name+this key contains a list of columns
Barry Warsawf71de3e2003-01-28 17:20:44 +000096
97def _columns_key(table):
98 return table + _columns
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000099
100#
101# these keys are found within table sub databases
102#
103_data = '._DATA_.' # this+column+this+rowid key contains table data
104_rowid = '._ROWID_.' # this+rowid+this key contains a unique entry for each
105 # row in the table. (no data is stored)
106_rowid_str_len = 8 # length in bytes of the unique rowid strings
Barry Warsawf71de3e2003-01-28 17:20:44 +0000107
108def _data_key(table, col, rowid):
109 return table + _data + col + _data + rowid
110
111def _search_col_data_key(table, col):
112 return table + _data + col + _data
113
114def _search_all_data_key(table):
115 return table + _data
116
117def _rowid_key(table, rowid):
118 return table + _rowid + rowid + _rowid
119
120def _search_rowid_key(table):
121 return table + _rowid
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000122
123def contains_metastrings(s) :
124 """Verify that the given string does not contain any
125 metadata strings that might interfere with dbtables database operation.
126 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000127 if (s.find(_table_names_key) >= 0 or
128 s.find(_columns) >= 0 or
129 s.find(_data) >= 0 or
130 s.find(_rowid) >= 0):
131 # Then
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000132 return 1
Barry Warsawf71de3e2003-01-28 17:20:44 +0000133 else:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000134 return 0
135
136
137class bsdTableDB :
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000138 def __init__(self, filename, dbhome, create=0, truncate=0, mode=0600,
Barry Warsawf71de3e2003-01-28 17:20:44 +0000139 recover=0, dbflags=0):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000140 """bsdTableDB(filename, dbhome, create=0, truncate=0, mode=0600)
141
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000142 Open database name in the dbhome BerkeleyDB directory.
143 Use keyword arguments when calling this constructor.
144 """
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000145 self.db = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000146 myflags = DB_THREAD
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000147 if create:
148 myflags |= DB_CREATE
149 flagsforenv = (DB_INIT_MPOOL | DB_INIT_LOCK | DB_INIT_LOG |
150 DB_INIT_TXN | dbflags)
151 # DB_AUTO_COMMIT isn't a valid flag for env.open()
152 try:
153 dbflags |= DB_AUTO_COMMIT
154 except AttributeError:
155 pass
156 if recover:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000157 flagsforenv = flagsforenv | DB_RECOVER
158 self.env = DBEnv()
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000159 # enable auto deadlock avoidance
160 self.env.set_lk_detect(DB_LOCK_DEFAULT)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000161 self.env.open(dbhome, myflags | flagsforenv)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000162 if truncate:
163 myflags |= DB_TRUNCATE
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000164 self.db = DB(self.env)
Gregory P. Smith455d46f2003-07-09 04:45:59 +0000165 # this code relies on DBCursor.set* methods to raise exceptions
166 # rather than returning None
167 self.db.set_get_returns_none(1)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000168 # allow duplicate entries [warning: be careful w/ metadata]
169 self.db.set_flags(DB_DUP)
170 self.db.open(filename, DB_BTREE, dbflags | myflags, mode)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000171 self.dbfilename = filename
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000172 # Initialize the table names list if this is a new database
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000173 txn = self.env.txn_begin()
174 try:
175 if not self.db.has_key(_table_names_key, txn):
176 self.db.put(_table_names_key, pickle.dumps([], 1), txn=txn)
177 # Yes, bare except
178 except:
179 txn.abort()
180 raise
181 else:
182 txn.commit()
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000183 # TODO verify more of the database's metadata?
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000184 self.__tablecolumns = {}
185
186 def __del__(self):
187 self.close()
188
189 def close(self):
190 if self.db is not None:
191 self.db.close()
192 self.db = None
193 if self.env is not None:
194 self.env.close()
195 self.env = None
196
197 def checkpoint(self, mins=0):
198 try:
199 self.env.txn_checkpoint(mins)
200 except DBIncompleteError:
201 pass
202
203 def sync(self):
204 try:
205 self.db.sync()
206 except DBIncompleteError:
207 pass
208
209 def _db_print(self) :
210 """Print the database to stdout for debugging"""
211 print "******** Printing raw database for debugging ********"
212 cur = self.db.cursor()
213 try:
214 key, data = cur.first()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000215 while 1:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000216 print repr({key: data})
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000217 next = cur.next()
218 if next:
219 key, data = next
220 else:
221 cur.close()
222 return
223 except DBNotFoundError:
224 cur.close()
225
226
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000227 def CreateTable(self, table, columns):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000228 """CreateTable(table, columns) - Create a new table in the database.
229
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000230 raises TableDBError if it already exists or for other DB errors.
231 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000232 assert isinstance(columns, ListType)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000233 txn = None
234 try:
235 # checking sanity of the table and column names here on
236 # table creation will prevent problems elsewhere.
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000237 if contains_metastrings(table):
238 raise ValueError(
239 "bad table name: contains reserved metastrings")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000240 for column in columns :
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000241 if contains_metastrings(column):
242 raise ValueError(
243 "bad column name: contains reserved metastrings")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000244
245 columnlist_key = _columns_key(table)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000246 if self.db.has_key(columnlist_key):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000247 raise TableAlreadyExists, "table already exists"
248
249 txn = self.env.txn_begin()
250 # store the table's column info
251 self.db.put(columnlist_key, pickle.dumps(columns, 1), txn=txn)
252
253 # add the table name to the tablelist
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000254 tablelist = pickle.loads(self.db.get(_table_names_key, txn=txn,
255 flags=DB_RMW))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000256 tablelist.append(table)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000257 # delete 1st, in case we opened with DB_DUP
258 self.db.delete(_table_names_key, txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000259 self.db.put(_table_names_key, pickle.dumps(tablelist, 1), txn=txn)
260
261 txn.commit()
262 txn = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000263 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000264 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000265 txn.abort()
266 raise TableDBError, dberror[1]
267
268
269 def ListTableColumns(self, table):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000270 """Return a list of columns in the given table.
271 [] if the table doesn't exist.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000272 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000273 assert isinstance(table, StringType)
274 if contains_metastrings(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000275 raise ValueError, "bad table name: contains reserved metastrings"
276
277 columnlist_key = _columns_key(table)
278 if not self.db.has_key(columnlist_key):
279 return []
280 pickledcolumnlist = self.db.get(columnlist_key)
281 if pickledcolumnlist:
282 return pickle.loads(pickledcolumnlist)
283 else:
284 return []
285
286 def ListTables(self):
287 """Return a list of tables in this database."""
288 pickledtablelist = self.db.get(_table_names_key)
289 if pickledtablelist:
290 return pickle.loads(pickledtablelist)
291 else:
292 return []
293
294 def CreateOrExtendTable(self, table, columns):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000295 """CreateOrExtendTable(table, columns)
296
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000297 Create a new table in the database.
298
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000299 If a table of this name already exists, extend it to have any
300 additional columns present in the given list as well as
301 all of its current columns.
302 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000303 assert isinstance(columns, ListType)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000304 try:
305 self.CreateTable(table, columns)
306 except TableAlreadyExists:
307 # the table already existed, add any new columns
308 txn = None
309 try:
310 columnlist_key = _columns_key(table)
311 txn = self.env.txn_begin()
312
313 # load the current column list
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000314 oldcolumnlist = pickle.loads(
315 self.db.get(columnlist_key, txn=txn, flags=DB_RMW))
316 # create a hash table for fast lookups of column names in the
317 # loop below
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000318 oldcolumnhash = {}
319 for c in oldcolumnlist:
320 oldcolumnhash[c] = c
321
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000322 # create a new column list containing both the old and new
323 # column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000324 newcolumnlist = copy.copy(oldcolumnlist)
325 for c in columns:
326 if not oldcolumnhash.has_key(c):
327 newcolumnlist.append(c)
328
329 # store the table's new extended column list
330 if newcolumnlist != oldcolumnlist :
331 # delete the old one first since we opened with DB_DUP
332 self.db.delete(columnlist_key, txn)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000333 self.db.put(columnlist_key,
334 pickle.dumps(newcolumnlist, 1),
335 txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000336
337 txn.commit()
338 txn = None
339
340 self.__load_column_info(table)
341 except DBError, dberror:
342 if txn:
343 txn.abort()
344 raise TableDBError, dberror[1]
345
346
347 def __load_column_info(self, table) :
348 """initialize the self.__tablecolumns dict"""
349 # check the column names
350 try:
351 tcolpickles = self.db.get(_columns_key(table))
352 except DBNotFoundError:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000353 raise TableDBError, "unknown table: %r" % (table,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000354 if not tcolpickles:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000355 raise TableDBError, "unknown table: %r" % (table,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000356 self.__tablecolumns[table] = pickle.loads(tcolpickles)
357
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000358 def __new_rowid(self, table, txn) :
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000359 """Create a new unique row identifier"""
360 unique = 0
Barry Warsawf71de3e2003-01-28 17:20:44 +0000361 while not unique:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000362 # Generate a random 64-bit row ID string
363 # (note: this code has <64 bits of randomness
364 # but it's plenty for our database id needs!)
Gregory P. Smith0dcc3cc2007-10-18 17:15:20 +0000365 # We must ensure that no null bytes are in the id value.
366 blist = []
367 for x in xrange(_rowid_str_len):
368 blist.append(random.randint(1,255))
369 newid = struct.pack('B'*_rowid_str_len, *blist)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000370
371 # Guarantee uniqueness by adding this key to the database
372 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000373 self.db.put(_rowid_key(table, newid), None, txn=txn,
374 flags=DB_NOOVERWRITE)
375 except DBKeyExistError:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000376 pass
377 else:
378 unique = 1
379
380 return newid
381
382
383 def Insert(self, table, rowdict) :
384 """Insert(table, datadict) - Insert a new row into the table
385 using the keys+values from rowdict as the column values.
386 """
387 txn = None
388 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000389 if not self.db.has_key(_columns_key(table)):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000390 raise TableDBError, "unknown table"
391
392 # check the validity of each column name
Barry Warsawf71de3e2003-01-28 17:20:44 +0000393 if not self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000394 self.__load_column_info(table)
395 for column in rowdict.keys() :
Barry Warsawf71de3e2003-01-28 17:20:44 +0000396 if not self.__tablecolumns[table].count(column):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000397 raise TableDBError, "unknown column: %r" % (column,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000398
399 # get a unique row identifier for this row
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000400 txn = self.env.txn_begin()
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000401 rowid = self.__new_rowid(table, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000402
403 # insert the row values into the table database
Barry Warsawf71de3e2003-01-28 17:20:44 +0000404 for column, dataitem in rowdict.items():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000405 # store the value
406 self.db.put(_data_key(table, column, rowid), dataitem, txn=txn)
407
408 txn.commit()
409 txn = None
410
411 except DBError, dberror:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000412 # WIBNI we could just abort the txn and re-raise the exception?
413 # But no, because TableDBError is not related to DBError via
414 # inheritance, so it would be backwards incompatible. Do the next
415 # best thing.
416 info = sys.exc_info()
417 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000418 txn.abort()
419 self.db.delete(_rowid_key(table, rowid))
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000420 raise TableDBError, dberror[1], info[2]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000421
422
Barry Warsawf71de3e2003-01-28 17:20:44 +0000423 def Modify(self, table, conditions={}, mappings={}):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000424 """Modify(table, conditions={}, mappings={}) - Modify items in rows matching 'conditions' using mapping functions in 'mappings'
425
426 * table - the table name
427 * conditions - a dictionary keyed on column names containing
428 a condition callable expecting the data string as an
429 argument and returning a boolean.
430 * mappings - a dictionary keyed on column names containing a
431 condition callable expecting the data string as an argument and
432 returning the new string for that column.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000433 """
434 try:
435 matching_rowids = self.__Select(table, [], conditions)
436
437 # modify only requested columns
438 columns = mappings.keys()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000439 for rowid in matching_rowids.keys():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000440 txn = None
441 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000442 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000443 txn = self.env.txn_begin()
444 # modify the requested column
445 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000446 dataitem = self.db.get(
447 _data_key(table, column, rowid),
Gregory P. Smith0dcc3cc2007-10-18 17:15:20 +0000448 txn=txn)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000449 self.db.delete(
450 _data_key(table, column, rowid),
451 txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000452 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000453 # XXXXXXX row key somehow didn't exist, assume no
454 # error
455 dataitem = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000456 dataitem = mappings[column](dataitem)
457 if dataitem <> None:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000458 self.db.put(
459 _data_key(table, column, rowid),
460 dataitem, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000461 txn.commit()
462 txn = None
463
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000464 # catch all exceptions here since we call unknown callables
465 except:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000466 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000467 txn.abort()
468 raise
469
470 except DBError, dberror:
471 raise TableDBError, dberror[1]
472
Barry Warsawf71de3e2003-01-28 17:20:44 +0000473 def Delete(self, table, conditions={}):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000474 """Delete(table, conditions) - Delete items matching the given
475 conditions from the table.
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000476
477 * conditions - a dictionary keyed on column names containing
478 condition functions expecting the data string as an
479 argument and returning a boolean.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000480 """
481 try:
482 matching_rowids = self.__Select(table, [], conditions)
483
484 # delete row data from all columns
485 columns = self.__tablecolumns[table]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000486 for rowid in matching_rowids.keys():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000487 txn = None
488 try:
489 txn = self.env.txn_begin()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000490 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000491 # delete the data key
492 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000493 self.db.delete(_data_key(table, column, rowid),
494 txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000495 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000496 # XXXXXXX column may not exist, assume no error
497 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000498
499 try:
500 self.db.delete(_rowid_key(table, rowid), txn)
501 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000502 # XXXXXXX row key somehow didn't exist, assume no error
503 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000504 txn.commit()
505 txn = None
506 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000507 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000508 txn.abort()
509 raise
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000510 except DBError, dberror:
511 raise TableDBError, dberror[1]
512
513
Barry Warsawf71de3e2003-01-28 17:20:44 +0000514 def Select(self, table, columns, conditions={}):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000515 """Select(table, columns, conditions) - retrieve specific row data
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000516 Returns a list of row column->value mapping dictionaries.
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000517
518 * columns - a list of which column data to return. If
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000519 columns is None, all columns will be returned.
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000520 * conditions - a dictionary keyed on column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000521 containing callable conditions expecting the data string as an
522 argument and returning a boolean.
523 """
524 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000525 if not self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000526 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000527 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000528 columns = self.__tablecolumns[table]
529 matching_rowids = self.__Select(table, columns, conditions)
530 except DBError, dberror:
531 raise TableDBError, dberror[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000532 # return the matches as a list of dictionaries
533 return matching_rowids.values()
534
535
Barry Warsawf71de3e2003-01-28 17:20:44 +0000536 def __Select(self, table, columns, conditions):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000537 """__Select() - Used to implement Select and Delete (above)
538 Returns a dictionary keyed on rowids containing dicts
539 holding the row data for columns listed in the columns param
540 that match the given conditions.
541 * conditions is a dictionary keyed on column names
542 containing callable conditions expecting the data string as an
543 argument and returning a boolean.
544 """
545 # check the validity of each column name
Barry Warsawf71de3e2003-01-28 17:20:44 +0000546 if not self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000547 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000548 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000549 columns = self.tablecolumns[table]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000550 for column in (columns + conditions.keys()):
551 if not self.__tablecolumns[table].count(column):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000552 raise TableDBError, "unknown column: %r" % (column,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000553
554 # keyed on rows that match so far, containings dicts keyed on
555 # column names containing the data for that row and column.
556 matching_rowids = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000557 # keys are rowids that do not match
558 rejected_rowids = {}
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000559
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000560 # attempt to sort the conditions in such a way as to minimize full
561 # column lookups
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000562 def cmp_conditions(atuple, btuple):
563 a = atuple[1]
564 b = btuple[1]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000565 if type(a) is type(b):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000566 if isinstance(a, PrefixCond) and isinstance(b, PrefixCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000567 # longest prefix first
568 return cmp(len(b.prefix), len(a.prefix))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000569 if isinstance(a, LikeCond) and isinstance(b, LikeCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000570 # longest likestr first
571 return cmp(len(b.likestr), len(a.likestr))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000572 return 0
573 if isinstance(a, ExactCond):
574 return -1
575 if isinstance(b, ExactCond):
576 return 1
577 if isinstance(a, PrefixCond):
578 return -1
579 if isinstance(b, PrefixCond):
580 return 1
581 # leave all unknown condition callables alone as equals
582 return 0
583
584 conditionlist = conditions.items()
585 conditionlist.sort(cmp_conditions)
586
587 # Apply conditions to column data to find what we want
588 cur = self.db.cursor()
589 column_num = -1
Barry Warsawf71de3e2003-01-28 17:20:44 +0000590 for column, condition in conditionlist:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000591 column_num = column_num + 1
592 searchkey = _search_col_data_key(table, column)
593 # speedup: don't linear search columns within loop
Barry Warsawf71de3e2003-01-28 17:20:44 +0000594 if column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000595 savethiscolumndata = 1 # save the data for return
Barry Warsawf71de3e2003-01-28 17:20:44 +0000596 else:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000597 savethiscolumndata = 0 # data only used for selection
598
599 try:
600 key, data = cur.set_range(searchkey)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000601 while key[:len(searchkey)] == searchkey:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000602 # extract the rowid from the key
603 rowid = key[-_rowid_str_len:]
604
Barry Warsawf71de3e2003-01-28 17:20:44 +0000605 if not rejected_rowids.has_key(rowid):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000606 # if no condition was specified or the condition
607 # succeeds, add row to our match list.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000608 if not condition or condition(data):
609 if not matching_rowids.has_key(rowid):
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000610 matching_rowids[rowid] = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000611 if savethiscolumndata:
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000612 matching_rowids[rowid][column] = data
Barry Warsawf71de3e2003-01-28 17:20:44 +0000613 else:
614 if matching_rowids.has_key(rowid):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000615 del matching_rowids[rowid]
616 rejected_rowids[rowid] = rowid
617
618 key, data = cur.next()
619
620 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000621 if dberror[0] != DB_NOTFOUND:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000622 raise
623 continue
624
625 cur.close()
626
627 # we're done selecting rows, garbage collect the reject list
628 del rejected_rowids
629
630 # extract any remaining desired column data from the
631 # database for the matching rows.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000632 if len(columns) > 0:
633 for rowid, rowdata in matching_rowids.items():
634 for column in columns:
635 if rowdata.has_key(column):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000636 continue
637 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000638 rowdata[column] = self.db.get(
639 _data_key(table, column, rowid))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000640 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000641 if dberror[0] != DB_NOTFOUND:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000642 raise
643 rowdata[column] = None
644
645 # return the matches
646 return matching_rowids
647
648
Barry Warsawf71de3e2003-01-28 17:20:44 +0000649 def Drop(self, table):
650 """Remove an entire table from the database"""
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000651 txn = None
652 try:
653 txn = self.env.txn_begin()
654
655 # delete the column list
656 self.db.delete(_columns_key(table), txn)
657
658 cur = self.db.cursor(txn)
659
660 # delete all keys containing this tables column and row info
661 table_key = _search_all_data_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000662 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000663 try:
664 key, data = cur.set_range(table_key)
665 except DBNotFoundError:
666 break
667 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000668 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000669 break
670 cur.delete()
671
672 # delete all rowids used by this table
673 table_key = _search_rowid_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000674 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000675 try:
676 key, data = cur.set_range(table_key)
677 except DBNotFoundError:
678 break
679 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000680 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000681 break
682 cur.delete()
683
684 cur.close()
685
686 # delete the tablename from the table name list
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000687 tablelist = pickle.loads(
688 self.db.get(_table_names_key, txn=txn, flags=DB_RMW))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000689 try:
690 tablelist.remove(table)
691 except ValueError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000692 # hmm, it wasn't there, oh well, that's what we want.
693 pass
694 # delete 1st, incase we opened with DB_DUP
695 self.db.delete(_table_names_key, txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000696 self.db.put(_table_names_key, pickle.dumps(tablelist, 1), txn=txn)
697
698 txn.commit()
699 txn = None
700
Barry Warsawf71de3e2003-01-28 17:20:44 +0000701 if self.__tablecolumns.has_key(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000702 del self.__tablecolumns[table]
703
704 except DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000705 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000706 txn.abort()
707 raise TableDBError, dberror[1]