PageRenderTime 75ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/src/boinc-7.0.36/py/Boinc/db_base.py

https://github.com/matszpk/native-boinc-for-android
Python | 549 lines | 524 code | 13 blank | 12 comment | 6 complexity | cefe66999899ee97a32e0b2bf9104347 MD5 | raw file
  1. ## $Id: db_base.py 22616 2010-11-03 18:25:24Z davea $
  2. # quarl 2003-10-16 initial version based on conglomeration of
  3. # coursesurvey/database.py and boinc/database.py
  4. # quarl 2003-10-16 implemented lazy lookups
  5. # DB_BASE - an awesome view of an SQL database in Python. All relational
  6. # objects are lazily cached. I.e. if table WORKUNIT has field RESULTID, wu =
  7. # database.Workunits.find1(name='Wu1') will look up Wu1; accessing wu.result
  8. # will do a database.Results.find1(id=wu.resultid) the first time
  9. from __future__ import generators
  10. import MySQLdb, MySQLdb.cursors
  11. import sys, os, weakref
  12. ID = '$Id: db_base.py 22616 2010-11-03 18:25:24Z davea $'
  13. dbconnection = None
  14. def list2dict(list):
  15. dict = {}
  16. for k in list: dict[k] = None
  17. return dict
  18. class DatabaseInconsistency(Exception):
  19. def __init__(self, descript=None, search_table=None, search_kwargs=None):
  20. self.descript = descript
  21. self.search_table = search_table
  22. self.search_kwargs = search_kwargs
  23. self.search_tree = []
  24. def __str__(self):
  25. return ("""** DATABASE INCONSISTENCY **
  26. %s
  27. search_table = %s
  28. search_kwargs = %s
  29. search_tree = [
  30. %s
  31. ] """ %(
  32. self.descript,
  33. self.search_table,
  34. self.search_kwargs,
  35. '\n'.join(
  36. map(lambda o:" %s#%s %s"%(o._table.table,o.__dict__.get('id'),o), self.search_tree))
  37. ))
  38. class Debug:
  39. def __init__(self):
  40. self.html = False
  41. def printline(self,s):
  42. if self.html:
  43. print "<!-- ## %s -->"%s
  44. else:
  45. print >>sys.stderr, "##", s
  46. debug = Debug()
  47. debug.mysql = not not os.environ.get('DEBUG_DB')
  48. def _execute_sql(cursor, command):
  49. '''Same as ``cursor.execute(command)``, but more verbose on error.'''
  50. try:
  51. cursor.execute(command)
  52. except MySQLdb.MySQLError, e:
  53. e.args += (command,)
  54. raise e
  55. def _commit_object(tablename, paramdict, id=None):
  56. """Takes a tablename, a parameter dict, and an optional id. Puts together
  57. the appropriate SQL command to commit the object to the database.
  58. Executes it. Returns the object's id."""
  59. assert(dbconnection)
  60. cursor = dbconnection.cursor()
  61. equalcommands = []
  62. for key in paramdict.keys():
  63. value = paramdict[key]
  64. if value == None:
  65. continue
  66. elif isinstance(value, int):
  67. equalcommands.append('%s=%d' %(key,value))
  68. else:
  69. equalcommands.append("%s='%s'"%(key,dbconnection.escape_string(str(value))))
  70. if id == None:
  71. command = 'INSERT INTO %s SET %s' % \
  72. (tablename, ', '.join(equalcommands))
  73. if debug.mysql:
  74. debug.printline("query: "+command)
  75. _execute_sql(cursor, command)
  76. # id = cursor.insert_id() #porters note: works w/MySQLdb only
  77. id = cursor.lastrowid
  78. else:
  79. command = "UPDATE %s SET %s WHERE id=%d" % \
  80. (tablename, ', '.join(equalcommands), id)
  81. if debug.mysql:
  82. debug.printline("query: "+command)
  83. _execute_sql(cursor, command)
  84. cursor.close()
  85. dbconnection.commit()
  86. return id
  87. def _remove_object(command, id=None):
  88. """Takes a command string, dbconnection object, and optional id. If an
  89. id is given, it assembles the SQL command and deletes the object
  90. from the database. Does nothing if no id is given."""
  91. assert(dbconnection)
  92. if id == None:
  93. pass
  94. else:
  95. cursor = dbconnection.cursor()
  96. command = 'DELETE FROM ' + command + \
  97. ' WHERE id=%d' % id
  98. if debug.mysql:
  99. debug.printline("query: "+command)
  100. _execute_sql(cursor, command)
  101. cursor.close()
  102. dbconnection.commit()
  103. def _select_object(table, searchdict, extra_args="", extra_params=[], select_what=None):
  104. assert(dbconnection)
  105. parameters = extra_params[:]
  106. join = None
  107. if '_join' in searchdict:
  108. join = searchdict['_join']
  109. del searchdict['_join']
  110. if '_extra_params' in searchdict:
  111. parameters += searchdict['_extra_params']
  112. del searchdict['_extra_params']
  113. command = 'SELECT %s from %s'%((select_what or "%s.*"%table) ,table)
  114. if join:
  115. command += "," + join
  116. for (key,value) in searchdict.items():
  117. if value == None:
  118. value = ''
  119. escaped_value = dbconnection.escape_string(str(value))
  120. if key == 'text':
  121. parameters.append("instr(%s,'%s')"%(key,escaped_value))
  122. else:
  123. parameters.append("%s='%s'"%(key,escaped_value))
  124. if parameters:
  125. command += ' WHERE ' + ' AND '.join(parameters)
  126. if extra_args:
  127. command += ' ' + extra_args.strip()
  128. cursor = dbconnection.cursor()
  129. if debug.mysql:
  130. debug.printline("query: "+command)
  131. _execute_sql(cursor, command)
  132. return cursor
  133. def _select_object_fetchall(*args, **kwargs):
  134. cursor = apply(_select_object, args, kwargs)
  135. results = cursor.fetchall()
  136. cursor.close()
  137. return results
  138. def _select_object_iterate(*args, **kwargs):
  139. cursor = apply(_select_object, args, kwargs)
  140. while True:
  141. result = cursor.fetchone()
  142. if not result: return
  143. yield result
  144. def _select_count_objects(*args, **kwargs):
  145. kwargs['select_what'] = 'count(*)'
  146. cursor = apply(_select_object, args, kwargs)
  147. result = cursor.fetchone().values()[0]
  148. cursor.close()
  149. return result
  150. class Options:
  151. pass
  152. options = Options()
  153. # keep up to this many objects in cache. we use a very bone-headed
  154. # cache-management algorithm: when we reach this many objects, drop the oldest
  155. # half.
  156. options.OBJECT_CACHE_SIZE = 1024
  157. # don't lookup referenced Ids until they are asked for. I.e., if
  158. # evaluatedclass.instructorteamid=123, then if instructorteam#123 hasn't been
  159. # looked up yet, don't look up evaluatedclass.instructorteam until it is
  160. # referenced. use DEBUG_DB=1 to check out this niftiness.
  161. options.LAZY_LOOKUPS = True
  162. class DatabaseTable:
  163. def __init__(self, table, columns, extra_columns=[],
  164. select_args = None, sort_results = False):
  165. self.table = table
  166. self.lcolumns = columns
  167. self.columns = list2dict(columns)
  168. self.extra_columns = list2dict(extra_columns)
  169. # self.object_class = object_class
  170. self.select_args = select_args
  171. self.sort_results = sort_results
  172. ## self.objects is a mapping from id->object which weakly references
  173. ## all current objects from this table. this guarantees that if a
  174. ## find() returns a row for which we already created an object in
  175. ## memory, we return the same one.
  176. self.objects = weakref.WeakValueDictionary()
  177. ## self.object_cache is a list of the N most recently retrieved
  178. ## objects. its values aren't really used; the list is used to ensure
  179. ## the strong reference count for self.objects[object] is nonzero to
  180. ## ensure is lifetime.
  181. ##
  182. ## This means if you look up database.Apps[1]: the first lookup does a
  183. ## MySQL SELECT; afterwards database.Apps[1] is free (only requires a
  184. ## lookup in database.Apps.objects). To prevent this object cache
  185. ## from growing without bound, database.Apps.object is a weak-valued
  186. ## dictionary. This means that if no one refers to the object, it is
  187. ## deleted from the dictionary. database.Apps.object_cache maintains
  188. ## a list of the OBJECT_CACHE_SIZE most recent lookups, which forces
  189. ## their strong reference count.
  190. self.object_cache = []
  191. self.defdict = {}
  192. for key in self.lcolumns:
  193. if key == 'id':
  194. self.defdict[key] = None
  195. elif key.endswith('id'):
  196. self.defdict[key[:-2]] = None
  197. elif key.endswith('ids'):
  198. self.defdict[key[:-3]] = None
  199. else:
  200. self.defdict[key] = None
  201. def _cache(self, object):
  202. """Maintain up to OBJECT_CACHE_SIZE objects in the object_cache list.
  203. The object's existence in this cache ensures its strong reference
  204. count is nonzero, so that it doesn't get implicitly dropped from
  205. self.objects."""
  206. if len(self.object_cache) >= options.OBJECT_CACHE_SIZE:
  207. self.object_cache = self.object_cache[:-options.OBJECT_CACHE_SIZE/2]
  208. self.object_cache.append(object)
  209. def _is_cached(self, id):
  210. '''Returns True if object is automatically-cached, i.e. in the weak
  211. reference cache.
  212. This is not the same as the manual cache invoked by _cache(), i.e. the
  213. strong reference cache.
  214. '''
  215. return id in self.objects
  216. def _modify_find_args(self, kwargs):
  217. '''Derived classes can override this function to modify kwargs.
  218. This is only called for non-trivial find args (if there are arguments
  219. and not just "id")'''
  220. pass
  221. def clear_cache(self):
  222. """
  223. Clears the cached objects list
  224. """
  225. self.object_cache = []
  226. def count(self, **kwargs):
  227. """Return the number of database objects matching keywords.
  228. Arguments are the same format as find()."""
  229. if not kwargs:
  230. # shortcut since this is the most common case
  231. return _select_count_objects(self.table, {})
  232. if kwargs.keys() == ['id']:
  233. # looking up by ID only, look in cache first:
  234. id = kwargs['id']
  235. if not id:
  236. return 0
  237. if id in self.objects:
  238. return 1
  239. self._modify_find_args(kwargs)
  240. kwargs = self.dict2database_fields(kwargs)
  241. return _select_count_objects(self.table, kwargs,
  242. extra_args=self.select_args)
  243. def find(self, **kwargs):
  244. """Return a list of database objects matching keywords.
  245. Allowed keywords are specified by self.columns.
  246. Objects are cached by ID so repeated lookups are quick.
  247. """
  248. if kwargs.keys() == ['id']:
  249. # looking up by ID only, look in cache first:
  250. id = kwargs['id']
  251. if not id:
  252. return [None]
  253. try:
  254. return [self.objects[id]]
  255. except KeyError:
  256. pass
  257. limbo_object = self.object_class(id=None) # prevent possible id recursion
  258. limbo_object.in_limbo = 1
  259. self.objects[id] = limbo_object
  260. self._cache(limbo_object)
  261. self._modify_find_args(kwargs)
  262. kwargs = self.dict2database_fields(kwargs)
  263. results = _select_object_fetchall(self.table, kwargs,
  264. extra_args=self.select_args)
  265. objects = self._create_objects_from_sql_results(results, kwargs)
  266. # up to this point objects should be equivalent to list(iterate(...))
  267. if self.sort_results:
  268. objects.sort()
  269. return objects
  270. def iterate(self, **kwargs):
  271. """Same as find(), but using generators, and no sorting."""
  272. if kwargs.keys() == ['id']:
  273. # looking up by ID only, look in cache first:
  274. id = kwargs['id']
  275. if not id:
  276. return
  277. try:
  278. yield self.objects[id]
  279. return
  280. except KeyError:
  281. pass
  282. limbo_object = self.object_class(id=None) # prevent possible id recursion
  283. limbo_object.in_limbo = 1
  284. self.objects[id] = limbo_object
  285. self._cache(limbo_object)
  286. self._modify_find_args(kwargs)
  287. kwargs = self.dict2database_fields(kwargs)
  288. for result in _select_object_iterate(self.table, kwargs,
  289. extra_args=self.select_args):
  290. yield self._create_object_from_sql_result(result)
  291. return
  292. def _create_objects_from_sql_results(self, results, kwargs):
  293. return map(self._create_object_from_sql_result, results)
  294. def _create_object_from_sql_result(self, result):
  295. id = result['id']
  296. try:
  297. # object already exists in cache?
  298. object = self.objects[id]
  299. if 'in_limbo' in object.__dict__:
  300. # earlier we set the object cache so that we don't recurse;
  301. # update it now with real values and delete the 'in limbo'
  302. # flag
  303. del object.__dict__['in_limbo']
  304. object.do_init(result)
  305. except KeyError:
  306. # create the object - looking up instructors, etc
  307. object = apply(self.object_class, [], result)
  308. if object.id:
  309. self.objects[object.id] = object
  310. self._cache(object)
  311. return object
  312. def find1(self, **kwargs):
  313. '''Return a single result. Raises a DatabaseInconsistency if not
  314. exactly 1 result returned.'''
  315. objects = apply(self.find, [], kwargs)
  316. if len(objects) != 1:
  317. raise DatabaseInconsistency(
  318. descript="find1: expected 1 result but found %d"%len(objects),
  319. search_table = self.table,
  320. search_kwargs = kwargs)
  321. return objects[0]
  322. def __getitem__(self, id):
  323. '''Lookup (possibly cached) object by id. Returns None if id==None.'''
  324. return id and self.find1(id=id)
  325. def objdict2database_fields(self, indict, lazydict):
  326. dict = {}
  327. for key in self.columns:
  328. if key.endswith('id'):
  329. xkey = key[:-2]
  330. if xkey in lazydict:
  331. # lazydict maps 'name' (without 'id') -> (table,id)
  332. dict[key] = lazydict[xkey][1]
  333. else:
  334. obj = indict[xkey]
  335. dict[key] = obj and obj.id or 0
  336. else:
  337. dict[key] = indict[key]
  338. return dict
  339. def _valid_query_keys(self):
  340. return self.columns.keys()+self.extra_columns.keys()+['_join','_extra_params']
  341. def dict2database_fields(self, indict):
  342. indict = indict.copy()
  343. dict = {}
  344. if 'id' in indict:
  345. dict['id'] = indict['id']
  346. del indict['id']
  347. for key in self._valid_query_keys():
  348. if key.endswith('id'):
  349. xkey = key[:-2]
  350. if xkey in indict:
  351. obj = indict[xkey]
  352. dict[key] = obj and obj.id
  353. del indict[xkey]
  354. else:
  355. if key in indict:
  356. dict[key] = indict[key]
  357. del indict[key]
  358. if len(indict):
  359. raise ValueError('Invalid key(s): %s'%indict)
  360. return dict
  361. class DatabaseObject:
  362. id_lookups = {} # set by init_table_classes
  363. def _set_field(self, key, value):
  364. """Set field KEY to VALUE. May be overridden by derived class.
  365. if options.LAZY_LOOKUPS is true, then possibly don't look up a value
  366. yet.
  367. """
  368. if key.endswith('id') and not key.endswith('_id'):
  369. xkey = key[:-2]
  370. table = self.id_lookups[xkey]._table
  371. id = value
  372. if options.LAZY_LOOKUPS:
  373. if table._is_cached(id):
  374. self.__dict__[xkey] = table.objects[id]
  375. else:
  376. del self.__dict__[xkey]
  377. self._lazy_lookups[xkey] = (table, id)
  378. else:
  379. # always lookup values
  380. self.__dict__[xkey] = table[id]
  381. else:
  382. self.__dict__[key] = value
  383. def __getattr__(self, name):
  384. if options.LAZY_LOOKUPS and name in self._lazy_lookups:
  385. (table, id) = self._lazy_lookups[name]
  386. del self._lazy_lookups[name]
  387. object = table[id] # this probably invokes MySQL SELECTs
  388. self.__dict__[name] = object
  389. return object
  390. raise AttributeError(name)
  391. def database_fields_to_self(self, dict):
  392. columns = self._table.columns
  393. self.__dict__.update(self._table.defdict) # set defaults to None
  394. # set this first so that if we get a DatabaseInconsistency we can see
  395. # the id
  396. self.id = dict.get('id')
  397. for (key, value) in dict.items():
  398. if key == 'id':
  399. continue
  400. if key or key+'id' in columns:
  401. self._set_field(key, value)
  402. else:
  403. raise ValueError("database '%s' object doesn't take argument '%s'"%(
  404. self._table.table, key))
  405. def do_init(self, kwargs):
  406. try:
  407. self.database_fields_to_self(kwargs)
  408. except DatabaseInconsistency, e:
  409. e.search_tree.append(self)
  410. raise
  411. # if no id then object starts dirty
  412. self._set_dirty(not self.id)
  413. def __init__(self, **kwargs):
  414. self._lazy_lookups = {}
  415. self.do_init(kwargs)
  416. def __eq__(self, other):
  417. return other!=None and isinstance(other, DatabaseObject) and self.id == other.id
  418. def __ne__(self, other):
  419. return not (self == other)
  420. def __hash__(self):
  421. return self.id or 0
  422. def _commit_params(self, paramdict):
  423. """Commits the object to the dbconnection database."""
  424. self.id = _commit_object(self._table.table, paramdict, self.id)
  425. def commit(self, force=False):
  426. if force or self._dirty:
  427. self._commit_params(self._table.objdict2database_fields(self.__dict__, self._lazy_lookups))
  428. self._set_dirty(False)
  429. def remove(self):
  430. """Removes the object from the dbconnection database."""
  431. _remove_object(self._table.table, self.id)
  432. self.id = None
  433. def dset(self, key, value):
  434. if self.__dict__[key] != value:
  435. self.__dict__[key] = value
  436. self._set_dirty()
  437. def __setattr__(self, key, value):
  438. if key in self._table.columns or key+'id' in self._table.columns:
  439. self.dset(key, value)
  440. else:
  441. self.__dict__[key] = value
  442. def _set_dirty(self, value=True):
  443. self.__dict__['_dirty'] = value
  444. def do_connect(db, user, passwd, host='localhost'):
  445. """Takes a database name, a username, and password. Connects to
  446. SQL server and makes a new Dbconnection."""
  447. global dbconnection
  448. if dbconnection:
  449. raise 'Already connected'
  450. dbconnection = MySQLdb.connect(db=db,host=host,user=user,passwd=passwd,
  451. cursorclass=MySQLdb.cursors.DictCursor)
  452. def close():
  453. """Closes the connection to the sql survey and deletes the Dbconnection object."""
  454. global dbconnection
  455. dbconnection.close()
  456. dbconnection = None
  457. def get_dbconnection():
  458. return dbconnection
  459. def set_dbconnection(d):
  460. dbconnection = d
  461. def init_table_classes(database_classes_, more_id_lookups = {}):
  462. """initialize the list of database classes and tables. To be called from
  463. database.py.
  464. """
  465. global database_classes, database_tables
  466. database_classes = database_classes_
  467. for Class in database_classes:
  468. Class._table.object_class = Class
  469. DatabaseObject.id_lookups[Class._table.table] = Class
  470. DatabaseObject.id_lookups.update(more_id_lookups)
  471. database_tables = map(lambda c: c._table, database_classes)
  472. def check_database_consistency():
  473. '''Raises DatabaseInconsistency on error.
  474. Loads the entire database into memory so will take a while.
  475. '''
  476. options.LAZY_LOOKUPS = False
  477. for table in database_tables:
  478. print '\rChecking %s: [counting]' %(table.table),
  479. sys.stdout.flush()
  480. count = table.count()
  481. i = 0
  482. j_limit = int(count / 100) # show progress every 1%
  483. j = j_limit
  484. print '\rChecking %s: [iterating]' %(table.table),
  485. sys.stdout.flush()
  486. for object in table.iterate():
  487. # we don't need to do anything here; just iterating through the
  488. # database will automatically read everything into memory
  489. i += 1
  490. if j == j_limit:
  491. print '\rChecking %s: [%d/%d] %3.f%%' %(table.table, i, count, 100.0*i/count),
  492. sys.stdout.flush()
  493. j = 0
  494. j += 1
  495. print '\rChecking %s: all %d rows are good' %(table.table, count)