VirtualBox

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

Last change on this file was 106061, checked in by vboxsync, 4 days ago

Copyright year updates by scm.

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

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