blob: 85a13ff72ad6a75cb40773cd945cf53d6938fe00 [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. Smith3fd22da2007-08-28 08:05:56 +000013# -- Gregory P. Smith <greg@krypto.org>
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000014
15# This provides a simple database table interface built on top of
16# the Python BerkeleyDB 3 interface.
17#
18_cvsid = '$Id$'
19
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000020import re
Barry Warsawf71de3e2003-01-28 17:20:44 +000021import sys
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000022import copy
Gregory P. Smith66077d82007-10-18 16:55:12 +000023import struct
Tim Peters95334a52004-08-08 00:54:21 +000024import random
Guido van Rossum99603b02007-07-20 00:22:32 +000025import pickle
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000026
Martin v. Löwiscccc58d2007-08-10 08:36:56 +000027from bsddb.db import *
28
29# All table names, row names etc. must be ASCII strings
Martin v. Löwis32ca4422007-08-11 06:13:20 +000030# However, rowids, when represented as strings, are latin-1 encoded
Martin v. Löwiscccc58d2007-08-10 08:36:56 +000031def _E(s):
32 return s.encode("ascii")
33
Thomas Wouters0e3f5912006-08-11 14:57:12 +000034# XXX(nnorwitz): is this correct? DBIncompleteError is conditional in _bsddb.c
35try:
36 DBIncompleteError
37except NameError:
38 class DBIncompleteError(Exception):
39 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000040
Guido van Rossumcd16bf62007-06-13 18:07:49 +000041class TableDBError(Exception):
Barry Warsawf71de3e2003-01-28 17:20:44 +000042 pass
43class TableAlreadyExists(TableDBError):
44 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000045
46
47class Cond:
48 """This condition matches everything"""
49 def __call__(self, s):
50 return 1
51
52class ExactCond(Cond):
53 """Acts as an exact match condition function"""
Martin v. Löwis32ca4422007-08-11 06:13:20 +000054 def __init__(self, strtomatch, encoding="utf-8"):
55 self.strtomatch = strtomatch.encode(encoding)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000056 def __call__(self, s):
57 return s == self.strtomatch
58
59class PrefixCond(Cond):
60 """Acts as a condition function for matching a string prefix"""
Martin v. Löwis32ca4422007-08-11 06:13:20 +000061 def __init__(self, prefix, encoding="utf-8"):
62 self.prefix = prefix.encode(encoding)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000063 def __call__(self, s):
64 return s[:len(self.prefix)] == self.prefix
65
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +000066class PostfixCond(Cond):
67 """Acts as a condition function for matching a string postfix"""
Martin v. Löwis32ca4422007-08-11 06:13:20 +000068 def __init__(self, postfix, encoding="utf-8"):
69 self.postfix = postfix.encode(encoding)
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +000070 def __call__(self, s):
71 return s[-len(self.postfix):] == self.postfix
72
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000073class LikeCond(Cond):
74 """
75 Acts as a function that will match using an SQL 'LIKE' style
76 string. Case insensitive and % signs are wild cards.
77 This isn't perfect but it should work for the simple common cases.
78 """
Martin v. Löwis32ca4422007-08-11 06:13:20 +000079 def __init__(self, likestr, re_flags=re.IGNORECASE, encoding="utf-8"):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000080 # escape python re characters
81 chars_to_escape = '.*+()[]?'
82 for char in chars_to_escape :
Barry Warsawf71de3e2003-01-28 17:20:44 +000083 likestr = likestr.replace(char, '\\'+char)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000084 # convert %s to wildcards
Barry Warsawf71de3e2003-01-28 17:20:44 +000085 self.likestr = likestr.replace('%', '.*')
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000086 self.re = re.compile('^'+self.likestr+'$', re_flags)
Martin v. Löwis32ca4422007-08-11 06:13:20 +000087 self.encoding = encoding
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000088 def __call__(self, s):
Martin v. Löwis32ca4422007-08-11 06:13:20 +000089 return self.re.match(s.decode(self.encoding))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +000090
91#
92# keys used to store database metadata
93#
Martin v. Löwiscccc58d2007-08-10 08:36:56 +000094_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):
Martin v. Löwiscccc58d2007-08-10 08:36:56 +000098 return _E(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):
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000109 return _E(table + _data + col + _data) + rowid
Barry Warsawf71de3e2003-01-28 17:20:44 +0000110
111def _search_col_data_key(table, col):
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000112 return _E(table + _data + col + _data)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000113
114def _search_all_data_key(table):
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000115 return _E(table + _data)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000116
117def _rowid_key(table, rowid):
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000118 return _E(table + _rowid) + rowid + _E(_rowid)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000119
120def _search_rowid_key(table):
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000121 return _E(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 :
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000138 def __init__(self, filename, dbhome, create=0, truncate=0, mode=0o600,
Barry Warsawf71de3e2003-01-28 17:20:44 +0000139 recover=0, dbflags=0):
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000140 """bsdTableDB(filename, dbhome, create=0, truncate=0, mode=0o600)
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000141
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:
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000175 if not self.db.has_key(_E(_table_names_key), txn):
176 self.db.put(_E(_table_names_key), pickle.dumps([], 1), txn=txn)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000177 # 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"""
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000211 print("******** Printing raw database for debugging ********")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000212 cur = self.db.cursor()
213 try:
214 key, data = cur.first()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000215 while 1:
Guido van Rossumbe19ed72007-02-09 05:37:30 +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):
Thomas Wouters73e5a5b2006-06-08 15:35:45 +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 """
Guido van Rossum13257902007-06-07 23:15:56 +0000232 assert isinstance(columns, list)
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):
Collin Wintera65e94c2007-08-22 21:45:20 +0000247 raise TableAlreadyExists("table already exists")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000248
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
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000254 tablelist = pickle.loads(self.db.get(_E(_table_names_key), txn=txn,
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000255 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
Gregory P. Smith66077d82007-10-18 16:55:12 +0000258 self.db.delete(_E(_table_names_key), txn=txn)
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000259 self.db.put(_E(_table_names_key), pickle.dumps(tablelist, 1), txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000260
261 txn.commit()
262 txn = None
Guido van Rossumb940e112007-01-10 16:19:56 +0000263 except DBError as dberror:
Collin Wintera65e94c2007-08-22 21:45:20 +0000264 raise TableDBError(dberror.args[1])
Martin v. Löwis32ca4422007-08-11 06:13:20 +0000265 finally:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000266 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000267 txn.abort()
Martin v. Löwis32ca4422007-08-11 06:13:20 +0000268 txn = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000269
270 def ListTableColumns(self, table):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000271 """Return a list of columns in the given table.
272 [] if the table doesn't exist.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000273 """
Guido van Rossum13257902007-06-07 23:15:56 +0000274 assert isinstance(table, str)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000275 if contains_metastrings(table):
Collin Wintera65e94c2007-08-22 21:45:20 +0000276 raise ValueError("bad table name: contains reserved metastrings")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000277
278 columnlist_key = _columns_key(table)
279 if not self.db.has_key(columnlist_key):
280 return []
281 pickledcolumnlist = self.db.get(columnlist_key)
282 if pickledcolumnlist:
283 return pickle.loads(pickledcolumnlist)
284 else:
285 return []
286
287 def ListTables(self):
288 """Return a list of tables in this database."""
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000289 pickledtablelist = self.db.get(_E(_table_names_key))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000290 if pickledtablelist:
291 return pickle.loads(pickledtablelist)
292 else:
293 return []
294
295 def CreateOrExtendTable(self, table, columns):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000296 """CreateOrExtendTable(table, columns)
297
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000298 Create a new table in the database.
299
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000300 If a table of this name already exists, extend it to have any
301 additional columns present in the given list as well as
302 all of its current columns.
303 """
Guido van Rossum13257902007-06-07 23:15:56 +0000304 assert isinstance(columns, list)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000305 try:
306 self.CreateTable(table, columns)
307 except TableAlreadyExists:
308 # the table already existed, add any new columns
309 txn = None
310 try:
311 columnlist_key = _columns_key(table)
312 txn = self.env.txn_begin()
313
314 # load the current column list
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000315 oldcolumnlist = pickle.loads(
316 self.db.get(columnlist_key, txn=txn, flags=DB_RMW))
317 # create a hash table for fast lookups of column names in the
318 # loop below
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000319 oldcolumnhash = {}
320 for c in oldcolumnlist:
321 oldcolumnhash[c] = c
322
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000323 # create a new column list containing both the old and new
324 # column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000325 newcolumnlist = copy.copy(oldcolumnlist)
326 for c in columns:
Guido van Rossum20435132006-08-21 00:21:47 +0000327 if c not in oldcolumnhash:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000328 newcolumnlist.append(c)
329
330 # store the table's new extended column list
331 if newcolumnlist != oldcolumnlist :
332 # delete the old one first since we opened with DB_DUP
Gregory P. Smith66077d82007-10-18 16:55:12 +0000333 self.db.delete(columnlist_key, txn=txn)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000334 self.db.put(columnlist_key,
335 pickle.dumps(newcolumnlist, 1),
336 txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000337
338 txn.commit()
339 txn = None
340
341 self.__load_column_info(table)
Guido van Rossumb940e112007-01-10 16:19:56 +0000342 except DBError as dberror:
Collin Wintera65e94c2007-08-22 21:45:20 +0000343 raise TableDBError(dberror.args[1])
Martin v. Löwis32ca4422007-08-11 06:13:20 +0000344 finally:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000345 if txn:
346 txn.abort()
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000347
348
349 def __load_column_info(self, table) :
350 """initialize the self.__tablecolumns dict"""
351 # check the column names
352 try:
353 tcolpickles = self.db.get(_columns_key(table))
354 except DBNotFoundError:
Collin Wintera65e94c2007-08-22 21:45:20 +0000355 raise TableDBError("unknown table: %r" % (table,))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000356 if not tcolpickles:
Collin Wintera65e94c2007-08-22 21:45:20 +0000357 raise TableDBError("unknown table: %r" % (table,))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000358 self.__tablecolumns[table] = pickle.loads(tcolpickles)
359
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000360 def __new_rowid(self, table, txn) :
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000361 """Create a new unique row identifier"""
362 unique = 0
Barry Warsawf71de3e2003-01-28 17:20:44 +0000363 while not unique:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000364 # Generate a random 64-bit row ID string
365 # (note: this code has <64 bits of randomness
366 # but it's plenty for our database id needs!)
Gregory P. Smith66077d82007-10-18 16:55:12 +0000367 # We must ensure that no null bytes are in the id value.
368 blist = []
369 for x in range(_rowid_str_len):
370 blist.append(random.randint(1,255))
371 newid = bytes(blist)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000372
373 # Guarantee uniqueness by adding this key to the database
374 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000375 self.db.put(_rowid_key(table, newid), None, txn=txn,
376 flags=DB_NOOVERWRITE)
377 except DBKeyExistError:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000378 pass
379 else:
380 unique = 1
381
382 return newid
383
384
385 def Insert(self, table, rowdict) :
386 """Insert(table, datadict) - Insert a new row into the table
387 using the keys+values from rowdict as the column values.
388 """
389 txn = None
390 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000391 if not self.db.has_key(_columns_key(table)):
Collin Wintera65e94c2007-08-22 21:45:20 +0000392 raise TableDBError("unknown table")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000393
394 # check the validity of each column name
Guido van Rossum20435132006-08-21 00:21:47 +0000395 if table not in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000396 self.__load_column_info(table)
397 for column in rowdict.keys() :
Barry Warsawf71de3e2003-01-28 17:20:44 +0000398 if not self.__tablecolumns[table].count(column):
Collin Wintera65e94c2007-08-22 21:45:20 +0000399 raise TableDBError("unknown column: %r" % (column,))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000400
401 # get a unique row identifier for this row
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000402 txn = self.env.txn_begin()
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000403 rowid = self.__new_rowid(table, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000404
405 # insert the row values into the table database
Barry Warsawf71de3e2003-01-28 17:20:44 +0000406 for column, dataitem in rowdict.items():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000407 # store the value
408 self.db.put(_data_key(table, column, rowid), dataitem, txn=txn)
409
410 txn.commit()
411 txn = None
412
Guido van Rossumb940e112007-01-10 16:19:56 +0000413 except DBError as dberror:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000414 # WIBNI we could just abort the txn and re-raise the exception?
415 # But no, because TableDBError is not related to DBError via
416 # inheritance, so it would be backwards incompatible. Do the next
417 # best thing.
418 info = sys.exc_info()
419 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000420 txn.abort()
421 self.db.delete(_rowid_key(table, rowid))
Martin v. Löwis32ca4422007-08-11 06:13:20 +0000422 txn = None
Collin Wintera65e94c2007-08-22 21:45:20 +0000423 raise TableDBError(dberror.args[1]).with_traceback(info[2])
Martin v. Löwis32ca4422007-08-11 06:13:20 +0000424 finally:
425 if txn:
426 txn.abort()
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000427
428
Barry Warsawf71de3e2003-01-28 17:20:44 +0000429 def Modify(self, table, conditions={}, mappings={}):
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000430 """Modify(table, conditions={}, mappings={}) - Modify items in rows matching 'conditions' using mapping functions in 'mappings'
431
432 * table - the table name
433 * conditions - a dictionary keyed on column names containing
434 a condition callable expecting the data string as an
435 argument and returning a boolean.
436 * mappings - a dictionary keyed on column names containing a
437 condition callable expecting the data string as an argument and
438 returning the new string for that column.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000439 """
440 try:
441 matching_rowids = self.__Select(table, [], conditions)
442
443 # modify only requested columns
444 columns = mappings.keys()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000445 for rowid in matching_rowids.keys():
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000446 rowid = rowid.encode("latin-1")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000447 txn = None
448 try:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000449 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000450 txn = self.env.txn_begin()
451 # modify the requested column
452 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000453 dataitem = self.db.get(
454 _data_key(table, column, rowid),
Gregory P. Smith66077d82007-10-18 16:55:12 +0000455 txn=txn)
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000456 self.db.delete(
457 _data_key(table, column, rowid),
Gregory P. Smith66077d82007-10-18 16:55:12 +0000458 txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000459 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000460 # XXXXXXX row key somehow didn't exist, assume no
461 # error
462 dataitem = None
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000463 dataitem = mappings[column](dataitem)
Guido van Rossumb053cd82006-08-24 03:53:23 +0000464 if dataitem != None:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000465 self.db.put(
466 _data_key(table, column, rowid),
467 dataitem, txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000468 txn.commit()
469 txn = None
470
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000471 # catch all exceptions here since we call unknown callables
Martin v. Löwis32ca4422007-08-11 06:13:20 +0000472 finally:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000473 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000474 txn.abort()
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000475
Guido van Rossumb940e112007-01-10 16:19:56 +0000476 except DBError as dberror:
Collin Wintera65e94c2007-08-22 21:45:20 +0000477 raise TableDBError(dberror.args[1])
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000478
Barry Warsawf71de3e2003-01-28 17:20:44 +0000479 def Delete(self, table, conditions={}):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000480 """Delete(table, conditions) - Delete items matching the given
481 conditions from the table.
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000482
483 * conditions - a dictionary keyed on column names containing
484 condition functions expecting the data string as an
485 argument and returning a boolean.
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000486 """
487 try:
488 matching_rowids = self.__Select(table, [], conditions)
489
490 # delete row data from all columns
491 columns = self.__tablecolumns[table]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000492 for rowid in matching_rowids.keys():
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000493 txn = None
494 try:
495 txn = self.env.txn_begin()
Barry Warsawf71de3e2003-01-28 17:20:44 +0000496 for column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000497 # delete the data key
498 try:
Martin v. Löwis32ca4422007-08-11 06:13:20 +0000499 self.db.delete(_data_key(table, column,
500 rowid.encode("latin-1")),
Gregory P. Smith66077d82007-10-18 16:55:12 +0000501 txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000502 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000503 # XXXXXXX column may not exist, assume no error
504 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000505
506 try:
Gregory P. Smith66077d82007-10-18 16:55:12 +0000507 self.db.delete(_rowid_key(table, rowid.encode("latin-1")), txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000508 except DBNotFoundError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000509 # XXXXXXX row key somehow didn't exist, assume no error
510 pass
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000511 txn.commit()
512 txn = None
Martin v. Löwis32ca4422007-08-11 06:13:20 +0000513 finally:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000514 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000515 txn.abort()
Guido van Rossumb940e112007-01-10 16:19:56 +0000516 except DBError as dberror:
Collin Wintera65e94c2007-08-22 21:45:20 +0000517 raise TableDBError(dberror.args[1])
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000518
519
Barry Warsawf71de3e2003-01-28 17:20:44 +0000520 def Select(self, table, columns, conditions={}):
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000521 """Select(table, columns, conditions) - retrieve specific row data
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000522 Returns a list of row column->value mapping dictionaries.
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000523
524 * columns - a list of which column data to return. If
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000525 columns is None, all columns will be returned.
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000526 * conditions - a dictionary keyed on column names
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000527 containing callable conditions expecting the data string as an
528 argument and returning a boolean.
529 """
530 try:
Guido van Rossum20435132006-08-21 00:21:47 +0000531 if table not in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000532 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000533 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000534 columns = self.__tablecolumns[table]
535 matching_rowids = self.__Select(table, columns, conditions)
Guido van Rossumb940e112007-01-10 16:19:56 +0000536 except DBError as dberror:
Collin Wintera65e94c2007-08-22 21:45:20 +0000537 raise TableDBError(dberror.args[1])
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000538 # return the matches as a list of dictionaries
539 return matching_rowids.values()
540
541
Barry Warsawf71de3e2003-01-28 17:20:44 +0000542 def __Select(self, table, columns, conditions):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000543 """__Select() - Used to implement Select and Delete (above)
544 Returns a dictionary keyed on rowids containing dicts
545 holding the row data for columns listed in the columns param
546 that match the given conditions.
547 * conditions is a dictionary keyed on column names
548 containing callable conditions expecting the data string as an
549 argument and returning a boolean.
550 """
551 # check the validity of each column name
Guido van Rossum20435132006-08-21 00:21:47 +0000552 if table not in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000553 self.__load_column_info(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000554 if columns is None:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000555 columns = self.tablecolumns[table]
Brett Cannon0072e432007-02-22 06:40:59 +0000556 for column in (columns + list(conditions.keys())):
Barry Warsawf71de3e2003-01-28 17:20:44 +0000557 if not self.__tablecolumns[table].count(column):
Collin Wintera65e94c2007-08-22 21:45:20 +0000558 raise TableDBError("unknown column: %r" % (column,))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000559
560 # keyed on rows that match so far, containings dicts keyed on
561 # column names containing the data for that row and column.
562 matching_rowids = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000563 # keys are rowids that do not match
564 rejected_rowids = {}
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000565
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000566 # attempt to sort the conditions in such a way as to minimize full
567 # column lookups
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000568 def cmp_conditions(atuple, btuple):
569 a = atuple[1]
570 b = btuple[1]
Barry Warsawf71de3e2003-01-28 17:20:44 +0000571 if type(a) is type(b):
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000572 if isinstance(a, PrefixCond) and isinstance(b, PrefixCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000573 # longest prefix first
574 return cmp(len(b.prefix), len(a.prefix))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000575 if isinstance(a, LikeCond) and isinstance(b, LikeCond):
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000576 # longest likestr first
577 return cmp(len(b.likestr), len(a.likestr))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000578 return 0
579 if isinstance(a, ExactCond):
580 return -1
581 if isinstance(b, ExactCond):
582 return 1
583 if isinstance(a, PrefixCond):
584 return -1
585 if isinstance(b, PrefixCond):
586 return 1
587 # leave all unknown condition callables alone as equals
588 return 0
589
Brett Cannon0072e432007-02-22 06:40:59 +0000590 conditionlist = list(conditions.items())
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000591 conditionlist.sort(cmp_conditions)
592
593 # Apply conditions to column data to find what we want
594 cur = self.db.cursor()
595 column_num = -1
Barry Warsawf71de3e2003-01-28 17:20:44 +0000596 for column, condition in conditionlist:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000597 column_num = column_num + 1
598 searchkey = _search_col_data_key(table, column)
599 # speedup: don't linear search columns within loop
Barry Warsawf71de3e2003-01-28 17:20:44 +0000600 if column in columns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000601 savethiscolumndata = 1 # save the data for return
Barry Warsawf71de3e2003-01-28 17:20:44 +0000602 else:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000603 savethiscolumndata = 0 # data only used for selection
604
605 try:
606 key, data = cur.set_range(searchkey)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000607 while key[:len(searchkey)] == searchkey:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000608 # extract the rowid from the key
Martin v. Löwis32ca4422007-08-11 06:13:20 +0000609 rowid = key[-_rowid_str_len:].decode("latin-1")
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000610
Guido van Rossum20435132006-08-21 00:21:47 +0000611 if rowid not in rejected_rowids:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000612 # if no condition was specified or the condition
613 # succeeds, add row to our match list.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000614 if not condition or condition(data):
Guido van Rossum20435132006-08-21 00:21:47 +0000615 if rowid not in matching_rowids:
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000616 matching_rowids[rowid] = {}
Barry Warsawf71de3e2003-01-28 17:20:44 +0000617 if savethiscolumndata:
Martin v. Löwisb2c7aff2002-11-23 11:26:07 +0000618 matching_rowids[rowid][column] = data
Barry Warsawf71de3e2003-01-28 17:20:44 +0000619 else:
Guido van Rossum20435132006-08-21 00:21:47 +0000620 if rowid in matching_rowids:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000621 del matching_rowids[rowid]
622 rejected_rowids[rowid] = rowid
623
624 key, data = cur.next()
625
Guido van Rossumb940e112007-01-10 16:19:56 +0000626 except DBError as dberror:
Guido van Rossumd84da1b2007-03-28 21:03:48 +0000627 if dberror.args[0] != DB_NOTFOUND:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000628 raise
629 continue
630
631 cur.close()
632
633 # we're done selecting rows, garbage collect the reject list
634 del rejected_rowids
635
636 # extract any remaining desired column data from the
637 # database for the matching rows.
Barry Warsawf71de3e2003-01-28 17:20:44 +0000638 if len(columns) > 0:
639 for rowid, rowdata in matching_rowids.items():
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000640 rowid = rowid.encode("latin-1")
Barry Warsawf71de3e2003-01-28 17:20:44 +0000641 for column in columns:
Guido van Rossum20435132006-08-21 00:21:47 +0000642 if column in rowdata:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000643 continue
644 try:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000645 rowdata[column] = self.db.get(
646 _data_key(table, column, rowid))
Guido van Rossumb940e112007-01-10 16:19:56 +0000647 except DBError as dberror:
Guido van Rossumd84da1b2007-03-28 21:03:48 +0000648 if dberror.args[0] != DB_NOTFOUND:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000649 raise
650 rowdata[column] = None
651
652 # return the matches
653 return matching_rowids
654
655
Barry Warsawf71de3e2003-01-28 17:20:44 +0000656 def Drop(self, table):
657 """Remove an entire table from the database"""
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000658 txn = None
659 try:
660 txn = self.env.txn_begin()
661
662 # delete the column list
Gregory P. Smith66077d82007-10-18 16:55:12 +0000663 self.db.delete(_columns_key(table), txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000664
665 cur = self.db.cursor(txn)
666
667 # delete all keys containing this tables column and row info
668 table_key = _search_all_data_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000669 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000670 try:
671 key, data = cur.set_range(table_key)
672 except DBNotFoundError:
673 break
674 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000675 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000676 break
677 cur.delete()
678
679 # delete all rowids used by this table
680 table_key = _search_rowid_key(table)
Barry Warsawf71de3e2003-01-28 17:20:44 +0000681 while 1:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000682 try:
683 key, data = cur.set_range(table_key)
684 except DBNotFoundError:
685 break
686 # only delete items in this table
Barry Warsawf71de3e2003-01-28 17:20:44 +0000687 if key[:len(table_key)] != table_key:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000688 break
689 cur.delete()
690
691 cur.close()
692
693 # delete the tablename from the table name list
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000694 tablelist = pickle.loads(
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000695 self.db.get(_E(_table_names_key), txn=txn, flags=DB_RMW))
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000696 try:
697 tablelist.remove(table)
698 except ValueError:
Barry Warsaw9a0d7792002-12-30 20:53:52 +0000699 # hmm, it wasn't there, oh well, that's what we want.
700 pass
701 # delete 1st, incase we opened with DB_DUP
Gregory P. Smith66077d82007-10-18 16:55:12 +0000702 self.db.delete(_E(_table_names_key), txn=txn)
Martin v. Löwiscccc58d2007-08-10 08:36:56 +0000703 self.db.put(_E(_table_names_key), pickle.dumps(tablelist, 1), txn=txn)
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000704
705 txn.commit()
706 txn = None
707
Guido van Rossum20435132006-08-21 00:21:47 +0000708 if table in self.__tablecolumns:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000709 del self.__tablecolumns[table]
710
Guido van Rossumb940e112007-01-10 16:19:56 +0000711 except DBError as dberror:
Collin Wintera65e94c2007-08-22 21:45:20 +0000712 raise TableDBError(dberror.args[1])
Martin v. Löwis32ca4422007-08-11 06:13:20 +0000713 finally:
Barry Warsawf71de3e2003-01-28 17:20:44 +0000714 if txn:
Martin v. Löwis6aa4a1f2002-11-19 08:09:52 +0000715 txn.abort()