Ticket #1099: dCursorMixin.py

File dCursorMixin.py, 68.8 kB (added by ed, 10 months ago)

dCursorMixin modified with debugging output

Line 
1 # -*- coding: utf-8 -*-
2 # dabo/db/dCursorMixin
3
4 import types
5 import datetime
6 import inspect
7 import random
8 import sys
9 import re
10 import array
11 # Make sure that the user's installation supports Decimal.
12 _USE_DECIMAL = True
13 try:
14     from decimal import Decimal
15 except ImportError:
16     _USE_DECIMAL = False
17
18 import dabo
19 import dabo.dConstants as kons
20 from dabo.dLocalize import _
21 import dabo.dException as dException
22 from dabo.dObject import dObject
23 from dNoEscQuoteStr import dNoEscQuoteStr
24 from dabo.db import dTable
25 from dabo.db.dDataSet import dDataSet
26 from dabo.lib import dates
27
28
29 class dCursorMixin(dObject):
30     """Dabo's cursor class, representing the lowest tier."""
31     _call_initProperties = False
32    
33    
34     def debug(self):
35         return self._bizobj.DataSource == "FotoInf"
36        
37        
38        
39     def __init__(self, sql="", *args, **kwargs):
40         self._convertStrToUnicode = True
41         self._initProperties()
42         if sql and isinstance(sql, basestring) and len(sql) > 0:
43             self.UserSQL = sql
44
45         #self.super()
46         #super(dCursorMixin, self).__init__()
47         ## pkm: Neither of the above are correct. We need to explicitly
48         ##      call dObject's __init__, otherwise the cursor object with
49         ##      which we are mixed-in will take the __init__.
50         dObject.__init__(self, *args, **kwargs)
51
52         # Just in case this is used outside of the context of a bizobj
53         if not hasattr(self, "superCursor") or self.superCursor is None:
54             myBases = self.__class__.__bases__
55             for base in myBases:
56                 # Find the first base class that doesn't have the 'autoPopulatePK'
57                 # attribute. Designate that class as the superCursor class.
58                 if hasattr(base, "fetchall"):
59                     self.superCursor = base
60                     break
61
62
63     def _initProperties(self):
64         # Holds the dict used for adding new blank records
65         self._blank = {}
66         # Writable version of the dbapi 'description' attribute
67         self.descriptionClean = None
68         # Last executed sql params
69         self.lastParams = None
70         # Column on which the result set is sorted
71         self.sortColumn = ""
72         # Order of the sorting. Should be either ASC, DESC or empty for no sort
73         self.sortOrder = ""
74         # Is the sort case-sensitive?
75         self.sortCase = True
76         # Holds the keys in the original, unsorted order for unsorting the dataset
77         self.__unsortedRows = []
78         # Holds the name of fields to be skipped when updating the backend, such
79         # as calculated or derived fields, or fields that are otherwise not to be updated.
80         self.__nonUpdateFields = None
81         # User-editable list of non-updated fields
82         self.nonUpdateFields = []
83         # Flag that is set when the user explicitly sets the Key Field
84         self._keyFieldSet = False
85         # Cursor that manages this cursor's SQL. Default to self;
86         # in some cases, such as a single bizobj managing several cursors,
87         # it will be a separate object.
88         self.sqlManager = self
89         # Attribute that holds the data of the cursor
90         self._records = dDataSet()
91         # Attribute that holds the current row number
92         self.__rownumber = -1
93         # Data structure info
94         self._dataStructure = None
95         self._table = ""
96         self._keyField = ""
97         self._userSQL = None
98         self._virtualFields = {}
99        
100         self._autoPopulatePK = True
101         self._autoQuoteNames = True
102
103         self.__tmpPK = -1       # temp PK value for new records.
104         # Holds the data types for each field
105         self._types = {}
106
107         # Holds reference to auxiliary cursor that handles queries that
108         # are not supposed to affect the record set.
109         self.__auxCursor = None
110         # Marks the cursor as an auxiliary cursor
111         self._isAuxiliary = False
112
113         # Reference to the object with backend-specific behaviors
114         self.__backend = None
115
116         # Reference to the bizobj that 'owns' this cursor, if any,
117         self._bizobj = None
118
119         # set properties for the SQL Builder functions
120         self.clearSQL()
121         self.hasSqlBuilder = True
122
123         # props for building the auxiliary cursor
124         self._cursorFactoryFunc = None
125         self._cursorFactoryClass = None
126
127         # mementos and new records, keyed on record object ids:
128         self._mementos = {}
129         self._newRecords = {}
130
131         # Flag preference cursors so that they don't fill up the logs
132         self._isPrefCursor = False
133
134         self.initProperties()
135
136
137     def setCursorFactory(self, func, cls):
138         self._cursorFactoryFunc = func
139         self._cursorFactoryClass = cls
140
141
142     def clearSQL(self):
143         self._fieldClause = ""
144         self._fromClause = ""
145         self._joinClause = ""
146         self._whereClause = ""
147         self._childFilterClause = ""
148         self._groupByClause = ""
149         self._orderByClause = ""
150         self._limitClause = ""
151         self._defaultLimit = 1000
152
153
154     def getSortColumn(self):
155         return self.sortColumn
156
157
158     def getSortOrder(self):
159         return self.sortOrder
160
161
162     def getSortCase(self):
163         return self.sortCase
164
165
166     def pkExpression(self, rec=None):
167         """Returns the PK expression for the passed record."""
168         if rec is None:
169             rec = self._records[self.RowNumber]
170         if isinstance(self.KeyField, tuple):
171             pk = tuple([rec[kk] for kk in self.KeyField])
172         else:
173             pk = rec[self.KeyField]
174         return pk
175
176
177     def _correctFieldType(self, field_val, field_name, _fromRequery=False):
178         """Correct the type of the passed field_val, based on self.DataStructure.
179
180         This is called by self.execute(), and contains code to convert all strings
181         to unicode, as well as to correct any datatypes that don't match what
182         self.DataStructure reports. The latter can happen with SQLite, for example,
183         which only knows about a quite limited number of types.
184         """
185         ret = field_val
186         showError = False
187         if _fromRequery:
188             pythonType = self._types.get(field_name, type(field_val))
189             daboType = dabo.db.getDaboType(pythonType)
190
191             if pythonType in (type(None), None) or isinstance(field_val, pythonType):
192                 # No conversion needed.
193                 return ret
194
195             if pythonType in (unicode,):
196                 # Unicode conversion happens below.
197                 pass
198             elif field_val is None:
199                 # Fields of any type can be None (NULL).
200                 return field_val
201             elif pythonType in (datetime.datetime, ) and isinstance(field_val, basestring):
202                 ret = dates.getDateTimeFromString(field_val)
203                 if ret is None:
204                     ret = field_val
205                 else:
206                     return ret
207             elif pythonType in (datetime.date,) and isinstance(field_val, basestring):
208                 ret = dates.getDateFromString(field_val)
209                 if ret is None:
210                     ret = field_val
211                 else:
212                     return ret
213             elif _USE_DECIMAL and pythonType in (Decimal,):
214                 ds = self.DataStructure
215                 ret = None
216                 _field_val = field_val
217                 if type(field_val) in (float,):
218                     # Can't convert to decimal directly from float
219                     _field_val = str(_field_val)
220                 # Need to convert to the correct scale:
221                 scale = None
222                 for s in ds:
223                     if s[0] == field_name:
224                         if len(s) > 5:
225                             scale = s[5]
226                 if scale is None:
227                     scale = 2
228                 return Decimal(_field_val).quantize(Decimal("0.%s" % (scale * "0",)))
229             else:
230                 try:
231                     return pythonType(field_val)
232                 except Exception, e:
233                     dabo.infoLog.write(_("_correctFieldType() failed for field: '%s'; value: '%s'; type: '%s'") %
234                             (field_name, field_val, type(field_val)))
235
236         # Do the unicode conversion last:
237         if isinstance(field_val, str) and self._convertStrToUnicode:
238             try:
239                 return unicode(field_val, self.Encoding)
240             except UnicodeDecodeError, e:
241                 # Try some common encodings:
242                 ok = False
243                 for enc in ("utf-8", "latin-1", "iso-8859-1"):
244                     if enc != self.Encoding:
245                         try:
246                             ret = unicode(field_val, enc)
247                             ok = True
248                         except UnicodeDecodeError:
249                             continue
250                         if ok:
251                             # change self.Encoding and log the message
252                             self.Encoding = enc
253                             dabo.errorLog.write(_("Field %(fname)s: Incorrect unicode encoding set; using '%(enc)s' instead")
254                                 % {'fname':field_name, 'enc':enc} )
255                             return ret
256                             break
257                 else:
258                     raise UnicodeDecodeError, e
259 #       elif isinstance(field_val, array.array):
260 #           # Usually blob data
261 #           ret = field_val.tostring()
262
263             dabo.errorLog.write(_("%s couldn't be converted to %s (field %s)")
264                     % (repr(field_val), pythonType, field_name))
265         return ret
266
267
268     def execute(self, sql, params=(), _fromRequery=False, errorClass=None):
269         """ Execute the sql, and populate the DataSet if it is a select statement."""
270         # The idea here is to let the super class do the actual work in
271         # retrieving the data. However, many cursor classes can only return
272         # row information as a list, not as a dictionary. This method will
273         # detect that, and convert the results to a dictionary.
274         if not isinstance(sql, unicode):
275             sql = unicode(sql, self.Encoding)
276         # Some backends, notably Firebird, require that fields be specially marked.
277         sql = self.processFields(sql)
278
279         try:
280             if params is None or len(params) == 0:
281                 res = self.superCursor.execute(self, sql)
282                 if not self.IsPrefCursor:
283                     dabo.dbActivityLog.write("SQL: %s" % sql.replace("\n", " "))
284             else:
285                 res = self.superCursor.execute(self, sql, params)
286                 if not self.IsPrefCursor:
287                     dabo.dbActivityLog.write("SQL: %s, PARAMS: %s" % (sql.replace("\n", " "), ", ".join(params)))
288         except Exception, e:
289             # There can be cases where errors are expected. In those cases, the
290             # calling routine will pass the class of the expected error, and will
291             # handle it appropriately.
292             if errorClass is not None and isinstance(e, errorClass):
293                 raise errorClass, e
294             dabo.dbActivityLog.write("FAILED SQL: %s, PARAMS: %s" % (sql.replace("\n", " "), ", ".join(params)))
295             # If this is due to a broken connection, let the user know.
296             # Different backends have different messages, but they
297             # should all contain the string 'connect' in them.
298             if "connect" in str(e).lower():
299                 raise dException.ConnectionLostException, e
300             if "access" in str(e).lower():
301                 raise dException.DBNoAccessException(e)
302             else:
303                 raise dException.DBQueryException(e, sql)
304
305         # Some backend programs do odd things to the description
306         # This allows each backend to handle these quirks individually.
307         self.BackendObject.massageDescription(self)
308
309         if _fromRequery:
310             self._storeFieldTypes()
311
312         if sql.strip().split()[0].lower() not in  ("select", "pragma"):
313             # No need to massage the data for DML commands
314             self._records = dDataSet(tuple())
315             return res
316
317         try:
318             _records = self.fetchall()
319         except Exception, e:
320             _records = tuple()
321             dabo.errorLog.write("Error fetching records: %s" % e)
322
323         if _records and not self.BackendObject._alreadyCorrectedFieldTypes:
324             if isinstance(_records[0], (tuple, list)):
325                 # Need to convert each row to a Dict, since the backend didn't do it.
326                 tmpRows = []
327                 fldNames = [f[0] for f in self.FieldDescription]
328                 for row in _records:
329                     dic = {}
330                     for idx, fldName in enumerate(fldNames):
331                         dic[fldName] = self._correctFieldType(field_val=row[idx],
332                                 field_name=fldName, _fromRequery=_fromRequery)
333                     tmpRows.append(dic)
334                 _records = tmpRows
335             else:
336                 # Already a DictCursor, but we still need to correct the field types.
337                 for row in _records:
338                     for fld, val in row.items():
339                         row[fld] = self._correctFieldType(field_val=val, field_name=fld,
340                                 _fromRequery=_fromRequery)
341
342         self._records = dDataSet(_records)
343
344         if self.RowCount > 0:
345             self.RowNumber = max(0, self.RowNumber)
346             maxrow = max(0, (self.RowCount-1) )
347             self.RowNumber = min(self.RowNumber, maxrow)
348
349         return res
350
351
352     def executeSafe(self, sql):
353         """Execute the passed SQL using an auxiliary cursor.
354         This is considered 'safe', because it won't harm the contents
355         of the main cursor.
356         """
357         ac = self.AuxCursor
358         self._syncAuxProperties()
359         return ac.execute(sql)
360
361
362     def _syncAuxProperties(self):
363         """Make sure that the auxiliary cursor has the same property
364         settings as the main cursor.
365         """
366         if self._isAuxiliary:
367             # Redundant!
368             return
369         ac = self.AuxCursor
370         ac.AutoPopulatePK = self.AutoPopulatePK
371         ac.AutoQuoteNames = self.AutoQuoteNames
372         ac.DataStructure = self.DataStructure
373         ac.IsPrefCursor = self.IsPrefCursor
374         ac.KeyField = self.KeyField
375         ac.Table = self.Table
376
377
378     def requery(self, params=None):
379         self._lastSQL = self.CurrentSQL
380         self.lastParams = params
381         self._savedStructureDescription = []
382        
383         if self.debug():
384             print "CURSOR REQUERY - USERSQL: ", self.UserSQL, self
385
386         self.execute(self.CurrentSQL, params, _fromRequery=True)
387
388         # clear mementos and new record flags:
389         self._mementos = {}
390         self._newRecords = {}
391
392         # Check for any derived fields that should not be included in
393         # any updates.
394         self.__setNonUpdateFields()
395
396         # Clear the unsorted list, and then apply the current sort
397         self.__unsortedRows = []
398         if self.sortColumn:
399             try:
400                 self.sort(self.sortColumn, self.sortOrder)
401             except dException.NoRecordsException, e:
402                 # No big deal
403                 pass
404         return True
405
406
407     def _storeFieldTypes(self, target=None):
408         """Stores the data type for each column in the result set."""
409         if target is None:
410             target = self
411         target._types = {}
412         for field in self.DataStructure:
413             field_alias, field_type = field[0], field[1]
414             target._types[field_alias] = dabo.db.getPythonType(field_type)
415
416
417     def sort(self, col, dir=None, caseSensitive=True):
418         """ Sort the result set on the specified column in the specified order.
419
420         If the sort direction is not specified, sort() cycles among Ascending,
421         Descending and no sort order.
422         """
423         currCol = self.sortColumn
424         currOrd = self.sortOrder
425         currCase = self.sortCase
426
427         # Check to make sure that we have data
428         if self.RowCount < 1:
429             raise dException.NoRecordsException, _("No rows to sort.")
430
431         # Make sure that the specified column is a column in the result set
432         if not self._records[0].has_key(col) and not self.VirtualFields.has_key(col):
433             raise dException.dException, _("Invalid column specified for sort: ") + col
434
435         newCol = col
436         if col == currCol:
437             # Not changing the column; most likely they are flipping
438             # the sort order.
439             if (dir is None) or not dir:
440                 # They didn't specify the sort. Cycle through the sort orders
441                 if currOrd == "ASC":
442                     newOrd = "DESC"
443                 elif currOrd == "DESC":
444                     newOrd = ""
445                 else:
446                     newOrd = "ASC"
447             else:
448                 if dir.upper() in ("ASC", "DESC", ""):
449                     newOrd = dir.upper()
450                 else:
451                     raise dException.dException, _("Invalid Sort direction specified: ") + dir
452
453         else:
454             # Different column specified.
455             if (dir is None) or not dir:
456                 # Start in ASC order
457                 newOrd = "ASC"
458             else:
459                 if dir.upper() in ("ASC", "DESC", ""):
460                     newOrd = dir.upper()
461                 else:
462                     raise dException.dException, _("Invalid Sort direction specified: ") + dir
463
464         self.__sortRows(newCol, newOrd, caseSensitive)
465         # Save the current sort values
466         self.sortColumn = newCol
467         self.sortOrder = newOrd
468         self.sortCase = caseSensitive
469
470
471     def __sortRows(self, col, ord, caseSensitive):
472         """ Sort the rows of the cursor.
473
474         At this point, we know we have a valid column and order. We need to
475         preserve the unsorted order if we haven't done that yet; then we sort
476         the data according to the request.
477         """
478         kf = self.KeyField
479         if not kf:
480             return
481
482         if not self.__unsortedRows:
483             # Record the PK values
484             for row in self._records:
485                 if self._compoundKey:
486                     key = tuple([row[k] for k in kf])
487                     self.__unsortedRows.append(key)
488                 else:
489                     self.__unsortedRows.append(row[self.KeyField])
490
491         # First, preserve the PK of the current row so that we can reset
492         # the RowNumber property to point to the same row in the new order.
493         try:
494             if self._compoundKey:
495                 currRow = self._records[self.RowNumber]
496                 currRowKey = tuple([currRow[k] for k in kf])
497             else:
498                 currRowKey = self._records[self.RowNumber][self.KeyField]
499         except IndexError:
500             # Row no longer exists, such as after a Requery that returns
501             # fewer rows.
502             currRowKey = None
503         # Create the list to hold the rows for sorting
504         sortList = []
505         if not ord:
506             # Restore the rows to their unsorted order
507             for row in self._records:
508                 if self._compoundKey:
509                     key = tuple([row[k] for k in kf])
510                     sortList.append([self.__unsortedRows.index(key), row])
511                 else:
512                     sortList.append([self.__unsortedRows.index(row[self.KeyField]), row])
513         else:
514             for row, rec in enumerate(self._records):
515                 sortList.append([self.getFieldVal(col, row), rec])
516         # At this point we have a list consisting of lists. Each of these member
517         # lists contain the sort value in the zeroth element, and the row as
518         # the first element.
519         # First, see if we are comparing strings
520         compString = isinstance(sortList[0][0], basestring)
521         sortfunc = None
522
523         # can't compare NoneType to some types: sort None lower than anything else:
524         def noneSort(vv, ww):
525             xx, yy = vv[0], ww[0]
526             if xx is None and yy is None:
527                 return 0
528             elif xx is None and yy is not None:
529                 return -1
530             elif xx is not None and yy is None:
531                 return 1
532             else:
533                 return cmp(xx, yy)
534
535         def caseInsensitiveSort(vv, ww):
536             vv, ww = vv[0], ww[0]
537             if vv is None:
538                 vv = ""
539             if ww is None:
540                 ww = ""
541             return cmp(vv.lower(), ww.lower())
542
543         if compString and not caseSensitive:
544             sortfunc = caseInsensitiveSort
545         else:
546             sortfunc = noneSort
547         sortList.sort(sortfunc)
548
549         # Unless DESC was specified as the sort order, we're done sorting
550         if ord == "DESC":
551             sortList.reverse()
552         # Extract the rows into a new list, then convert them back to the _records tuple
553         newRows = []
554         for elem in sortList:
555             newRows.append(elem[1])
556         self._records = dDataSet(newRows)
557
558         # restore the RowNumber
559         if currRowKey:
560             for ii in range(0, self.RowCount):
561                 row = self._records[ii]
562                 if self._compoundKey:
563                     key = tuple([row[k] for k in kf])
564                     found = (key == currRowKey)
565                 else:
566                     found = row[self.KeyField] == currRowKey
567