|
1 #----------------------------------------------------------------------- |
|
2 # |
|
3 # Copyright (C) 2000, 2001 by Autonomous Zone Industries |
|
4 # Copyright (C) 2002 Gregory P. Smith |
|
5 # |
|
6 # License: This is free software. You may use this software for any |
|
7 # purpose including modification/redistribution, so long as |
|
8 # this header remains intact and that you do not claim any |
|
9 # rights of ownership or authorship of this software. This |
|
10 # software has been tested, but no warranty is expressed or |
|
11 # implied. |
|
12 # |
|
13 # -- Gregory P. Smith <greg@krypto.org> |
|
14 |
|
15 # This provides a simple database table interface built on top of |
|
16 # the Python Berkeley DB 3 interface. |
|
17 # |
|
18 _cvsid = '$Id: dbtables.py 66088 2008-08-31 14:00:51Z jesus.cea $' |
|
19 |
|
20 import re |
|
21 import sys |
|
22 import copy |
|
23 import random |
|
24 import struct |
|
25 import cPickle as pickle |
|
26 |
|
27 try: |
|
28 # For Pythons w/distutils pybsddb |
|
29 from bsddb3 import db |
|
30 except ImportError: |
|
31 # For Python 2.3 |
|
32 from bsddb import db |
|
33 |
|
34 # XXX(nnorwitz): is this correct? DBIncompleteError is conditional in _bsddb.c |
|
35 if not hasattr(db,"DBIncompleteError") : |
|
36 class DBIncompleteError(Exception): |
|
37 pass |
|
38 db.DBIncompleteError = DBIncompleteError |
|
39 |
|
40 class TableDBError(StandardError): |
|
41 pass |
|
42 class TableAlreadyExists(TableDBError): |
|
43 pass |
|
44 |
|
45 |
|
46 class Cond: |
|
47 """This condition matches everything""" |
|
48 def __call__(self, s): |
|
49 return 1 |
|
50 |
|
51 class ExactCond(Cond): |
|
52 """Acts as an exact match condition function""" |
|
53 def __init__(self, strtomatch): |
|
54 self.strtomatch = strtomatch |
|
55 def __call__(self, s): |
|
56 return s == self.strtomatch |
|
57 |
|
58 class PrefixCond(Cond): |
|
59 """Acts as a condition function for matching a string prefix""" |
|
60 def __init__(self, prefix): |
|
61 self.prefix = prefix |
|
62 def __call__(self, s): |
|
63 return s[:len(self.prefix)] == self.prefix |
|
64 |
|
65 class PostfixCond(Cond): |
|
66 """Acts as a condition function for matching a string postfix""" |
|
67 def __init__(self, postfix): |
|
68 self.postfix = postfix |
|
69 def __call__(self, s): |
|
70 return s[-len(self.postfix):] == self.postfix |
|
71 |
|
72 class LikeCond(Cond): |
|
73 """ |
|
74 Acts as a function that will match using an SQL 'LIKE' style |
|
75 string. Case insensitive and % signs are wild cards. |
|
76 This isn't perfect but it should work for the simple common cases. |
|
77 """ |
|
78 def __init__(self, likestr, re_flags=re.IGNORECASE): |
|
79 # escape python re characters |
|
80 chars_to_escape = '.*+()[]?' |
|
81 for char in chars_to_escape : |
|
82 likestr = likestr.replace(char, '\\'+char) |
|
83 # convert %s to wildcards |
|
84 self.likestr = likestr.replace('%', '.*') |
|
85 self.re = re.compile('^'+self.likestr+'$', re_flags) |
|
86 def __call__(self, s): |
|
87 return self.re.match(s) |
|
88 |
|
89 # |
|
90 # keys used to store database metadata |
|
91 # |
|
92 _table_names_key = '__TABLE_NAMES__' # list of the tables in this db |
|
93 _columns = '._COLUMNS__' # table_name+this key contains a list of columns |
|
94 |
|
95 def _columns_key(table): |
|
96 return table + _columns |
|
97 |
|
98 # |
|
99 # these keys are found within table sub databases |
|
100 # |
|
101 _data = '._DATA_.' # this+column+this+rowid key contains table data |
|
102 _rowid = '._ROWID_.' # this+rowid+this key contains a unique entry for each |
|
103 # row in the table. (no data is stored) |
|
104 _rowid_str_len = 8 # length in bytes of the unique rowid strings |
|
105 |
|
106 |
|
107 def _data_key(table, col, rowid): |
|
108 return table + _data + col + _data + rowid |
|
109 |
|
110 def _search_col_data_key(table, col): |
|
111 return table + _data + col + _data |
|
112 |
|
113 def _search_all_data_key(table): |
|
114 return table + _data |
|
115 |
|
116 def _rowid_key(table, rowid): |
|
117 return table + _rowid + rowid + _rowid |
|
118 |
|
119 def _search_rowid_key(table): |
|
120 return table + _rowid |
|
121 |
|
122 def contains_metastrings(s) : |
|
123 """Verify that the given string does not contain any |
|
124 metadata strings that might interfere with dbtables database operation. |
|
125 """ |
|
126 if (s.find(_table_names_key) >= 0 or |
|
127 s.find(_columns) >= 0 or |
|
128 s.find(_data) >= 0 or |
|
129 s.find(_rowid) >= 0): |
|
130 # Then |
|
131 return 1 |
|
132 else: |
|
133 return 0 |
|
134 |
|
135 |
|
136 class bsdTableDB : |
|
137 def __init__(self, filename, dbhome, create=0, truncate=0, mode=0600, |
|
138 recover=0, dbflags=0): |
|
139 """bsdTableDB(filename, dbhome, create=0, truncate=0, mode=0600) |
|
140 |
|
141 Open database name in the dbhome Berkeley DB directory. |
|
142 Use keyword arguments when calling this constructor. |
|
143 """ |
|
144 self.db = None |
|
145 myflags = db.DB_THREAD |
|
146 if create: |
|
147 myflags |= db.DB_CREATE |
|
148 flagsforenv = (db.DB_INIT_MPOOL | db.DB_INIT_LOCK | db.DB_INIT_LOG | |
|
149 db.DB_INIT_TXN | dbflags) |
|
150 # DB_AUTO_COMMIT isn't a valid flag for env.open() |
|
151 try: |
|
152 dbflags |= db.DB_AUTO_COMMIT |
|
153 except AttributeError: |
|
154 pass |
|
155 if recover: |
|
156 flagsforenv = flagsforenv | db.DB_RECOVER |
|
157 self.env = db.DBEnv() |
|
158 # enable auto deadlock avoidance |
|
159 self.env.set_lk_detect(db.DB_LOCK_DEFAULT) |
|
160 self.env.open(dbhome, myflags | flagsforenv) |
|
161 if truncate: |
|
162 myflags |= db.DB_TRUNCATE |
|
163 self.db = db.DB(self.env) |
|
164 # this code relies on DBCursor.set* methods to raise exceptions |
|
165 # rather than returning None |
|
166 self.db.set_get_returns_none(1) |
|
167 # allow duplicate entries [warning: be careful w/ metadata] |
|
168 self.db.set_flags(db.DB_DUP) |
|
169 self.db.open(filename, db.DB_BTREE, dbflags | myflags, mode) |
|
170 self.dbfilename = filename |
|
171 |
|
172 if sys.version_info[0] >= 3 : |
|
173 class cursor_py3k(object) : |
|
174 def __init__(self, dbcursor) : |
|
175 self._dbcursor = dbcursor |
|
176 |
|
177 def close(self) : |
|
178 return self._dbcursor.close() |
|
179 |
|
180 def set_range(self, search) : |
|
181 v = self._dbcursor.set_range(bytes(search, "iso8859-1")) |
|
182 if v != None : |
|
183 v = (v[0].decode("iso8859-1"), |
|
184 v[1].decode("iso8859-1")) |
|
185 return v |
|
186 |
|
187 def __next__(self) : |
|
188 v = getattr(self._dbcursor, "next")() |
|
189 if v != None : |
|
190 v = (v[0].decode("iso8859-1"), |
|
191 v[1].decode("iso8859-1")) |
|
192 return v |
|
193 |
|
194 class db_py3k(object) : |
|
195 def __init__(self, db) : |
|
196 self._db = db |
|
197 |
|
198 def cursor(self, txn=None) : |
|
199 return cursor_py3k(self._db.cursor(txn=txn)) |
|
200 |
|
201 def has_key(self, key, txn=None) : |
|
202 return getattr(self._db,"has_key")(bytes(key, "iso8859-1"), |
|
203 txn=txn) |
|
204 |
|
205 def put(self, key, value, flags=0, txn=None) : |
|
206 key = bytes(key, "iso8859-1") |
|
207 if value != None : |
|
208 value = bytes(value, "iso8859-1") |
|
209 return self._db.put(key, value, flags=flags, txn=txn) |
|
210 |
|
211 def put_bytes(self, key, value, txn=None) : |
|
212 key = bytes(key, "iso8859-1") |
|
213 return self._db.put(key, value, txn=txn) |
|
214 |
|
215 def get(self, key, txn=None, flags=0) : |
|
216 key = bytes(key, "iso8859-1") |
|
217 v = self._db.get(key, txn=txn, flags=flags) |
|
218 if v != None : |
|
219 v = v.decode("iso8859-1") |
|
220 return v |
|
221 |
|
222 def get_bytes(self, key, txn=None, flags=0) : |
|
223 key = bytes(key, "iso8859-1") |
|
224 return self._db.get(key, txn=txn, flags=flags) |
|
225 |
|
226 def delete(self, key, txn=None) : |
|
227 key = bytes(key, "iso8859-1") |
|
228 return self._db.delete(key, txn=txn) |
|
229 |
|
230 def close (self) : |
|
231 return self._db.close() |
|
232 |
|
233 self.db = db_py3k(self.db) |
|
234 else : # Python 2.x |
|
235 pass |
|
236 |
|
237 # Initialize the table names list if this is a new database |
|
238 txn = self.env.txn_begin() |
|
239 try: |
|
240 if not getattr(self.db, "has_key")(_table_names_key, txn): |
|
241 getattr(self.db, "put_bytes", self.db.put) \ |
|
242 (_table_names_key, pickle.dumps([], 1), txn=txn) |
|
243 # Yes, bare except |
|
244 except: |
|
245 txn.abort() |
|
246 raise |
|
247 else: |
|
248 txn.commit() |
|
249 # TODO verify more of the database's metadata? |
|
250 self.__tablecolumns = {} |
|
251 |
|
252 def __del__(self): |
|
253 self.close() |
|
254 |
|
255 def close(self): |
|
256 if self.db is not None: |
|
257 self.db.close() |
|
258 self.db = None |
|
259 if self.env is not None: |
|
260 self.env.close() |
|
261 self.env = None |
|
262 |
|
263 def checkpoint(self, mins=0): |
|
264 try: |
|
265 self.env.txn_checkpoint(mins) |
|
266 except db.DBIncompleteError: |
|
267 pass |
|
268 |
|
269 def sync(self): |
|
270 try: |
|
271 self.db.sync() |
|
272 except db.DBIncompleteError: |
|
273 pass |
|
274 |
|
275 def _db_print(self) : |
|
276 """Print the database to stdout for debugging""" |
|
277 print "******** Printing raw database for debugging ********" |
|
278 cur = self.db.cursor() |
|
279 try: |
|
280 key, data = cur.first() |
|
281 while 1: |
|
282 print repr({key: data}) |
|
283 next = cur.next() |
|
284 if next: |
|
285 key, data = next |
|
286 else: |
|
287 cur.close() |
|
288 return |
|
289 except db.DBNotFoundError: |
|
290 cur.close() |
|
291 |
|
292 |
|
293 def CreateTable(self, table, columns): |
|
294 """CreateTable(table, columns) - Create a new table in the database. |
|
295 |
|
296 raises TableDBError if it already exists or for other DB errors. |
|
297 """ |
|
298 assert isinstance(columns, list) |
|
299 |
|
300 txn = None |
|
301 try: |
|
302 # checking sanity of the table and column names here on |
|
303 # table creation will prevent problems elsewhere. |
|
304 if contains_metastrings(table): |
|
305 raise ValueError( |
|
306 "bad table name: contains reserved metastrings") |
|
307 for column in columns : |
|
308 if contains_metastrings(column): |
|
309 raise ValueError( |
|
310 "bad column name: contains reserved metastrings") |
|
311 |
|
312 columnlist_key = _columns_key(table) |
|
313 if getattr(self.db, "has_key")(columnlist_key): |
|
314 raise TableAlreadyExists, "table already exists" |
|
315 |
|
316 txn = self.env.txn_begin() |
|
317 # store the table's column info |
|
318 getattr(self.db, "put_bytes", self.db.put)(columnlist_key, |
|
319 pickle.dumps(columns, 1), txn=txn) |
|
320 |
|
321 # add the table name to the tablelist |
|
322 tablelist = pickle.loads(getattr(self.db, "get_bytes", |
|
323 self.db.get) (_table_names_key, txn=txn, flags=db.DB_RMW)) |
|
324 tablelist.append(table) |
|
325 # delete 1st, in case we opened with DB_DUP |
|
326 self.db.delete(_table_names_key, txn=txn) |
|
327 getattr(self.db, "put_bytes", self.db.put)(_table_names_key, |
|
328 pickle.dumps(tablelist, 1), txn=txn) |
|
329 |
|
330 txn.commit() |
|
331 txn = None |
|
332 except db.DBError, dberror: |
|
333 if txn: |
|
334 txn.abort() |
|
335 if sys.version_info[0] < 3 : |
|
336 raise TableDBError, dberror[1] |
|
337 else : |
|
338 raise TableDBError, dberror.args[1] |
|
339 |
|
340 |
|
341 def ListTableColumns(self, table): |
|
342 """Return a list of columns in the given table. |
|
343 [] if the table doesn't exist. |
|
344 """ |
|
345 assert isinstance(table, str) |
|
346 if contains_metastrings(table): |
|
347 raise ValueError, "bad table name: contains reserved metastrings" |
|
348 |
|
349 columnlist_key = _columns_key(table) |
|
350 if not getattr(self.db, "has_key")(columnlist_key): |
|
351 return [] |
|
352 pickledcolumnlist = getattr(self.db, "get_bytes", |
|
353 self.db.get)(columnlist_key) |
|
354 if pickledcolumnlist: |
|
355 return pickle.loads(pickledcolumnlist) |
|
356 else: |
|
357 return [] |
|
358 |
|
359 def ListTables(self): |
|
360 """Return a list of tables in this database.""" |
|
361 pickledtablelist = self.db.get_get(_table_names_key) |
|
362 if pickledtablelist: |
|
363 return pickle.loads(pickledtablelist) |
|
364 else: |
|
365 return [] |
|
366 |
|
367 def CreateOrExtendTable(self, table, columns): |
|
368 """CreateOrExtendTable(table, columns) |
|
369 |
|
370 Create a new table in the database. |
|
371 |
|
372 If a table of this name already exists, extend it to have any |
|
373 additional columns present in the given list as well as |
|
374 all of its current columns. |
|
375 """ |
|
376 assert isinstance(columns, list) |
|
377 |
|
378 try: |
|
379 self.CreateTable(table, columns) |
|
380 except TableAlreadyExists: |
|
381 # the table already existed, add any new columns |
|
382 txn = None |
|
383 try: |
|
384 columnlist_key = _columns_key(table) |
|
385 txn = self.env.txn_begin() |
|
386 |
|
387 # load the current column list |
|
388 oldcolumnlist = pickle.loads( |
|
389 getattr(self.db, "get_bytes", |
|
390 self.db.get)(columnlist_key, txn=txn, flags=db.DB_RMW)) |
|
391 # create a hash table for fast lookups of column names in the |
|
392 # loop below |
|
393 oldcolumnhash = {} |
|
394 for c in oldcolumnlist: |
|
395 oldcolumnhash[c] = c |
|
396 |
|
397 # create a new column list containing both the old and new |
|
398 # column names |
|
399 newcolumnlist = copy.copy(oldcolumnlist) |
|
400 for c in columns: |
|
401 if not oldcolumnhash.has_key(c): |
|
402 newcolumnlist.append(c) |
|
403 |
|
404 # store the table's new extended column list |
|
405 if newcolumnlist != oldcolumnlist : |
|
406 # delete the old one first since we opened with DB_DUP |
|
407 self.db.delete(columnlist_key, txn=txn) |
|
408 getattr(self.db, "put_bytes", self.db.put)(columnlist_key, |
|
409 pickle.dumps(newcolumnlist, 1), |
|
410 txn=txn) |
|
411 |
|
412 txn.commit() |
|
413 txn = None |
|
414 |
|
415 self.__load_column_info(table) |
|
416 except db.DBError, dberror: |
|
417 if txn: |
|
418 txn.abort() |
|
419 if sys.version_info[0] < 3 : |
|
420 raise TableDBError, dberror[1] |
|
421 else : |
|
422 raise TableDBError, dberror.args[1] |
|
423 |
|
424 |
|
425 def __load_column_info(self, table) : |
|
426 """initialize the self.__tablecolumns dict""" |
|
427 # check the column names |
|
428 try: |
|
429 tcolpickles = getattr(self.db, "get_bytes", |
|
430 self.db.get)(_columns_key(table)) |
|
431 except db.DBNotFoundError: |
|
432 raise TableDBError, "unknown table: %r" % (table,) |
|
433 if not tcolpickles: |
|
434 raise TableDBError, "unknown table: %r" % (table,) |
|
435 self.__tablecolumns[table] = pickle.loads(tcolpickles) |
|
436 |
|
437 def __new_rowid(self, table, txn) : |
|
438 """Create a new unique row identifier""" |
|
439 unique = 0 |
|
440 while not unique: |
|
441 # Generate a random 64-bit row ID string |
|
442 # (note: might have <64 bits of true randomness |
|
443 # but it's plenty for our database id needs!) |
|
444 blist = [] |
|
445 for x in xrange(_rowid_str_len): |
|
446 blist.append(random.randint(0,255)) |
|
447 newid = struct.pack('B'*_rowid_str_len, *blist) |
|
448 |
|
449 if sys.version_info[0] >= 3 : |
|
450 newid = newid.decode("iso8859-1") # 8 bits |
|
451 |
|
452 # Guarantee uniqueness by adding this key to the database |
|
453 try: |
|
454 self.db.put(_rowid_key(table, newid), None, txn=txn, |
|
455 flags=db.DB_NOOVERWRITE) |
|
456 except db.DBKeyExistError: |
|
457 pass |
|
458 else: |
|
459 unique = 1 |
|
460 |
|
461 return newid |
|
462 |
|
463 |
|
464 def Insert(self, table, rowdict) : |
|
465 """Insert(table, datadict) - Insert a new row into the table |
|
466 using the keys+values from rowdict as the column values. |
|
467 """ |
|
468 |
|
469 txn = None |
|
470 try: |
|
471 if not getattr(self.db, "has_key")(_columns_key(table)): |
|
472 raise TableDBError, "unknown table" |
|
473 |
|
474 # check the validity of each column name |
|
475 if not self.__tablecolumns.has_key(table): |
|
476 self.__load_column_info(table) |
|
477 for column in rowdict.keys() : |
|
478 if not self.__tablecolumns[table].count(column): |
|
479 raise TableDBError, "unknown column: %r" % (column,) |
|
480 |
|
481 # get a unique row identifier for this row |
|
482 txn = self.env.txn_begin() |
|
483 rowid = self.__new_rowid(table, txn=txn) |
|
484 |
|
485 # insert the row values into the table database |
|
486 for column, dataitem in rowdict.items(): |
|
487 # store the value |
|
488 self.db.put(_data_key(table, column, rowid), dataitem, txn=txn) |
|
489 |
|
490 txn.commit() |
|
491 txn = None |
|
492 |
|
493 except db.DBError, dberror: |
|
494 # WIBNI we could just abort the txn and re-raise the exception? |
|
495 # But no, because TableDBError is not related to DBError via |
|
496 # inheritance, so it would be backwards incompatible. Do the next |
|
497 # best thing. |
|
498 info = sys.exc_info() |
|
499 if txn: |
|
500 txn.abort() |
|
501 self.db.delete(_rowid_key(table, rowid)) |
|
502 if sys.version_info[0] < 3 : |
|
503 raise TableDBError, dberror[1], info[2] |
|
504 else : |
|
505 raise TableDBError, dberror.args[1], info[2] |
|
506 |
|
507 |
|
508 def Modify(self, table, conditions={}, mappings={}): |
|
509 """Modify(table, conditions={}, mappings={}) - Modify items in rows matching 'conditions' using mapping functions in 'mappings' |
|
510 |
|
511 * table - the table name |
|
512 * conditions - a dictionary keyed on column names containing |
|
513 a condition callable expecting the data string as an |
|
514 argument and returning a boolean. |
|
515 * mappings - a dictionary keyed on column names containing a |
|
516 condition callable expecting the data string as an argument and |
|
517 returning the new string for that column. |
|
518 """ |
|
519 |
|
520 try: |
|
521 matching_rowids = self.__Select(table, [], conditions) |
|
522 |
|
523 # modify only requested columns |
|
524 columns = mappings.keys() |
|
525 for rowid in matching_rowids.keys(): |
|
526 txn = None |
|
527 try: |
|
528 for column in columns: |
|
529 txn = self.env.txn_begin() |
|
530 # modify the requested column |
|
531 try: |
|
532 dataitem = self.db.get( |
|
533 _data_key(table, column, rowid), |
|
534 txn=txn) |
|
535 self.db.delete( |
|
536 _data_key(table, column, rowid), |
|
537 txn=txn) |
|
538 except db.DBNotFoundError: |
|
539 # XXXXXXX row key somehow didn't exist, assume no |
|
540 # error |
|
541 dataitem = None |
|
542 dataitem = mappings[column](dataitem) |
|
543 if dataitem <> None: |
|
544 self.db.put( |
|
545 _data_key(table, column, rowid), |
|
546 dataitem, txn=txn) |
|
547 txn.commit() |
|
548 txn = None |
|
549 |
|
550 # catch all exceptions here since we call unknown callables |
|
551 except: |
|
552 if txn: |
|
553 txn.abort() |
|
554 raise |
|
555 |
|
556 except db.DBError, dberror: |
|
557 if sys.version_info[0] < 3 : |
|
558 raise TableDBError, dberror[1] |
|
559 else : |
|
560 raise TableDBError, dberror.args[1] |
|
561 |
|
562 def Delete(self, table, conditions={}): |
|
563 """Delete(table, conditions) - Delete items matching the given |
|
564 conditions from the table. |
|
565 |
|
566 * conditions - a dictionary keyed on column names containing |
|
567 condition functions expecting the data string as an |
|
568 argument and returning a boolean. |
|
569 """ |
|
570 |
|
571 try: |
|
572 matching_rowids = self.__Select(table, [], conditions) |
|
573 |
|
574 # delete row data from all columns |
|
575 columns = self.__tablecolumns[table] |
|
576 for rowid in matching_rowids.keys(): |
|
577 txn = None |
|
578 try: |
|
579 txn = self.env.txn_begin() |
|
580 for column in columns: |
|
581 # delete the data key |
|
582 try: |
|
583 self.db.delete(_data_key(table, column, rowid), |
|
584 txn=txn) |
|
585 except db.DBNotFoundError: |
|
586 # XXXXXXX column may not exist, assume no error |
|
587 pass |
|
588 |
|
589 try: |
|
590 self.db.delete(_rowid_key(table, rowid), txn=txn) |
|
591 except db.DBNotFoundError: |
|
592 # XXXXXXX row key somehow didn't exist, assume no error |
|
593 pass |
|
594 txn.commit() |
|
595 txn = None |
|
596 except db.DBError, dberror: |
|
597 if txn: |
|
598 txn.abort() |
|
599 raise |
|
600 except db.DBError, dberror: |
|
601 if sys.version_info[0] < 3 : |
|
602 raise TableDBError, dberror[1] |
|
603 else : |
|
604 raise TableDBError, dberror.args[1] |
|
605 |
|
606 |
|
607 def Select(self, table, columns, conditions={}): |
|
608 """Select(table, columns, conditions) - retrieve specific row data |
|
609 Returns a list of row column->value mapping dictionaries. |
|
610 |
|
611 * columns - a list of which column data to return. If |
|
612 columns is None, all columns will be returned. |
|
613 * conditions - a dictionary keyed on column names |
|
614 containing callable conditions expecting the data string as an |
|
615 argument and returning a boolean. |
|
616 """ |
|
617 try: |
|
618 if not self.__tablecolumns.has_key(table): |
|
619 self.__load_column_info(table) |
|
620 if columns is None: |
|
621 columns = self.__tablecolumns[table] |
|
622 matching_rowids = self.__Select(table, columns, conditions) |
|
623 except db.DBError, dberror: |
|
624 if sys.version_info[0] < 3 : |
|
625 raise TableDBError, dberror[1] |
|
626 else : |
|
627 raise TableDBError, dberror.args[1] |
|
628 # return the matches as a list of dictionaries |
|
629 return matching_rowids.values() |
|
630 |
|
631 |
|
632 def __Select(self, table, columns, conditions): |
|
633 """__Select() - Used to implement Select and Delete (above) |
|
634 Returns a dictionary keyed on rowids containing dicts |
|
635 holding the row data for columns listed in the columns param |
|
636 that match the given conditions. |
|
637 * conditions is a dictionary keyed on column names |
|
638 containing callable conditions expecting the data string as an |
|
639 argument and returning a boolean. |
|
640 """ |
|
641 # check the validity of each column name |
|
642 if not self.__tablecolumns.has_key(table): |
|
643 self.__load_column_info(table) |
|
644 if columns is None: |
|
645 columns = self.tablecolumns[table] |
|
646 for column in (columns + conditions.keys()): |
|
647 if not self.__tablecolumns[table].count(column): |
|
648 raise TableDBError, "unknown column: %r" % (column,) |
|
649 |
|
650 # keyed on rows that match so far, containings dicts keyed on |
|
651 # column names containing the data for that row and column. |
|
652 matching_rowids = {} |
|
653 # keys are rowids that do not match |
|
654 rejected_rowids = {} |
|
655 |
|
656 # attempt to sort the conditions in such a way as to minimize full |
|
657 # column lookups |
|
658 def cmp_conditions(atuple, btuple): |
|
659 a = atuple[1] |
|
660 b = btuple[1] |
|
661 if type(a) is type(b): |
|
662 if isinstance(a, PrefixCond) and isinstance(b, PrefixCond): |
|
663 # longest prefix first |
|
664 return cmp(len(b.prefix), len(a.prefix)) |
|
665 if isinstance(a, LikeCond) and isinstance(b, LikeCond): |
|
666 # longest likestr first |
|
667 return cmp(len(b.likestr), len(a.likestr)) |
|
668 return 0 |
|
669 if isinstance(a, ExactCond): |
|
670 return -1 |
|
671 if isinstance(b, ExactCond): |
|
672 return 1 |
|
673 if isinstance(a, PrefixCond): |
|
674 return -1 |
|
675 if isinstance(b, PrefixCond): |
|
676 return 1 |
|
677 # leave all unknown condition callables alone as equals |
|
678 return 0 |
|
679 |
|
680 if sys.version_info[0] < 3 : |
|
681 conditionlist = conditions.items() |
|
682 conditionlist.sort(cmp_conditions) |
|
683 else : # Insertion Sort. Please, improve |
|
684 conditionlist = [] |
|
685 for i in conditions.items() : |
|
686 for j, k in enumerate(conditionlist) : |
|
687 r = cmp_conditions(k, i) |
|
688 if r == 1 : |
|
689 conditionlist.insert(j, i) |
|
690 break |
|
691 else : |
|
692 conditionlist.append(i) |
|
693 |
|
694 # Apply conditions to column data to find what we want |
|
695 cur = self.db.cursor() |
|
696 column_num = -1 |
|
697 for column, condition in conditionlist: |
|
698 column_num = column_num + 1 |
|
699 searchkey = _search_col_data_key(table, column) |
|
700 # speedup: don't linear search columns within loop |
|
701 if column in columns: |
|
702 savethiscolumndata = 1 # save the data for return |
|
703 else: |
|
704 savethiscolumndata = 0 # data only used for selection |
|
705 |
|
706 try: |
|
707 key, data = cur.set_range(searchkey) |
|
708 while key[:len(searchkey)] == searchkey: |
|
709 # extract the rowid from the key |
|
710 rowid = key[-_rowid_str_len:] |
|
711 |
|
712 if not rejected_rowids.has_key(rowid): |
|
713 # if no condition was specified or the condition |
|
714 # succeeds, add row to our match list. |
|
715 if not condition or condition(data): |
|
716 if not matching_rowids.has_key(rowid): |
|
717 matching_rowids[rowid] = {} |
|
718 if savethiscolumndata: |
|
719 matching_rowids[rowid][column] = data |
|
720 else: |
|
721 if matching_rowids.has_key(rowid): |
|
722 del matching_rowids[rowid] |
|
723 rejected_rowids[rowid] = rowid |
|
724 |
|
725 key, data = cur.next() |
|
726 |
|
727 except db.DBError, dberror: |
|
728 if sys.version_info[0] < 3 : |
|
729 if dberror[0] != db.DB_NOTFOUND: |
|
730 raise |
|
731 else : |
|
732 if dberror.args[0] != db.DB_NOTFOUND: |
|
733 raise |
|
734 continue |
|
735 |
|
736 cur.close() |
|
737 |
|
738 # we're done selecting rows, garbage collect the reject list |
|
739 del rejected_rowids |
|
740 |
|
741 # extract any remaining desired column data from the |
|
742 # database for the matching rows. |
|
743 if len(columns) > 0: |
|
744 for rowid, rowdata in matching_rowids.items(): |
|
745 for column in columns: |
|
746 if rowdata.has_key(column): |
|
747 continue |
|
748 try: |
|
749 rowdata[column] = self.db.get( |
|
750 _data_key(table, column, rowid)) |
|
751 except db.DBError, dberror: |
|
752 if sys.version_info[0] < 3 : |
|
753 if dberror[0] != db.DB_NOTFOUND: |
|
754 raise |
|
755 else : |
|
756 if dberror.args[0] != db.DB_NOTFOUND: |
|
757 raise |
|
758 rowdata[column] = None |
|
759 |
|
760 # return the matches |
|
761 return matching_rowids |
|
762 |
|
763 |
|
764 def Drop(self, table): |
|
765 """Remove an entire table from the database""" |
|
766 txn = None |
|
767 try: |
|
768 txn = self.env.txn_begin() |
|
769 |
|
770 # delete the column list |
|
771 self.db.delete(_columns_key(table), txn=txn) |
|
772 |
|
773 cur = self.db.cursor(txn) |
|
774 |
|
775 # delete all keys containing this tables column and row info |
|
776 table_key = _search_all_data_key(table) |
|
777 while 1: |
|
778 try: |
|
779 key, data = cur.set_range(table_key) |
|
780 except db.DBNotFoundError: |
|
781 break |
|
782 # only delete items in this table |
|
783 if key[:len(table_key)] != table_key: |
|
784 break |
|
785 cur.delete() |
|
786 |
|
787 # delete all rowids used by this table |
|
788 table_key = _search_rowid_key(table) |
|
789 while 1: |
|
790 try: |
|
791 key, data = cur.set_range(table_key) |
|
792 except db.DBNotFoundError: |
|
793 break |
|
794 # only delete items in this table |
|
795 if key[:len(table_key)] != table_key: |
|
796 break |
|
797 cur.delete() |
|
798 |
|
799 cur.close() |
|
800 |
|
801 # delete the tablename from the table name list |
|
802 tablelist = pickle.loads( |
|
803 getattr(self.db, "get_bytes", self.db.get)(_table_names_key, |
|
804 txn=txn, flags=db.DB_RMW)) |
|
805 try: |
|
806 tablelist.remove(table) |
|
807 except ValueError: |
|
808 # hmm, it wasn't there, oh well, that's what we want. |
|
809 pass |
|
810 # delete 1st, incase we opened with DB_DUP |
|
811 self.db.delete(_table_names_key, txn=txn) |
|
812 getattr(self.db, "put_bytes", self.db.put)(_table_names_key, |
|
813 pickle.dumps(tablelist, 1), txn=txn) |
|
814 |
|
815 txn.commit() |
|
816 txn = None |
|
817 |
|
818 if self.__tablecolumns.has_key(table): |
|
819 del self.__tablecolumns[table] |
|
820 |
|
821 except db.DBError, dberror: |
|
822 if txn: |
|
823 txn.abort() |
|
824 if sys.version_info[0] < 3 : |
|
825 raise TableDBError, dberror[1] |
|
826 else : |
|
827 raise TableDBError, dberror.args[1] |