VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/db.py@ 53363

Last change on this file since 53363 was 52776, checked in by vboxsync, 10 years ago

fix OSE

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 21.9 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: db.py 52776 2014-09-17 14:51:43Z vboxsync $
3
4"""
5Test Manager - Database Interface.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2014 Oracle Corporation
11
12This file is part of VirtualBox Open Source Edition (OSE), as
13available from http://www.virtualbox.org. This file is free software;
14you can redistribute it and/or modify it under the terms of the GNU
15General Public License (GPL) as published by the Free Software
16Foundation, in version 2 as it comes in the "COPYING" file of the
17VirtualBox OSE distribution. VirtualBox OSE is distributed in the
18hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
19
20The contents of this file may alternatively be used under the terms
21of the Common Development and Distribution License Version 1.0
22(CDDL) only, as it comes in the "COPYING.CDDL" file of the
23VirtualBox OSE distribution, in which case the provisions of the
24CDDL are applicable instead of those of the GPL.
25
26You may elect to license modified versions of this file under the
27terms and conditions of either the GPL or the CDDL or both.
28"""
29__version__ = "$Revision: 52776 $"
30
31
32# Standard python imports.
33import datetime;
34import os;
35import psycopg2;
36import psycopg2.extensions;
37import sys;
38
39# Validation Kit imports.
40from common import utils, webutils;
41from testmanager import config;
42
43# Fix psycho unicode handling in psycopg2 with python 2.x.
44if sys.version_info[0] < 3:
45 psycopg2.extensions.register_type(psycopg2.extensions.UNICODE);
46 psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY);
47
48
49def isDbTimestampInfinity(tsValue):
50 """
51 Checks if tsValue is an infinity timestamp.
52 """
53 ## @todo improve this test...
54 return tsValue.year >= 9999;
55
56def isDbTimestamp(oValue):
57 """
58 Checks if oValue is a DB timestamp object.
59 """
60 if isinstance(oValue, datetime.datetime):
61 return True;
62 if utils.isString(oValue):
63 ## @todo detect strings as well.
64 return False;
65 return getattr(oValue, 'pydatetime', None) != None;
66
67def dbTimestampToDatetime(oValue):
68 """
69 Converts a database timestamp to a datetime instance.
70 """
71 if isinstance(oValue, datetime.datetime):
72 return oValue;
73 if utils.isString(oValue):
74 raise Exception('TODO');
75 return oValue.pydatetime();
76
77def dbTimestampToZuluDatetime(oValue):
78 """
79 Converts a database timestamp to a zulu datetime instance.
80 """
81 tsValue = dbTimestampToDatetime(oValue);
82
83 if tsValue.tzinfo is not None:
84 class UTC(datetime.tzinfo):
85 """UTC TZ Info Class"""
86 def utcoffset(self, _):
87 return datetime.timedelta(0);
88 def tzname(self, _):
89 return "UTC";
90 def dst(self, _):
91 return datetime.timedelta(0);
92 tsValue = tsValue.astimezone(UTC());
93
94 return tsValue;
95
96
97class TMDatabaseIntegrityException(Exception):
98 """
99 Herolds a database integrity error up the callstack.
100
101 Do NOT use directly, only thru TMDatabaseConnection.integrityException.
102 Otherwise, we won't be able to log the issue.
103 """
104 pass;
105
106
107class TMDatabaseCursor(object):
108 """ Cursor wrapper class. """
109
110 def __init__(self, oDb, oCursor):
111 self._oDb = oDb;
112 self._oCursor = oCursor;
113
114 def execute(self, sOperation, aoArgs = None):
115 """ See TMDatabaseConnection.execute()"""
116 return self._oDb.executeInternal(self._oCursor, sOperation, aoArgs, utils.getCallerName());
117
118 def callProc(self, sProcedure, aoArgs = None):
119 """ See TMDatabaseConnection.callProc()"""
120 return self._oDb.callProcInternal(self._oCursor, sProcedure, aoArgs, utils.getCallerName());
121
122 def fetchOne(self):
123 """Wrapper around Psycopg2.cursor.fetchone."""
124 return self._oCursor.fetchone();
125
126 def fetchMany(self, cRows = None):
127 """Wrapper around Psycopg2.cursor.fetchmany."""
128 return self._oCursor.fetchmany(cRows if cRows is not None else self._oCursor.arraysize);
129
130 def fetchAll(self):
131 """Wrapper around Psycopg2.cursor.fetchall."""
132 return self._oCursor.fetchall();
133
134 def getRowCount(self):
135 """Wrapper around Psycopg2.cursor.rowcount."""
136 return self._oCursor.rowcount;
137
138 def formatBindArgs(self, sStatement, aoArgs):
139 """Wrapper around Psycopg2.cursor.mogrify."""
140 oRet = self._oCursor.mogrify(sStatement, aoArgs);
141 if sys.version_info[0] >= 3 and not isinstance(oRet, str):
142 oRet = oRet.decode('utf-8');
143 return oRet;
144
145 @staticmethod
146 def isTsInfinity(tsValue):
147 """ Checks if tsValue is an infinity timestamp. """
148 return isDbTimestampInfinity(tsValue);
149
150
151class TMDatabaseConnection(object):
152 """
153 Test Manager Database Access class.
154
155 This class contains no logic, just raw access abstraction and utilities,
156 as well as some debug help and some statistics.
157 """
158
159 def __init__(self, fnDPrint = None, oSrvGlue = None):
160 """
161 Database connection wrapper.
162 The fnDPrint is for debug logging of all database activity.
163
164 Raises an exception on failure.
165 """
166
167 sAppName = '%s-%s' % (os.getpid(), os.path.basename(sys.argv[0]),)
168 if len(sAppName) >= 64:
169 sAppName = sAppName[:64];
170 os.environ['PGAPPNAME'] = sAppName;
171
172 dArgs = \
173 { \
174 'database': config.g_ksDatabaseName,
175 'user': config.g_ksDatabaseUser,
176 'password': config.g_ksDatabasePassword,
177 # 'application_name': sAppName, - Darn stale debian! :/
178 };
179 if config.g_ksDatabaseAddress is not None:
180 dArgs['host'] = config.g_ksDatabaseAddress;
181 if config.g_ksDatabasePort is not None:
182 dArgs['port'] = config.g_ksDatabasePort;
183 self._oConn = psycopg2.connect(**dArgs); # pylint: disable=W0142
184 self._oConn.set_client_encoding('UTF-8');
185 self._oCursor = self._oConn.cursor();
186 self._oExplainConn = None;
187 self._oExplainCursor = None;
188 if config.g_kfWebUiSqlTraceExplain and config.g_kfWebUiSqlTrace:
189 self._oExplainConn = psycopg2.connect(**dArgs); # pylint: disable=W0142
190 self._oExplainConn.set_client_encoding('UTF-8');
191 self._oExplainCursor = self._oExplainConn.cursor();
192 self._fTransaction = False;
193 self._tsCurrent = None;
194 self._tsCurrentMinusOne = None;
195
196 assert self.isAutoCommitting() is False;
197
198 # Debug and introspection.
199 self._fnDPrint = fnDPrint;
200 self._aoTraceBack = [];
201
202 # Exception class handles.
203 self.oXcptError = psycopg2.Error;
204
205 if oSrvGlue is not None:
206 oSrvGlue.registerDebugInfoCallback(self.debugInfoCallback);
207
208 def isAutoCommitting(self):
209 """ Work around missing autocommit attribute in older versions."""
210 return getattr(self._oConn, 'autocommit', False);
211
212 def close(self):
213 """
214 Closes the connection and renders all cursors useless.
215 """
216 if self._oCursor is not None:
217 self._oCursor.close();
218 self._oCursor = None;
219
220 if self._oConn is not None:
221 self._oConn.close();
222 self._oConn = None;
223
224 if self._oExplainCursor is not None:
225 self._oExplainCursor.close();
226 self._oExplainCursor = None;
227
228 if self._oExplainConn is not None:
229 self._oExplainConn.close();
230 self._oExplainConn = None;
231
232
233 def _startedTransaction(self):
234 """
235 Called to work the _fTransaction and related variables when starting
236 a transaction.
237 """
238 self._fTransaction = True;
239 self._tsCurrent = None;
240 self._tsCurrentMinusOne = None;
241 return None;
242
243 def _endedTransaction(self):
244 """
245 Called to work the _fTransaction and related variables when ending
246 a transaction.
247 """
248 self._fTransaction = False;
249 self._tsCurrent = None;
250 self._tsCurrentMinusOne = None;
251 return None;
252
253 def begin(self):
254 """
255 Currently just for marking where a transaction starts in the code.
256 """
257 assert self._oConn is not None;
258 assert self.isAutoCommitting() is False;
259 self._aoTraceBack.append([utils.timestampNano(), 'START TRANSACTION', 0, 0, utils.getCallerName(), None]);
260 self._startedTransaction();
261 return True;
262
263 def commit(self, sCallerName = None):
264 """ Wrapper around Psycopg2.connection.commit."""
265 assert self._fTransaction is True;
266
267 nsStart = utils.timestampNano();
268 oRc = self._oConn.commit();
269 cNsElapsed = utils.timestampNano() - nsStart;
270
271 if sCallerName is None:
272 sCallerName = utils.getCallerName();
273 self._aoTraceBack.append([nsStart, 'COMMIT', cNsElapsed, 0, sCallerName, None]);
274 self._endedTransaction();
275 return oRc;
276
277 def maybeCommit(self, fCommit):
278 """
279 Commits if fCommit is True.
280 Returns True if committed, False if not.
281 """
282 if fCommit is True:
283 self.commit(utils.getCallerName());
284 return True;
285 return False;
286
287 def rollback(self):
288 """ Wrapper around Psycopg2.connection.rollback."""
289 nsStart = utils.timestampNano();
290 oRc = self._oConn.rollback();
291 cNsElapsed = utils.timestampNano() - nsStart;
292
293 self._aoTraceBack.append([nsStart, 'ROLLBACK', cNsElapsed, 0, utils.getCallerName(), None]);
294 self._endedTransaction();
295 return oRc;
296
297 #
298 # Internal cursor workers.
299 #
300
301 def executeInternal(self, oCursor, sOperation, aoArgs, sCallerName):
302 """
303 Execute a query or command.
304
305 Mostly a wrapper around the psycopg2 cursor method with the same name,
306 but collect data for traceback.
307 """
308 if aoArgs is None:
309 aoArgs = list();
310
311 sBound = oCursor.mogrify(unicode(sOperation), aoArgs);
312 if sys.version_info[0] >= 3 and not isinstance(sBound, str):
313 sBound = sBound.decode('utf-8');
314
315 aasExplain = None;
316 if self._oExplainCursor is not None:
317 try:
318 if config.g_kfWebUiSqlTraceExplainTiming:
319 self._oExplainCursor.execute('EXPLAIN (ANALYZE, BUFFERS, COSTS, VERBOSE, TIMING) ' + sBound);
320 else:
321 self._oExplainCursor.execute('EXPLAIN (ANALYZE, BUFFERS, COSTS, VERBOSE) ' + sBound);
322 except Exception as oXcpt:
323 aasExplain = [ ['Explain exception: '], [str(oXcpt)]];
324 try: self._oExplainConn.rollback();
325 except: pass;
326 else:
327 aasExplain = self._oExplainCursor.fetchall();
328
329 nsStart = utils.timestampNano();
330 try:
331 oRc = oCursor.execute(sBound);
332 except Exception as oXcpt:
333 cNsElapsed = utils.timestampNano() - nsStart;
334 self._aoTraceBack.append([nsStart, 'oXcpt=%s; Statement: %s' % (oXcpt, sBound), cNsElapsed, 0, sCallerName, None]);
335 if self._fnDPrint is not None:
336 self._fnDPrint('db::execute %u ns, caller %s: oXcpt=%s; Statement: %s'
337 % (cNsElapsed, sCallerName, oXcpt, sBound));
338 raise;
339 cNsElapsed = utils.timestampNano() - nsStart;
340
341 if self._fTransaction is False and not self.isAutoCommitting(): # Even SELECTs starts transactions with psycopg2, see FAQ.
342 self._aoTraceBack.append([nsStart, '[START TRANSACTION]', 0, 0, sCallerName, None]);
343 self._startedTransaction();
344 self._aoTraceBack.append([nsStart, sBound, cNsElapsed, oCursor.rowcount, sCallerName, aasExplain]);
345 if self._fnDPrint is not None:
346 self._fnDPrint('db::execute %u ns, caller %s: "\n%s"' % (cNsElapsed, sCallerName, sBound));
347 if self.isAutoCommitting():
348 self._aoTraceBack.append([nsStart, '[AUTO COMMIT]', 0, 0, sCallerName, None]);
349
350 return oRc;
351
352 def callProcInternal(self, oCursor, sProcedure, aoArgs, sCallerName):
353 """
354 Call a stored procedure.
355
356 Mostly a wrapper around the psycopg2 cursor method 'callproc', but
357 collect data for traceback.
358 """
359 if aoArgs is None:
360 aoArgs = list();
361
362 nsStart = utils.timestampNano();
363 try:
364 oRc = oCursor.callproc(sProcedure, aoArgs);
365 except Exception as oXcpt:
366 cNsElapsed = utils.timestampNano() - nsStart;
367 self._aoTraceBack.append([nsStart, 'oXcpt=%s; Calling: %s(%s)' % (oXcpt, sProcedure, aoArgs),
368 cNsElapsed, 0, sCallerName, None]);
369 if self._fnDPrint is not None:
370 self._fnDPrint('db::callproc %u ns, caller %s: oXcpt=%s; Calling: %s(%s)'
371 % (cNsElapsed, sCallerName, oXcpt, sProcedure, aoArgs));
372 raise;
373 cNsElapsed = utils.timestampNano() - nsStart;
374
375 if self._fTransaction is False and not self.isAutoCommitting(): # Even SELECTs starts transactions with psycopg2, see FAQ.
376 self._aoTraceBack.append([nsStart, '[START TRANSACTION]', 0, 0, sCallerName, None]);
377 self._startedTransaction();
378 self._aoTraceBack.append([nsStart, '%s(%s)' % (sProcedure, aoArgs), cNsElapsed, oCursor.rowcount, sCallerName, None]);
379 if self._fnDPrint is not None:
380 self._fnDPrint('db::callproc %u ns, caller %s: "%s(%s)"' % (cNsElapsed, sCallerName, sProcedure, aoArgs));
381 if self.isAutoCommitting():
382 self._aoTraceBack.append([nsStart, '[AUTO COMMIT]', 0, 0, sCallerName, sCallerName, None]);
383
384 return oRc;
385
386 def _fetchOne(self, oCursor):
387 """Wrapper around Psycopg2.cursor.fetchone."""
388 oRow = oCursor.fetchone()
389 if self._fnDPrint is not None:
390 self._fnDPrint('db:fetchOne returns: %s' % (oRow,));
391 return oRow;
392
393 def _fetchMany(self, oCursor, cRows):
394 """Wrapper around Psycopg2.cursor.fetchmany."""
395 return oCursor.fetchmany(cRows if cRows is not None else oCursor.arraysize);
396
397 def _fetchAll(self, oCursor):
398 """Wrapper around Psycopg2.cursor.fetchall."""
399 return oCursor.fetchall()
400
401 def _getRowCountWorker(self, oCursor):
402 """Wrapper around Psycopg2.cursor.rowcount."""
403 return oCursor.rowcount;
404
405
406 #
407 # Default cursor access.
408 #
409
410 def execute(self, sOperation, aoArgs = None):
411 """
412 Execute a query or command.
413
414 Mostly a wrapper around the psycopg2 cursor method with the same name,
415 but collect data for traceback.
416 """
417 return self.executeInternal(self._oCursor, sOperation, aoArgs, utils.getCallerName());
418
419 def callProc(self, sProcedure, aoArgs = None):
420 """
421 Call a stored procedure.
422
423 Mostly a wrapper around the psycopg2 cursor method 'callproc', but
424 collect data for traceback.
425 """
426 return self.callProcInternal(self._oCursor, sProcedure, aoArgs, utils.getCallerName());
427
428 def fetchOne(self):
429 """Wrapper around Psycopg2.cursor.fetchone."""
430 return self._oCursor.fetchone();
431
432 def fetchMany(self, cRows = None):
433 """Wrapper around Psycopg2.cursor.fetchmany."""
434 return self._oCursor.fetchmany(cRows if cRows is not None else self._oCursor.arraysize);
435
436 def fetchAll(self):
437 """Wrapper around Psycopg2.cursor.fetchall."""
438 return self._oCursor.fetchall();
439
440 def getRowCount(self):
441 """Wrapper around Psycopg2.cursor.rowcount."""
442 return self._oCursor.rowcount;
443
444 def formatBindArgs(self, sStatement, aoArgs):
445 """Wrapper around Psycopg2.cursor.mogrify."""
446 oRet = self._oCursor.mogrify(sStatement, aoArgs);
447 if sys.version_info[0] >= 3 and not isinstance(oRet, str):
448 oRet = oRet.decode('utf-8');
449 return oRet;
450
451 def getCurrentTimestamps(self):
452 """
453 Returns the current timestamp and the current timestamp minus one tick.
454 This will start a transaction if necessary.
455 """
456 if self._tsCurrent is None:
457 self.execute('SELECT CURRENT_TIMESTAMP, CURRENT_TIMESTAMP - INTERVAL \'1 microsecond\'');
458 (self._tsCurrent, self._tsCurrentMinusOne) = self.fetchOne();
459 return (self._tsCurrent, self._tsCurrentMinusOne);
460
461 def getCurrentTimestamp(self):
462 """
463 Returns the current timestamp.
464 This will start a transaction if necessary.
465 """
466 if self._tsCurrent is None:
467 self.getCurrentTimestamps();
468 return self._tsCurrent;
469
470 def getCurrentTimestampMinusOne(self):
471 """
472 Returns the current timestamp minus one tick.
473 This will start a transaction if necessary.
474 """
475 if self._tsCurrentMinusOne is None:
476 self.getCurrentTimestamps();
477 return self._tsCurrentMinusOne;
478
479
480 #
481 # Additional cursors.
482 #
483 def openCursor(self):
484 """
485 Opens a new cursor (TMDatabaseCursor).
486 """
487 oCursor = self._oConn.cursor();
488 return TMDatabaseCursor(self, oCursor);
489
490 #
491 # Utilities.
492 #
493
494 @staticmethod
495 def isTsInfinity(tsValue):
496 """ Checks if tsValue is an infinity timestamp. """
497 return isDbTimestampInfinity(tsValue);
498
499 #
500 # Error stuff.
501 #
502 def integrityException(self, sMessage):
503 """
504 Database integrity reporter and exception factory.
505 Returns an TMDatabaseIntegrityException which the caller can raise.
506 """
507 ## @todo Create a new database connection and log the issue in the SystemLog table.
508 ## Alternatively, rollback whatever is going on and do it using the current one.
509 return TMDatabaseIntegrityException(sMessage);
510
511
512 #
513 # Debugging.
514 #
515
516 def dprint(self, sText):
517 """
518 Debug output.
519 """
520 if not self._fnDPrint:
521 return False;
522 self._fnDPrint(sText);
523 return True;
524
525 def debugHtmlReport(self, tsStart = 0):
526 """
527 Used to get a SQL activity dump as HTML, usually for WuiBase._sDebug.
528 """
529 cNsElapsed = 0;
530 for aEntry in self._aoTraceBack:
531 cNsElapsed += aEntry[2];
532
533 sDebug = '<h3>SQL Debug Log (total time %s ns):</h3>\n' \
534 '<table class="tmsqltable">\n' \
535 ' <tr>\n' \
536 ' <th>No.</th>\n' \
537 ' <th>Timestamp (ns)</th>\n' \
538 ' <th>Elapsed (ns)</th>\n' \
539 ' <th>Rows Returned</th>\n' \
540 ' <th>Command</th>\n' \
541 ' <th>Caller</th>\n' \
542 ' </tr>\n' \
543 % (utils.formatNumber(cNsElapsed, '&nbsp;'),);
544
545 iEntry = 0;
546 for aEntry in self._aoTraceBack:
547 iEntry += 1;
548 sDebug += ' <tr>\n' \
549 ' <td align="right">%s</td>\n' \
550 ' <td align="right">%s</td>\n' \
551 ' <td align="right">%s</td>\n' \
552 ' <td align="right">%s</td>\n' \
553 ' <td><pre>%s</pre></td>\n' \
554 ' <td>%s</td>\n' \
555 ' </tr>\n' \
556 % (iEntry,
557 utils.formatNumber(aEntry[0] - tsStart, '&nbsp;'),
558 utils.formatNumber(aEntry[2], '&nbsp;'),
559 utils.formatNumber(aEntry[3], '&nbsp;'),
560 webutils.escapeElem(aEntry[1]),
561 webutils.escapeElem(aEntry[4]),
562 );
563 if aEntry[5] is not None:
564 sDebug += ' <tr>\n' \
565 ' <td colspan="6"><pre style="white-space: pre-wrap;">%s</pre></td>\n' \
566 ' </tr>\n' \
567 % (webutils.escapeElem('\n'.join([aoRow[0] for aoRow in aEntry[5]])),);
568
569 sDebug += '</table>';
570 return sDebug;
571
572 def debugTextReport(self, tsStart = 0):
573 """
574 Used to get a SQL activity dump as text.
575 """
576 cNsElapsed = 0;
577 for aEntry in self._aoTraceBack:
578 cNsElapsed += aEntry[2];
579
580 sHdr = 'SQL Debug Log (total time %s ns)' % (utils.formatNumber(cNsElapsed),);
581 sDebug = sHdr + '\n' + '-' * len(sHdr) + '\n';
582
583 iEntry = 0;
584 for aEntry in self._aoTraceBack:
585 iEntry += 1;
586 sHdr = 'Query #%s Timestamp: %s ns Elapsed: %s ns Rows: %s Caller: %s' \
587 % ( iEntry,
588 utils.formatNumber(aEntry[0] - tsStart),
589 utils.formatNumber(aEntry[2]),
590 utils.formatNumber(aEntry[3]),
591 aEntry[4], );
592 sDebug += '\n' + sHdr + '\n' + '-' * len(sHdr) + '\n';
593
594 sDebug += aEntry[1];
595 if sDebug[-1] != '\n':
596 sDebug += '\n';
597
598 if aEntry[5] is not None:
599 sDebug += 'Explain:\n' \
600 ' %s\n' \
601 % ( '\n'.join([aoRow[0] for aoRow in aEntry[5]]),);
602
603 return sDebug;
604
605 def debugInfoCallback(self, oGlue, fHtml):
606 """ Called back by the glue code on error. """
607 oGlue.write('\n');
608 if not fHtml: oGlue.write(self.debugTextReport());
609 else: oGlue.write(self.debugHtmlReport());
610 oGlue.write('\n');
611 return True;
612
613 def debugEnableExplain(self):
614 """ Enabled explain. """
615 if self._oExplainConn is None:
616 dArgs = \
617 { \
618 'database': config.g_ksDatabaseName,
619 'user': config.g_ksDatabaseUser,
620 'password': config.g_ksDatabasePassword,
621 # 'application_name': sAppName, - Darn stale debian! :/
622 };
623 if config.g_ksDatabaseAddress is not None:
624 dArgs['host'] = config.g_ksDatabaseAddress;
625 if config.g_ksDatabasePort is not None:
626 dArgs['port'] = config.g_ksDatabasePort;
627 self._oExplainConn = psycopg2.connect(**dArgs); # pylint: disable=W0142
628 self._oExplainCursor = self._oExplainConn.cursor();
629 return True;
630
Note: See TracBrowser for help on using the repository browser.

© 2025 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette