blob: 2b0a2bf1f31c452c50f57bc682237f132b3cb29a [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
Jesus Ceaca3939c2008-05-22 15:27:38 +000016# the Python Berkeley DB 3 interface.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000017#
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
Barry Warsawf71de3e2003-01-28 17:20:44 +000025import cPickle as pickle
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000026
Barry Warsawf71de3e2003-01-28 17:20:44 +000027try:
Gregory P. Smith41631e82003-09-21 00:08:14 +000028 # For Pythons w/distutils pybsddb
Jesus Cea4907d272008-08-31 14:00:51 +000029 from bsddb3 import db
Gregory P. Smith41631e82003-09-21 00:08:14 +000030except ImportError:
Barry Warsawf71de3e2003-01-28 17:20:44 +000031 # For Python 2.3
Jesus Cea4907d272008-08-31 14:00:51 +000032 from bsddb import db
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000033
Neal Norwitz6aaccc62006-06-11 08:35:14 +000034# XXX(nnorwitz): is this correct? DBIncompleteError is conditional in _bsddb.c
Jesus Cea4907d272008-08-31 14:00:51 +000035if not hasattr(db,"DBIncompleteError") :
Neal Norwitz6aaccc62006-06-11 08:35:14 +000036 class DBIncompleteError(Exception):
37 pass
Jesus Cea4907d272008-08-31 14:00:51 +000038 db.DBIncompleteError = DBIncompleteError
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000039
Barry Warsawf71de3e2003-01-28 17:20:44 +000040class TableDBError(StandardError):
41 pass
42class TableAlreadyExists(TableDBError):
43 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000044
45
46class Cond:
47 """This condition matches everything"""
48 def __call__(self, s):
49 return 1
50
51class 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
58class 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
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +000065class 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
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000072class 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 :
Barry Warsawf71de3e2003-01-28 17:20:44 +000082 likestr = likestr.replace(char, '\\'+char)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000083 # convert %s to wildcards
Barry Warsawf71de3e2003-01-28 17:20:44 +000084 self.likestr = likestr.replace('%', '.*')
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000085 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
Barry Warsawf71de3e2003-01-28 17:20:44 +000094
95def _columns_key(table):
96 return table + _columns
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000097
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
Barry Warsawf71de3e2003-01-28 17:20:44 +0000105
Jesus Cea4907d272008-08-31 14:00:51 +0000106
Barry Warsawf71de3e2003-01-28 17:20:44 +0000107def _data_key(table, col, rowid):
108 return table + _data + col + _data + rowid
109
110def _search_col_data_key(table, col):
111 return table + _data + col + _data
112
113def _search_all_data_key(table):
114 return table + _data
115
116def _rowid_key(table, rowid):
117 return table + _rowid + rowid + _rowid
118
119def _search_rowid_key(table):
120 return table + _rowid
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000121
122def contains_metastrings(s) :
123 """Verify that the given string does not contain any
124 metadata strings that might interfere with dbtables database operation.
125 """
Barry Warsawf71de3e2003-01-28 17:20:44 +0000126 if (s.find(_table_names_key) >= 0 or
127 s.find(_columns) >= 0 or
128 s.find(_data) >= 0 or
129 s.find(_rowid) >= 0):
130 # Then
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000131 return 1
Barry Warsawf71de3e2003-01-28 17:20:44 +0000132 else:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000133 return 0
134
135
136class bsdTableDB :
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000137 def __init__(self, filename, dbhome, create=0, truncate=0, mode=0600,
Barry Warsawf71de3e2003-01-28 17:20:44 +0000138 recover=0, dbflags=0):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000139 """bsdTableDB(filename, dbhome, create=0, truncate=0, mode=0600)
140
Jesus Ceaca3939c2008-05-22 15:27:38 +0000141 Open database name in the dbhome Berkeley DB directory.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000142 Use keyword arguments when calling this constructor.
143 """
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000144 self.db = None
Jesus Cea4907d272008-08-31 14:00:51 +0000145 myflags = db.DB_THREAD
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000146 if create:
Jesus Cea4907d272008-08-31 14:00:51 +0000147 myflags |= db.DB_CREATE
148 flagsforenv = (db.DB_INIT_MPOOL | db.DB_INIT_LOCK | db.DB_INIT_LOG |
149 db.DB_INIT_TXN | dbflags)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000150 # DB_AUTO_COMMIT isn't a valid flag for env.open()
151 try:
Jesus Cea4907d272008-08-31 14:00:51 +0000152 dbflags |= db.DB_AUTO_COMMIT
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000153 except AttributeError:
154 pass
155 if recover:
Jesus Cea4907d272008-08-31 14:00:51 +0000156 flagsforenv = flagsforenv | db.DB_RECOVER
157 self.env = db.DBEnv()
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000158 # enable auto deadlock avoidance
Jesus Cea4907d272008-08-31 14:00:51 +0000159 self.env.set_lk_detect(db.DB_LOCK_DEFAULT)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000160 self.env.open(dbhome, myflags | flagsforenv)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000161 if truncate:
Jesus Cea4907d272008-08-31 14:00:51 +0000162 myflags |= db.DB_TRUNCATE
163 self.db = db.DB(self.env)
Gregory P. Smith455d46f2003-07-09 04:45:59 +0000164 # this code relies on DBCursor.set* methods to raise exceptions
165 # rather than returning None
166 self.db.set_get_returns_none(1)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000167 # allow duplicate entries [warning: be careful w/ metadata]
Jesus Cea4907d272008-08-31 14:00:51 +0000168 self.db.set_flags(db.DB_DUP)
169 self.db.open(filename, db.DB_BTREE, dbflags | myflags, mode)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000170 self.dbfilename = filename
Jesus Cea4907d272008-08-31 14:00:51 +0000171
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
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000237 # Initialize the table names list if this is a new database
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000238 txn = self.env.txn_begin()
239 try:
Jesus Cea4907d272008-08-31 14:00:51 +0000240 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)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000243 # Yes, bare except
244 except:
245 txn.abort()
246 raise
247 else:
248 txn.commit()
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000249 # TODO verify more of the database's metadata?
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000250 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)
Jesus Cea4907d272008-08-31 14:00:51 +0000266 except db.DBIncompleteError:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000267 pass
268
269 def sync(self):
270 try:
271 self.db.sync()
Jesus Cea4907d272008-08-31 14:00:51 +0000272 except db.DBIncompleteError:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000273 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()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000281 while 1:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000282 print repr({key: data})
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000283 next = cur.next()
284 if next:
285 key, data = next
286 else:
287 cur.close()
288 return
Jesus Cea4907d272008-08-31 14:00:51 +0000289 except db.DBNotFoundError:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000290 cur.close()
291
292
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000293 def CreateTable(self, table, columns):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000294 """CreateTable(table, columns) - Create a new table in the database.
295
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000296 raises TableDBError if it already exists or for other DB errors.
297 """
Jesus Ceac5a11fa2008-07-23 11:38:42 +0000298 assert isinstance(columns, list)
Jesus Cea4907d272008-08-31 14:00:51 +0000299
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000300 txn = None
301 try:
302 # checking sanity of the table and column names here on
303 # table creation will prevent problems elsewhere.
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000304 if contains_metastrings(table):
305 raise ValueError(
306 "bad table name: contains reserved metastrings")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000307 for column in columns :
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000308 if contains_metastrings(column):
309 raise ValueError(
310 "bad column name: contains reserved metastrings")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000311
312 columnlist_key = _columns_key(table)
Jesus Cea4907d272008-08-31 14:00:51 +0000313 if getattr(self.db, "has_key")(columnlist_key):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000314 raise TableAlreadyExists, "table already exists"
315
316 txn = self.env.txn_begin()
317 # store the table's column info
Jesus Cea4907d272008-08-31 14:00:51 +0000318 getattr(self.db, "put_bytes", self.db.put)(columnlist_key,
319 pickle.dumps(columns, 1), txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000320
321 # add the table name to the tablelist
Jesus Cea4907d272008-08-31 14:00:51 +0000322 tablelist = pickle.loads(getattr(self.db, "get_bytes",
323 self.db.get) (_table_names_key, txn=txn, flags=db.DB_RMW))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000324 tablelist.append(table)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000325 # delete 1st, in case we opened with DB_DUP
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000326 self.db.delete(_table_names_key, txn=txn)
Jesus Cea4907d272008-08-31 14:00:51 +0000327 getattr(self.db, "put_bytes", self.db.put)(_table_names_key,
328 pickle.dumps(tablelist, 1), txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000329
330 txn.commit()
331 txn = None
Jesus Cea4907d272008-08-31 14:00:51 +0000332 except db.DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000333 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000334 txn.abort()
Jesus Cea4907d272008-08-31 14:00:51 +0000335 if sys.version_info[0] < 3 :
336 raise TableDBError, dberror[1]
337 else :
338 raise TableDBError, dberror.args[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000339
340
341 def ListTableColumns(self, table):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000342 """Return a list of columns in the given table.
343 [] if the table doesn't exist.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000344 """
Jesus Ceac5a11fa2008-07-23 11:38:42 +0000345 assert isinstance(table, str)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000346 if contains_metastrings(table):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000347 raise ValueError, "bad table name: contains reserved metastrings"
348
349 columnlist_key = _columns_key(table)
Jesus Cea4907d272008-08-31 14:00:51 +0000350 if not getattr(self.db, "has_key")(columnlist_key):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000351 return []
Jesus Cea4907d272008-08-31 14:00:51 +0000352 pickledcolumnlist = getattr(self.db, "get_bytes",
353 self.db.get)(columnlist_key)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000354 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."""
Jesus Cea4907d272008-08-31 14:00:51 +0000361 pickledtablelist = self.db.get_get(_table_names_key)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000362 if pickledtablelist:
363 return pickle.loads(pickledtablelist)
364 else:
365 return []
366
367 def CreateOrExtendTable(self, table, columns):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000368 """CreateOrExtendTable(table, columns)
369
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000370 Create a new table in the database.
371
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000372 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 """
Jesus Ceac5a11fa2008-07-23 11:38:42 +0000376 assert isinstance(columns, list)
Jesus Cea4907d272008-08-31 14:00:51 +0000377
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000378 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
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000388 oldcolumnlist = pickle.loads(
Jesus Cea4907d272008-08-31 14:00:51 +0000389 getattr(self.db, "get_bytes",
390 self.db.get)(columnlist_key, txn=txn, flags=db.DB_RMW))
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000391 # create a hash table for fast lookups of column names in the
392 # loop below
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000393 oldcolumnhash = {}
394 for c in oldcolumnlist:
395 oldcolumnhash[c] = c
396
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000397 # create a new column list containing both the old and new
398 # column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000399 newcolumnlist = copy.copy(oldcolumnlist)
400 for c in columns:
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000401 if not c in oldcolumnhash:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000402 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
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000407 self.db.delete(columnlist_key, txn=txn)
Jesus Cea4907d272008-08-31 14:00:51 +0000408 getattr(self.db, "put_bytes", self.db.put)(columnlist_key,
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000409 pickle.dumps(newcolumnlist, 1),
410 txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000411
412 txn.commit()
413 txn = None
414
415 self.__load_column_info(table)
Jesus Cea4907d272008-08-31 14:00:51 +0000416 except db.DBError, dberror:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000417 if txn:
418 txn.abort()
Jesus Cea4907d272008-08-31 14:00:51 +0000419 if sys.version_info[0] < 3 :
420 raise TableDBError, dberror[1]
421 else :
422 raise TableDBError, dberror.args[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000423
424
425 def __load_column_info(self, table) :
426 """initialize the self.__tablecolumns dict"""
427 # check the column names
428 try:
Jesus Cea4907d272008-08-31 14:00:51 +0000429 tcolpickles = getattr(self.db, "get_bytes",
430 self.db.get)(_columns_key(table))
431 except db.DBNotFoundError:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000432 raise TableDBError, "unknown table: %r" % (table,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000433 if not tcolpickles:
Walter Dörwald70a6b492004-02-12 17:35:32 +0000434 raise TableDBError, "unknown table: %r" % (table,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000435 self.__tablecolumns[table] = pickle.loads(tcolpickles)
436
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000437 def __new_rowid(self, table, txn) :
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000438 """Create a new unique row identifier"""
439 unique = 0
Barry Warsawf71de3e2003-01-28 17:20:44 +0000440 while not unique:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000441 # Generate a random 64-bit row ID string
Gregory P. Smith6d331ca2007-11-01 21:15:36 +0000442 # (note: might have <64 bits of true randomness
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000443 # but it's plenty for our database id needs!)
Gregory P. Smith3ef21cb2007-10-18 16:32:02 +0000444 blist = []
445 for x in xrange(_rowid_str_len):
Gregory P. Smith6d331ca2007-11-01 21:15:36 +0000446 blist.append(random.randint(0,255))
Gregory P. Smith3ef21cb2007-10-18 16:32:02 +0000447 newid = struct.pack('B'*_rowid_str_len, *blist)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000448
Jesus Cea4907d272008-08-31 14:00:51 +0000449 if sys.version_info[0] >= 3 :
450 newid = newid.decode("iso8859-1") # 8 bits
451
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000452 # Guarantee uniqueness by adding this key to the database
453 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000454 self.db.put(_rowid_key(table, newid), None, txn=txn,
Jesus Cea4907d272008-08-31 14:00:51 +0000455 flags=db.DB_NOOVERWRITE)
456 except db.DBKeyExistError:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000457 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 """
Jesus Cea4907d272008-08-31 14:00:51 +0000468
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000469 txn = None
470 try:
Jesus Cea4907d272008-08-31 14:00:51 +0000471 if not getattr(self.db, "has_key")(_columns_key(table)):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000472 raise TableDBError, "unknown table"
473
474 # check the validity of each column name
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000475 if not table in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000476 self.__load_column_info(table)
477 for column in rowdict.keys() :
Barry Warsawf71de3e2003-01-28 17:20:44 +0000478 if not self.__tablecolumns[table].count(column):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000479 raise TableDBError, "unknown column: %r" % (column,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000480
481 # get a unique row identifier for this row
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000482 txn = self.env.txn_begin()
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000483 rowid = self.__new_rowid(table, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000484
485 # insert the row values into the table database
Barry Warsawf71de3e2003-01-28 17:20:44 +0000486 for column, dataitem in rowdict.items():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000487 # store the value
488 self.db.put(_data_key(table, column, rowid), dataitem, txn=txn)
489
490 txn.commit()
491 txn = None
492
Jesus Cea4907d272008-08-31 14:00:51 +0000493 except db.DBError, dberror:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000494 # 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:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000500 txn.abort()
501 self.db.delete(_rowid_key(table, rowid))
Jesus Cea4907d272008-08-31 14:00:51 +0000502 if sys.version_info[0] < 3 :
503 raise TableDBError, dberror[1], info[2]
504 else :
505 raise TableDBError, dberror.args[1], info[2]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000506
507
Barry Warsawf71de3e2003-01-28 17:20:44 +0000508 def Modify(self, table, conditions={}, mappings={}):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000509 """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.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000518 """
Jesus Cea4907d272008-08-31 14:00:51 +0000519
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000520 try:
521 matching_rowids = self.__Select(table, [], conditions)
522
523 # modify only requested columns
524 columns = mappings.keys()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000525 for rowid in matching_rowids.keys():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000526 txn = None
527 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000528 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000529 txn = self.env.txn_begin()
530 # modify the requested column
531 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000532 dataitem = self.db.get(
533 _data_key(table, column, rowid),
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000534 txn=txn)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000535 self.db.delete(
536 _data_key(table, column, rowid),
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000537 txn=txn)
Jesus Cea4907d272008-08-31 14:00:51 +0000538 except db.DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000539 # XXXXXXX row key somehow didn't exist, assume no
540 # error
541 dataitem = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000542 dataitem = mappings[column](dataitem)
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000543 if dataitem != None:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000544 self.db.put(
545 _data_key(table, column, rowid),
546 dataitem, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000547 txn.commit()
548 txn = None
549
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000550 # catch all exceptions here since we call unknown callables
551 except:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000552 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000553 txn.abort()
554 raise
555
Jesus Cea4907d272008-08-31 14:00:51 +0000556 except db.DBError, dberror:
557 if sys.version_info[0] < 3 :
558 raise TableDBError, dberror[1]
559 else :
560 raise TableDBError, dberror.args[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000561
Barry Warsawf71de3e2003-01-28 17:20:44 +0000562 def Delete(self, table, conditions={}):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000563 """Delete(table, conditions) - Delete items matching the given
564 conditions from the table.
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000565
566 * conditions - a dictionary keyed on column names containing
567 condition functions expecting the data string as an
568 argument and returning a boolean.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000569 """
Jesus Cea4907d272008-08-31 14:00:51 +0000570
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000571 try:
572 matching_rowids = self.__Select(table, [], conditions)
573
574 # delete row data from all columns
575 columns = self.__tablecolumns[table]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000576 for rowid in matching_rowids.keys():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000577 txn = None
578 try:
579 txn = self.env.txn_begin()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000580 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000581 # delete the data key
582 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000583 self.db.delete(_data_key(table, column, rowid),
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000584 txn=txn)
Jesus Cea4907d272008-08-31 14:00:51 +0000585 except db.DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000586 # XXXXXXX column may not exist, assume no error
587 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000588
589 try:
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000590 self.db.delete(_rowid_key(table, rowid), txn=txn)
Jesus Cea4907d272008-08-31 14:00:51 +0000591 except db.DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000592 # XXXXXXX row key somehow didn't exist, assume no error
593 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000594 txn.commit()
595 txn = None
Jesus Cea4907d272008-08-31 14:00:51 +0000596 except db.DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000597 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000598 txn.abort()
599 raise
Jesus Cea4907d272008-08-31 14:00:51 +0000600 except db.DBError, dberror:
601 if sys.version_info[0] < 3 :
602 raise TableDBError, dberror[1]
603 else :
604 raise TableDBError, dberror.args[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000605
606
Barry Warsawf71de3e2003-01-28 17:20:44 +0000607 def Select(self, table, columns, conditions={}):
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000608 """Select(table, columns, conditions) - retrieve specific row data
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000609 Returns a list of row column->value mapping dictionaries.
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000610
611 * columns - a list of which column data to return. If
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000612 columns is None, all columns will be returned.
Gregory P. Smithff7d9912006-06-08 05:17:08 +0000613 * conditions - a dictionary keyed on column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000614 containing callable conditions expecting the data string as an
615 argument and returning a boolean.
616 """
617 try:
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000618 if not table in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000619 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000620 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000621 columns = self.__tablecolumns[table]
622 matching_rowids = self.__Select(table, columns, conditions)
Jesus Cea4907d272008-08-31 14:00:51 +0000623 except db.DBError, dberror:
624 if sys.version_info[0] < 3 :
625 raise TableDBError, dberror[1]
626 else :
627 raise TableDBError, dberror.args[1]
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000628 # return the matches as a list of dictionaries
629 return matching_rowids.values()
630
631
Barry Warsawf71de3e2003-01-28 17:20:44 +0000632 def __Select(self, table, columns, conditions):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000633 """__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
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000642 if not table in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000643 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000644 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000645 columns = self.tablecolumns[table]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000646 for column in (columns + conditions.keys()):
647 if not self.__tablecolumns[table].count(column):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000648 raise TableDBError, "unknown column: %r" % (column,)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000649
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 = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000653 # keys are rowids that do not match
654 rejected_rowids = {}
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000655
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000656 # attempt to sort the conditions in such a way as to minimize full
657 # column lookups
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000658 def cmp_conditions(atuple, btuple):
659 a = atuple[1]
660 b = btuple[1]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000661 if type(a) is type(b):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000662 if isinstance(a, PrefixCond) and isinstance(b, PrefixCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000663 # longest prefix first
664 return cmp(len(b.prefix), len(a.prefix))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000665 if isinstance(a, LikeCond) and isinstance(b, LikeCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000666 # longest likestr first
667 return cmp(len(b.likestr), len(a.likestr))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000668 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
Jesus Cea4907d272008-08-31 14:00:51 +0000680 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)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000693
694 # Apply conditions to column data to find what we want
695 cur = self.db.cursor()
696 column_num = -1
Barry Warsawf71de3e2003-01-28 17:20:44 +0000697 for column, condition in conditionlist:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000698 column_num = column_num + 1
699 searchkey = _search_col_data_key(table, column)
700 # speedup: don't linear search columns within loop
Barry Warsawf71de3e2003-01-28 17:20:44 +0000701 if column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000702 savethiscolumndata = 1 # save the data for return
Barry Warsawf71de3e2003-01-28 17:20:44 +0000703 else:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000704 savethiscolumndata = 0 # data only used for selection
705
706 try:
707 key, data = cur.set_range(searchkey)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000708 while key[:len(searchkey)] == searchkey:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000709 # extract the rowid from the key
710 rowid = key[-_rowid_str_len:]
711
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000712 if not rowid in rejected_rowids:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000713 # if no condition was specified or the condition
714 # succeeds, add row to our match list.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000715 if not condition or condition(data):
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000716 if not rowid in matching_rowids:
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000717 matching_rowids[rowid] = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000718 if savethiscolumndata:
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000719 matching_rowids[rowid][column] = data
Barry Warsawf71de3e2003-01-28 17:20:44 +0000720 else:
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000721 if rowid in matching_rowids:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000722 del matching_rowids[rowid]
723 rejected_rowids[rowid] = rowid
724
725 key, data = cur.next()
726
Jesus Cea4907d272008-08-31 14:00:51 +0000727 except db.DBError, dberror:
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000728 if dberror.args[0] != db.DB_NOTFOUND:
729 raise
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000730 continue
731
732 cur.close()
733
734 # we're done selecting rows, garbage collect the reject list
735 del rejected_rowids
736
737 # extract any remaining desired column data from the
738 # database for the matching rows.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000739 if len(columns) > 0:
740 for rowid, rowdata in matching_rowids.items():
741 for column in columns:
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000742 if column in rowdata:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000743 continue
744 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000745 rowdata[column] = self.db.get(
746 _data_key(table, column, rowid))
Jesus Cea4907d272008-08-31 14:00:51 +0000747 except db.DBError, dberror:
748 if sys.version_info[0] < 3 :
749 if dberror[0] != db.DB_NOTFOUND:
750 raise
751 else :
752 if dberror.args[0] != db.DB_NOTFOUND:
753 raise
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000754 rowdata[column] = None
755
756 # return the matches
757 return matching_rowids
758
759
Barry Warsawf71de3e2003-01-28 17:20:44 +0000760 def Drop(self, table):
761 """Remove an entire table from the database"""
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000762 txn = None
763 try:
764 txn = self.env.txn_begin()
765
766 # delete the column list
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000767 self.db.delete(_columns_key(table), txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000768
769 cur = self.db.cursor(txn)
770
771 # delete all keys containing this tables column and row info
772 table_key = _search_all_data_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000773 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000774 try:
775 key, data = cur.set_range(table_key)
Jesus Cea4907d272008-08-31 14:00:51 +0000776 except db.DBNotFoundError:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000777 break
778 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000779 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000780 break
781 cur.delete()
782
783 # delete all rowids used by this table
784 table_key = _search_rowid_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000785 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000786 try:
787 key, data = cur.set_range(table_key)
Jesus Cea4907d272008-08-31 14:00:51 +0000788 except db.DBNotFoundError:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000789 break
790 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000791 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000792 break
793 cur.delete()
794
795 cur.close()
796
797 # delete the tablename from the table name list
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000798 tablelist = pickle.loads(
Jesus Cea4907d272008-08-31 14:00:51 +0000799 getattr(self.db, "get_bytes", self.db.get)(_table_names_key,
800 txn=txn, flags=db.DB_RMW))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000801 try:
802 tablelist.remove(table)
803 except ValueError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000804 # hmm, it wasn't there, oh well, that's what we want.
805 pass
806 # delete 1st, incase we opened with DB_DUP
Gregory P. Smithafed3a42007-10-18 07:56:54 +0000807 self.db.delete(_table_names_key, txn=txn)
Jesus Cea4907d272008-08-31 14:00:51 +0000808 getattr(self.db, "put_bytes", self.db.put)(_table_names_key,
809 pickle.dumps(tablelist, 1), txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000810
811 txn.commit()
812 txn = None
813
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000814 if table in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000815 del self.__tablecolumns[table]
816
Jesus Cea4907d272008-08-31 14:00:51 +0000817 except db.DBError, dberror:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000818 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000819 txn.abort()
Antoine Pitrou63b0cb22009-10-14 18:01:33 +0000820 raise TableDBError(dberror.args[1])