Ticket #1370: dCursorMixin.py

File dCursorMixin.py, 87.1 kB (added by JacekK, 2 years ago)
Line 
1 # -*- coding: utf-8 -*-
2 # dabo/db/dCursorMixin
3
4 import datetime
5 import time
6 import re
7 from decimal import Decimal
8 import dabo
9 import dabo.dConstants as kons
10 from dabo.dLocalize import _
11 import dabo.dException as dException
12 from dabo.dObject import dObject
13 from dNoEscQuoteStr import dNoEscQuoteStr
14 from dabo.db.dDataSet import dDataSet
15 from dabo.lib import dates
16 from dabo.lib.utils import noneSortKey, caseInsensitiveSortKey
17 from dabo.lib.utils import ustr
18
19
20 class dCursorMixin(dObject):
21     """Dabo's cursor class, representing the lowest tier."""
22     _call_initProperties = False
23     # Make these class attributes, so that they are shared among all instances
24     _fieldStructure = {}
25     _fieldsToAlwaysCorrectType = []
26
27     def __init__(self, sql="", *args, **kwargs):
28         self._convertStrToUnicode = True
29         self._initProperties()
30         if sql and isinstance(sql, basestring) and len(sql) > 0:
31             self.UserSQL = sql
32
33         #self.super()
34         #super(dCursorMixin, self).__init__()
35         ## pkm: Neither of the above are correct. We need to explicitly
36         ##      call dObject's __init__, otherwise the cursor object with
37         ##      which we are mixed-in will take the __init__.
38         dObject.__init__(self, *args, **kwargs)
39
40         # Just in case this is used outside of the context of a bizobj
41         if not hasattr(self, "superCursor") or self.superCursor is None:
42             myBases = self.__class__.__bases__
43             for base in myBases:
44                 # Find the first base class that doesn't have the 'autoPopulatePK'
45                 # attribute. Designate that class as the superCursor class.
46                 if hasattr(base, "fetchall"):
47                     self.superCursor = base
48                     break
49
50
51     def _initProperties(self):
52         # Holds the dict used for adding new blank records
53         self._blank = {}
54         # Flag for indicating NULL default values were set
55         self._nullDefaults = False
56         # Writable version of the dbapi 'description' attribute
57         self.descriptionClean = None
58         # Last executed sql params
59         self.lastParams = None
60         # Column on which the result set is sorted
61         self.sortColumn = ""
62         # Order of the sorting. Should be either ASC, DESC or empty for no sort
63         self.sortOrder = ""
64         # Is the sort case-sensitive?
65         self.sortCase = True
66         # Holds the last SQL run in a requery() call.
67         self._lastSQL = ""
68         # Hold the time that this cursor was last requeried.
69         self.lastRequeryTime = 0
70         # These are used to determine if the field list of successive select statements
71         # are identical.
72         self.__lastExecute = ""
73         self.__lastFieldList = ""
74         self._whitespacePat = re.compile(r"(\s+)")
75         self._selectStatementPat = re.compile(r"\bselect\b(.+)\bfrom\b", re.I | re.M | re.S)
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         # Get the parameter for the backend type
135         self._paramPlaceholder = None
136
137         self.initProperties()
138
139
140     def setCursorFactory(self, func, cls):
141         self._cursorFactoryFunc = func
142         self._cursorFactoryClass = cls
143
144
145     def clearSQL(self):
146         self._fieldClause = ""
147         self._fromClause = ""
148         self._joinClause = ""
149         self._whereClause = ""
150         self._childFilterClause = ""
151         self._groupByClause = ""
152         self._orderByClause = ""
153         self._limitClause = ""
154         self._defaultLimit = 1000
155
156
157     def getSortColumn(self):
158         return self.sortColumn
159
160
161     def getSortOrder(self):
162         return self.sortOrder
163
164
165     def getSortCase(self):
166         return self.sortCase
167
168
169     def pkExpression(self, rec=None):
170         """Returns the PK expression for the passed record."""
171         if rec is None:
172             try:
173                 rec = self._records[self.RowNumber]
174             except IndexError:
175                 rec = {}
176         if isinstance(self.KeyField, tuple):
177             if rec:
178                 pk = tuple([rec[kk] for kk in self.KeyField])
179             else:
180                 pk = tuple([None for kk in self.KeyField])
181         else:
182             pk = rec.get(self.KeyField, None)
183         return pk
184
185
186     def pkFieldExpression(self):
187         """Returns the field or comma-separated list of field names
188         for the PK for this cursor's table.
189         """
190         if isinstance(self.KeyField, tuple):
191             pkField = ", ".join([kk for kk in self.KeyField])
192         else:
193             pkField = self.KeyField
194         return pkField
195
196
197     def _correctFieldType(self, field_val, field_name, _newQuery=False):
198         """Correct the type of the passed field_val, based on self.DataStructure.
199
200         This is called by self.execute(), and contains code to convert all strings
201         to unicode, as well as to correct any datatypes that don't match what
202         self.DataStructure reports. The latter can happen with SQLite, for example,
203         which only knows about a quite limited number of types.
204         """
205         if field_val is None:
206             return field_val
207         ret = field_val
208         if _newQuery or (field_name in self._fieldsToAlwaysCorrectType):
209             pythonType = self._types.get(field_name, None)
210             if pythonType is None or pythonType == type(None):
211                 pythonType = self._types[field_name] = dabo.db.getDataType(type(field_val))
212
213             if isinstance(field_val, str) and self._convertStrToUnicode:
214                 # convert to unicode
215                 pass
216             elif pythonType is None or isinstance(field_val, pythonType):
217                 # No conversion needed.
218                 return ret
219             else:
220                 self._fieldsToAlwaysCorrectType.append(field_name)
221
222             if pythonType in (unicode,):
223                 # Unicode conversion happens below.
224                 pass
225             elif pythonType in (datetime.datetime, ) and isinstance(field_val, basestring):
226                 ret = dates.getDateTimeFromString(field_val)
227                 if ret is None:
228                     ret = field_val
229                 else:
230                     return ret
231             elif pythonType in (datetime.date,) and isinstance(field_val, basestring):
232                 ret = dates.getDateFromString(field_val)
233                 if ret is None:
234                     ret = field_val
235                 else:
236                     return ret
237             elif pythonType in (Decimal,):
238                 ds = self.DataStructure
239                 ret = None
240                 _field_val = field_val
241                 if type(field_val) in (float,):
242                     # Can't convert to decimal directly from float
243                     _field_val = ustr(_field_val)
244                 # Need to convert to the correct scale:
245                 scale = None
246                 for s in ds:
247                     if s[0] == field_name:
248                         if len(s) > 5:
249                             scale = s[5]
250                 if scale is None:
251                     scale = 2
252                 return Decimal(_field_val).quantize(Decimal("0.%s" % (scale * "0",)))
253             else:
254                 try:
255                     return pythonType(field_val)
256                 except Exception, e:
257                     tfv = type(field_val)
258                     dabo.infoLog.write(_("_correctFieldType() failed for field: '%(field_name)s'; value: '%(field_val)s'; type: '%(tfv)s'")
259                             % locals())
260
261         # Do the unicode conversion last:
262         if isinstance(field_val, str) and self._convertStrToUnicode:
263             try:
264                 decoded = field_val.decode(self.Encoding)
265                 return decoded
266             except UnicodeDecodeError, e:
267                 # Try some common encodings:
268                 ok = False
269                 for enc in ("utf-8", "latin-1", "iso-8859-1"):
270                     if enc != self.Encoding:
271                         try:
272                             ret = field_val.decode(enc)
273                             ok = True
274                         except UnicodeDecodeError:
275                             continue
276                         if ok:
277                             # change self.Encoding and log the message
278                             self.Encoding = enc
279                             dabo.errorLog.write(_("Field %(fname)s: Incorrect unicode encoding set; using '%(enc)s' instead")
280                                 % {'fname':field_name, 'enc':enc} )
281                             return ret
282                 else:
283                     raise e
284 #       elif isinstance(field_val, array.array):
285 #           # Usually blob data
286 #           ret = field_val.tostring()
287
288             rfv = repr(field_val)
289             dabo.errorLog.write(_("%(rfv)s couldn't be converted to %(pythonType)s (field %(field_name)s)")
290                     % locals())
291         return ret
292
293
294     def execute(self, sql, params=None, _newQuery=False, errorClass=None, convertQMarks=False):
295         """Execute the sql, and populate the DataSet if it is a select statement."""
296         # The idea here is to let the super class do the actual work in
297         # retrieving the data. However, many cursor classes can only return
298         # row information as a list, not as a dictionary. This method will
299         # detect that, and convert the results to a dictionary.
300         if isinstance(sql, unicode):
301             sql = sql.encode(self.Encoding)
302         sql = self.processFields(sql)
303         if convertQMarks:
304             sql = self._qMarkToParamPlaceholder(sql)
305         # Some backends, notably Firebird, require that fields be specially marked.
306         sql = self.processFields(sql)
307         try:
308             if params:
309                 res = self.superCursor.execute(self, sql, params)
310                 if not self.IsPrefCursor:
311                     try:
312                         dabo.dbActivityLog.write("SQL: %s, PARAMS: %s" % (
313                                 sql.decode(self.Encoding).replace("\n", " "),
314                                 ', '.join("%s" % p for p in params)))
315                     except StandardError:
316                         # A problem with writing to the log, most likely due to encoding issues
317                         dabo.dbActivityLog.write("FAILED SQL: %s")
318             else:
319                 res = self.superCursor.execute(self, sql)
320                 if not self.IsPrefCursor:
321                     dabo.dbActivityLog.write("SQL: %s" % (
322                             sql.decode(self.Encoding).replace("\n", " "),))
323         except Exception, e:
324             # There can be cases where errors are expected. In those cases, the
325             # calling routine will pass the class of the expected error, and will
326             # handle it appropriately.
327             if errorClass is not None and isinstance(e, errorClass):
328                 raise e
329             if params:
330                 try:
331                     dabo.dbActivityLog.write("FAILED SQL: %s, PARAMS: %s" % (
332                             sql.decode(self.Encoding).replace("\n", " "),
333                             ', '.join("%s" % p for p in params)))
334                 except StandardError:
335                     # A problem with writing to the log, most likely due to encoding issues
336                     dabo.dbActivityLog.write("FAILED SQL: %s")
337             else:
338                 dabo.dbActivityLog.write("FAILED SQL: %s" % (
339                         sql.decode(self.Encoding).replace("\n", " "),))
340             # Database errors need to be decoded from database encoding.
341             try:
342                 errMsg = ustr(e)
343             except UnicodeError:
344                 errMsg = unicode(e)
345             # If this is due to a broken connection, let the user know.
346             # Different backends have different messages, but they
347             # should all contain the string 'connect' in them.
348             if "connect" in errMsg.lower():
349                 raise dException.ConnectionLostException(errMsg)
350             elif "access" in errMsg.lower():
351                 raise dException.DBNoAccessException(errMsg)
352             else:
353                 dabo.dbActivityLog.write(
354                         _("DBQueryException encountered in execute(): %s\n%s") % (errMsg, sql))
355                 raise dException.DBQueryException(errMsg)
356
357         # Some backend programs do odd things to the description
358         # This allows each backend to handle these quirks individually.
359         self.BackendObject.massageDescription(self)
360
361         if self._newStructure(sql):
362             self._storeFieldTypes()
363
364         if sql.split(None, 1)[0].lower() not in ("select", "pragma"):
365             # No need to massage the data for DML commands
366             self._records = dDataSet(tuple())
367             return res
368
369         try:
370             _records = self.fetchall()
371         except Exception, e:
372             _records = dabo.db.dDataSet()
373             # Database errors need to be decoded from database encoding.
374             try:
375                 errMsg = ustr(e).decode(self.Encoding)
376             except UnicodeError:
377                 errMsg = unicode(e)
378             dabo.errorLog.write("Error fetching records: %s" % errMsg)
379
380         if _records and not self.BackendObject._alreadyCorrectedFieldTypes:
381             if isinstance(_records[0], (tuple, list)):
382                 # Need to convert each row to a Dict, since the backend didn't do it.
383                 tmpRows = []
384                 fldNames = [f[0] for f in self.FieldDescription]
385                 for row in _records:
386                     dic = {}
387                     for idx, fldName in enumerate(fldNames):
388                         dic[fldName] = self._correctFieldType(field_val=row[idx],
389                                 field_name=fldName, _newQuery=_newQuery)
390                     tmpRows.append(dic)
391                 _records = tmpRows
392             else:
393                 # Already a DictCursor, but we still need to correct the field types.
394                 for row in _records:
395                     for fld, val in row.items():
396                         row[fld] = self._correctFieldType(field_val=val,
397                                 field_name=fld, _newQuery=_newQuery)
398
399         self._records = dDataSet(_records)
400         # This will handle bounds issues
401         self.RowNumber = self.RowNumber
402         return res
403
404
405     def executeSafe(self, sql, params=None):
406         """Execute the passed SQL using an auxiliary cursor.
407         This is considered 'safe', because it won't harm the contents
408         of the main cursor. Returns the temp cursor.
409         """
410         ac = self.AuxCursor
411         self._syncAuxProperties()
412         ac.execute(sql, params)
413         return ac
414
415
416     def _newStructure(self, sql):
417         """Attempts to parse the SQL to determine if the fields being selected will require
418         a new call to set the structure. Non-select statements likewise will return False.
419         """
420         if self._isAuxiliary:
421             return False
422         if sql == self.__lastExecute:
423             return False
424         # See if it's a select statement
425         mtch = self._selectStatementPat.search(sql)
426         if not mtch:
427             return False
428         # Normalize white space
429         fldlist = self._whitespacePat.sub(" ", mtch.groups()[0]).strip()
430         if self.__lastFieldList == fldlist:
431             return False
432         else:
433             self.__lastFieldList = fldlist
434             return True
435
436
437     def _syncAuxProperties(self):
438         """Make sure that the auxiliary cursor has the same property
439         settings as the main cursor.
440         """
441         if self._isAuxiliary:
442             # Redundant!
443             return
444         ac = self.AuxCursor
445         ac.AutoPopulatePK = self.AutoPopulatePK
446         ac.AutoQuoteNames = self.AutoQuoteNames
447         ac.DataStructure = self.DataStructure
448         ac.IsPrefCursor = self.IsPrefCursor
449         ac.KeyField = self.KeyField
450         ac.Table = self.Table
451
452
453     def requery(self, params=None):
454         currSQL = self.CurrentSQL
455         newQuery = (self._lastSQL != currSQL)
456         self._lastSQL = currSQL
457         self.lastParams = params
458         self._savedStructureDescription = []
459
460         self.execute(currSQL, params, _newQuery=newQuery)
461
462         # clear mementos and new record flags:
463         self._mementos = {}
464         self._newRecords = {}
465         # Record the requery time for caching purposes
466         self.lastRequeryTime = time.time()
467
468         if newQuery:
469             # Check for any derived fields that should not be included in
470             # any updates.
471             self.__setNonUpdateFields()
472
473         # Clear the unsorted list, and then apply the current sort
474         self.__unsortedRows = []
475         if self.sortColumn:
476             try:
477                 self.sort(self.sortColumn, self.sortOrder)
478             except dException.NoRecordsException:
479                 # No big deal
480                 pass
481         return True
482
483
484     def _storeFieldTypes(self, target=None):
485         """Stores the data type for each column in the result set."""
486         try:
487             ## The Record object must be reinstantiated to reflect the new structure:
488             del(self._cursorRecord)
489         except AttributeError:
490             pass
491         if target is None:
492             target = self
493         target._types = {}
494         for field in self.DataStructure:
495             field_alias, field_type = field[0], field[1]
496             target._types[field_alias] = dabo.db.getPythonType(field_type)
497
498
499     def sort(self, col, ordr=None, caseSensitive=True):
500         """ Sort the result set on the specified column in the specified order. If the sort
501         direction is not specified, default to ascending order. If 'cycle' is specified as the
502         direction, use the next ordering in the list [None, 'ASC', 'DESC']. The possible
503         values for 'ordr' are:
504             None
505             "" (i.e., an empty string)
506             ASC
507             DESC
508             CYCLE
509         Only the first three characters are significant; case is ignored.
510         """
511         currCol = self.sortColumn
512         currOrd = self.sortOrder
513         if ordr is None:
514             ordr = "ASC"
515         elif ordr == "":
516             ordr = None
517         if ordr[:3].upper() == "CYC":
518             ordr = {"ASC": "DESC", "DES": None, None: "ASC"}[currOrd]
519             col = currCol
520
521         # Make sure that the specified column is a column in the result set
522         if not [True for t in self.DataStructure if t[0] == col]  and col not in self.VirtualFields:
523             raise dException.dException(
524                     _("Invalid column specified for sort: ") + col)
525
526         newCol = col
527         if col == currCol:
528             # Not changing the column; most likely they are flipping
529             # the sort order.
530             if (ordr is None) or not ordr:
531                 # They didn't specify the sort order. Cycle through the sort orders
532                 if currOrd == "ASC":
533                     newOrd = "DESC"
534                 elif currOrd == "DESC":
535                     newOrd = ""
536                 else:
537                     newOrd = "ASC"
538             else:
539                 if ordr.upper() in ("ASC", "DESC", ""):
540                     newOrd = ordr.upper()
541                 else:
542                     raise dException.dException(
543                             _("Invalid Sort direction specified: ") + ordr)
544
545         else:
546             # Different column specified.
547             if (ordr is None) or not ordr:
548                 # Start in ASC order
549                 newOrd = "ASC"
550             else:
551                 if ordr.upper() in ("ASC", "DESC", ""):
552                     newOrd = ordr.upper()
553                 else:
554                     raise dException.dException(
555                             _("Invalid Sort direction specified: ") + ordr)
556
557         self.__sortRows(newCol, newOrd, caseSensitive)
558         # Save the current sort values
559         self.sortColumn = newCol
560         self.sortOrder = newOrd
561         self.sortCase = caseSensitive
562
563
564     def __sortRows(self, col, ordr, caseSensitive):
565         """ Sort the rows of the cursor.
566
567         At this point, we know we have a valid column and order. We need to
568         preserve the unsorted order if we haven't done that yet; then we sort
569         the data according to the request.
570         """
571         kf = self.KeyField
572         if not kf or not self.RowCount:
573             return
574
575         if not self.__unsortedRows:
576             # Record the PK values
577             for row in self._records:
578                 if self._compoundKey:
579                     key = tuple([row[k] for k in kf])
580                     self.__unsortedRows.append(key)
581                 else:
582                     self.__unsortedRows.append(row[kf])
583
584         # First, preserve the PK of the current row so that we can reset
585         # the RowNumber property to point to the same row in the new order.
586         try:
587             if self._compoundKey:
588                 currRow = self._records[self.RowNumber]
589                 currRowKey = tuple([currRow[k] for k in kf])
590             else:
591                 currRowKey = self._records[self.RowNumber][kf]
592         except IndexError:
593             # Row no longer exists, such as after a Requery that returns
594             # fewer rows.
595             currRowKey = None
596         # Create the list to hold the rows for sorting
597         sortList = []
598         if not ordr:
599             # Restore the rows to their unsorted order
600             for row in self._records:
601                 if self._compoundKey:
602                     key = tuple([row[k] for k in kf])
603                     sortList.append([self.__unsortedRows.index(key), row])
604                 else:
605                     sortList.append([self.__unsortedRows.index(row[kf]), row])
606         else:
607             for row, rec in enumerate(self._records):
608                 sortList.append([self.getFieldVal(col, row), rec])
609         # At this point we have a list consisting of lists. Each of these member
610         # lists contain the sort value in the zeroth element, and the row as
611         # the first element.
612         # First, see if we are comparing strings
613         compString = isinstance(sortList[0][0], basestring)
614
615         if compString and not caseSensitive:
616             sortKey = caseInsensitiveSortKey
617         else:
618             sortKey = noneSortKey
619         sortList.sort(key=sortKey, reverse=(ordr == "DESC"))
620
621         # Extract the rows into a new list, then convert them back to the _records tuple
622         newRows = [elem[1] for elem in sortList]
623         self._records = dDataSet(newRows)
624
625         # restore the RowNumber
626         if currRowKey:
627             for ii in range(0, self.RowCount):
628                 row = self._records[ii]
629                 if self._compoundKey:
630                     key = tuple([row[k] for k in kf])
631                     found = (key == currRowKey)
632                 else:
633                     found = row[kf] == currRowKey
634                 if found:
635                     self.RowNumber = ii
636                     break
637         else:
638             self.RowNumber = 0
639
640
641     def cursorToXML(self):
642         """ Returns an XML string containing the information necessary to
643         re-create this cursor.
644         """
645         base = """<?xml version="1.0" encoding="%s"?>
646 <dabocursor xmlns="http://www.dabodev.com"
647 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
648 xsi:schemaLocation="http://www.dabodev.com dabocursor.xsd"
649 xsi:noNamespaceSchemaLocation = "http://dabodev.com/schema/dabocursor.xsd">
650     <cursor autopopulate="%s" keyfield="%s" table="%s">
651 %s
652     </cursor>
653 </dabocursor>"""
654
655         rowTemplate = """       <row>
656 %s
657         </row>
658 """
659
660         colTemplate = """           <column name="%s" type="%s">%s</column>"""
661
662         rowXML = ""
663         for rec in self._records:
664             recInfo = [ colTemplate % (k, self.getType(v), self.escape(v))
665                     for k,v in rec.items() ]
666             rowXML += rowTemplate % "\n".join(recInfo)
667         return base % (self.Encoding, self.AutoPopulatePK, self.KeyField,
668                 self.Table, rowXML)
669
670
671     def _xmlForRow(self, row=None):
672         """Returns the XML for the specified row; if no row is specified,
673         the current row is used.
674         """
675         colTemplate = """   <column name="%s" type="%s">%s</column>"""
676         if row is None:
677             row = self.RowNumber
678         recInfo = [colTemplate % (k, self.getType(v), self.escape(v))
679                 for k,v in self._records[row].items()]
680         return "\n".join(recInfo)
681
682
683     def getType(self, val):
684         try:
685             ret = re.search("type '([^']+)'", ustr(type(val))).groups()[0]
686         except IndexError:
687             ret = "-unknown-"
688         return ret
689
690
691     def escape(self, val):
692         """ Provides the proper escaping of values in XML output """
693         ret = val
694         if isinstance(val, basestring):
695             if ("\n" in val) or ("<" in val) or ("&" in val):
696                 ret = "<![CDATA[%s]]>" % val.encode(self.Encoding)
697         return ret
698
699
700     def setNonUpdateFields(self, fldList=None):
701         if fldList is None:
702             fldList = []
703         self.nonUpdateFields = fldList
704
705
706     def getNonUpdateFields(self):
707         if self.__nonUpdateFields is None:
708             # They haven't been set yet
709             self.__setNonUpdateFields()
710         return list(set(self.nonUpdateFields + self.__nonUpdateFields))
711
712
713     def __setNonUpdateFields(self, nonUp=None):
714         """Automatically set the non-update fields."""
715         if nonUp is not None:
716             # This is being called back by the BackendObject
717             self.__nonUpdateFields = nonUp
718             return
719         dataStructure = getattr(self, "_dataStructure", None)
720         if dataStructure is not None:
721             # Use the explicitly-set DataStructure to find the NonUpdateFields.
722             self.__nonUpdateFields = [f[0] for f in self.DataStructure
723                     if (f[3] != self.Table) or not f[4]]
724         else:
725             # Create the _dataStructure attribute
726             self._getDataStructure()
727             # Delegate to the backend object to figure it out.
728             self.__nonUpdateFields = self.BackendObject.setNonUpdateFields(self)
729
730
731     def isChanged(self, allRows=True, includeNewUnchanged=False):
732         """Return True if there are any changes to the local field values.
733
734         If allRows is True (the default), all records in the recordset will be
735         considered. Otherwise, only the current record will be checked.
736
737         If includeNewUnchanged is True, new records that have not been
738         modified from their default values, which normally are not
739         considered 'changed', will be counted as 'changed'.
740         """
741         if allRows:
742             if includeNewUnchanged:
743                 return (len(self._mementos) > 0) or (len(self._newRecords) > 0)
744             else:
745                 return len(self._mementos) > 0
746         else:
747             row = self.RowNumber
748             try:
749                 rec = self._records[row]
750             except IndexError:
751                 # self.RowNumber doesn't exist (init phase?) Nothing's changed:
752                 return False
753             recKey = self.pkExpression(rec)
754             modrec = self._mementos.get(recKey, None)
755             if not modrec and includeNewUnchanged:
756                 modrec = recKey in self._newRecords
757             return bool(modrec)
758
759
760     def setNewFlag(self):
761         """Set the current record to be flagged as a new record.
762
763         dBizobj will automatically call this method as appropriate, but if you are
764         using dCursor without a proxy dBizobj, you'll need to manually call this
765         method after cursor.new(), and (if applicable) after cursor.genTempAutoPK().
766         For example:
767             cursor.new()
768             cursor.genTempAutoPK()
769             cursor.setNewFlag()
770         """
771         pk = None
772         if self.KeyField:
773             pk = self.getPK()
774             self._newRecords[pk] = None
775         # Add the 'new record' flag
776         self._records[self.RowNumber][kons.CURSOR_TMPKEY_FIELD] = pk
777
778
779     def genTempAutoPK(self):
780         """ Create a temporary PK for a new record. Set the key field to this
781         value, and also create a temp field to hold it so that when saving the
782         new record, child records that are linked to this one can be updated
783         with the actual PK value.
784         """
785         rec = self._records[self.RowNumber]
786         kf = self.KeyField
787         try:
788             if isinstance(kf, tuple):
789                 pkVal = rec[kf[0]]
790             else:
791                 pkVal = rec[kf]
792         except (IndexError, KeyError):
793             # No records; default to string
794             pkVal = ""
795
796         tmpPK = self._genTempPKVal(pkVal)
797         if isinstance(kf, tuple):
798             for key in kf:
799                 rec[key] = tmpPK
800         else:
801             rec[kf] = tmpPK
802         rec[kons.CURSOR_TMPKEY_FIELD] = tmpPK
803         return tmpPK
804
805
806     def _genTempPKVal(self, pkValue):
807         """ Return the next available temp PK value. It will be a string, and
808         postfixed with '-dabotmp' to avoid potential conflicts with actual PKs
809         """
810         ret = self.__tmpPK
811         # Decrement the temp PK value
812         self.__tmpPK -= 1
813         if isinstance(pkValue, basestring):
814             ret = "%s-dabotmp" % ret
815         return ret
816
817
818     def getPK(self, row=None):
819         """ Returns the value of the PK field in the current or passed record number.
820         If that record is a new unsaved record, return the temp PK value. If this is a
821         compound PK, return a tuple containing each field's values.
822         """
823         if self.RowCount <= 0:
824             raise dException.NoRecordsException(
825                     _("No records in the data set."))
826         ret = None
827         if row is None:
828             row = self.RowNumber
829         rec = self._records[row]
830         recKey = self.pkExpression(rec)
831         if (recKey in self._newRecords) and self.AutoPopulatePK:
832             # New, unsaved record
833             ret = rec[kons.CURSOR_TMPKEY_FIELD]
834         else:
835             kf = self.KeyField
836             if isinstance(kf, tuple):
837                 ret = tuple([rec[k] for k in kf])
838             else:
839                 ret = rec.get(kf, None)
840         return ret
841
842
843     def getFieldVal(self, fld, row=None, _rowChangeCallback=None):
844         """Return the value of the specified field in the current or specified row."""
845         if self.RowCount <= 0:
846             raise dException.NoRecordsException(_("No records in the data set."))
847         if row is None:
848             row = self.RowNumber
849         try:
850             rec = self._records[row]
851         except IndexError:
852             cnt = len(self._records)
853             raise dException.RowNotFoundException(
854                     _("Row #%(row)s requested, but the data set has only %(cnt)s row(s),") % locals())
855         if isinstance(fld, (tuple, list)):
856             ret = []
857             for xFld in fld:
858                 ret.append(self.getFieldVal(xFld, row=row))
859             return tuple(ret)
860         else:
861             try:
862                 return rec[fld]
863             except KeyError:
864                 try:
865                     vf = self.VirtualFields[fld]
866                     if not isinstance(vf, dict):
867                         vf = {"func": vf}
868
869                     requery_children = (vf.get("requery_children", False) and bool(_rowChangeCallback))
870
871                     # Move to specified row if necessary, and then call the VirtualFields
872                     # function, which expects to be on the correct row.
873                     if not requery_children:
874                         # The VirtualFields 'requery_children' key is False, or
875                         # we aren't being called by a bizobj, so there aren't child bizobjs.
876                         _oldrow = self.RowNumber
877                         self.RowNumber = row
878                         ret = vf["func"]()
879                         self.RowNumber = _oldrow
880                         return ret
881                     else:
882                         # The VirtualFields definition's 'requery_children' key is True, so
883                         # we need to request a row change and requery of any child bizobjs
884                         # as necessary, before executing the virtual field function.
885                         _rowChangeCallback(row)
886                         return vf["func"]()
887                 except KeyError:
888                     raise dException.FieldNotFoundException("%s '%s' %s" % (
889                             _("Field"), fld, _("does not exist in the data set")))
890
891
892     def _fldTypeFromDB(self, fld):
893         """Try to determine the field type from the database information
894         If the field isn't found, return None.
895         """
896         ret = None
897         flds = self.getFields()
898         try:
899             typ = [ff[1] for ff in flds if ff[0] == fld][0]
900         except IndexError:
901             # This 'fld' value is not a native field, so no way to
902             # determine its type
903             typ = None
904         if typ:
905             try:
906                 ret = {"C": unicode, "D": datetime.date, "B": bool, "G": long,
907                     "N": float, "M": unicode, "I": int, "T": datetime.datetime}[typ]
908             except KeyError:
909                 ret = None
910         return ret
911
912
913     def _hasValidKeyField(self):
914         """Return True if the KeyField exists and names valid fields."""
915         try:
916             self.checkPK()
917         except dException.MissingPKException:
918             return False
919         return True
920
921
922     def setValuesByDict(self, valDict, row=None):
923         """Set the value for multiple fields with one call by passing a dict containing
924         the field names as keys, and the new values as values.
925         """
926         for fld, val in valDict.items():
927             self.setFieldVal(fld, val, row)
928
929
930     def setFieldVal(self, fld, val, row=None):
931         """Set the value of the specified field."""
932         if self.RowCount <= 0:
933             raise dException.NoRecordsException(_("No records in the data set"))
934         if row is None:
935             row = self.RowNumber
936
937         try:
938             rec = self._records[row]
939         except IndexError:
940             cnt = len(self._records)
941             raise dException.RowNotFoundException(
942                     _("Row #%(row)s requested, but the data set has only %(cnt)s row(s),") % locals())
943         valid_pk = self._hasValidKeyField()
944         keyField = self.KeyField
945         if fld not in rec:
946             if fld in self.VirtualFields:
947                 # ignore
948                 return
949             raise dException.FieldNotFoundException(
950                     _("Field '%s' does not exist in the data set.") % (fld,))
951
952         try:
953             fldType = self._types[fld]
954         except KeyError:
955             fldType = self._fldTypeFromDB(fld)
956         nonUpdateFields = self.getNonUpdateFields()
957         if fldType is not None:
958             if fldType != type(val):
959                 convTypes = (str, unicode, int, float, long, complex)
960                 if issubclass(fldType, basestring) and isinstance(val, convTypes):
961                     if issubclass(fldType, str):
962                         val = ustr(val)
963                     else:
964                         val = unicode(val)
965                 elif issubclass(fldType, int) and isinstance(val, bool):
966                     # convert bool to int (original field val was bool, but UI
967                     # changed to int.
968                     val = int(val)
969                 elif issubclass(fldType, int) and isinstance(val, long):
970                     # convert long to int (original field val was int, but UI
971                     # changed to long.
972                     val = int(val)
973                 elif issubclass(fldType, long) and isinstance(val, int):
974                     # convert int to long (original field val was long, but UI
975                     # changed to int.
976                     val = long(val)
977
978             if fldType != type(val):
979                 ignore = False
980                 # Date and DateTime types are handled as character, even if the
981                 # native field type is not. Ignore these. NOTE: we have to deal with the
982                 # string representation of these classes, as there is no primitive for either
983                 # 'DateTime' or 'Date'.
984                 dtStrings = ("<type 'DateTime'>", "<type 'Date'>", "<type 'datetime.datetime'>")
985                 if ustr(fldType) in dtStrings and isinstance(val, basestring):
986                     ignore = True
987                 elif issubclass(fldType, basestring) and isinstance(val, basestring):
988                     ignore = True
989                 elif val is None or fldType is type(None):
990                     # Any field type can potentially hold None values (NULL). Ignore these.
991                     ignore = True
992                 elif isinstance(val, dNoEscQuoteStr):
993                     # Sometimes you want to set it to a sql function, equation, ect.
994                     ignore = True
995                 elif fld in nonUpdateFields:
996                     # don't worry so much if this is just a calculated field.
997                     ignore = True
998                 else:
999                     # This can also happen with a new record, since we just stuff the
1000                     # fields full of empty strings.
1001                     ignore = (rec.get(keyField, None) in self._newRecords)
1002
1003                 if not ignore:
1004                     sft, stv = ustr(fldType), ustr(type(val))
1005                     msg = _("!!! Data Type Mismatch: field=%(fld)s. Expecting: %(sft)s; got: %(stv)s") % locals()
1006                     dabo.errorLog.write(msg)
1007
1008         # If the new value is different from the current value, change it and also
1009         # update the mementos if necessary.
1010         old_val = rec[fld]
1011         if old_val != val:
1012             if valid_pk:
1013                 if fld == keyField:
1014                     # Changing the key field value, need to key the mementos on the new
1015                     # value, not the old. Additionally, need to copy the mementos from the
1016                     # old key value to the new one.
1017                     keyFieldValue = val
1018                     old_mem = self._mementos.get(old_val, None)
1019                     if old_mem is not None:
1020                         self._mementos[keyFieldValue] = old_mem
1021                         del self._mementos[old_val]
1022                 elif self._compoundKey and fld in keyField:
1023                     # Changing the value of one key field, need to key the mementos on
1024                     # the new compound key value not the old. Additionally, need
1025                     #to copy the mementos from the old key value to the new one.
1026                     oldKeyFieldValue = tuple([rec[k] for k in keyField])
1027                     old_mem = self._mementos.get(oldKeyFieldValue, None)
1028                     keyFieldValue = list(oldKeyFieldValue)
1029                     keyFieldValue[list(keyField).index(fld)] = val
1030                     keyFieldValue = tuple(keyFieldValue)
1031                     if old_mem is not None:
1032                         self._mementos[keyFieldValue] = old_mem
1033                         del self._mementos[oldKeyFieldValue]
1034                 else:
1035                     if self._compoundKey:
1036                         keyFieldValue = tuple([rec[k] for k in keyField])
1037                     else:
1038                         keyFieldValue = rec[keyField]
1039                 mem = self._mementos.get(keyFieldValue, {})
1040                 if (fld in mem) or (fld in nonUpdateFields):
1041                     # Memento is already there, or it isn't updateable.
1042                     pass
1043                 else:
1044                     # Save the memento for this field.
1045                     mem[fld] = old_val
1046
1047                 try:
1048                     if mem[fld] == val:
1049                         # Value changed back to the original memento value; delete the memento.
1050                         del mem[fld]
1051                 except KeyError:
1052                     pass
1053                 if mem:
1054                     self._mementos[keyFieldValue] = mem
1055                 else:
1056                     self._clearMemento(row)
1057             else:
1058                 dabo.infoLog.write("Field value changed, but the memento"
1059                         " can't be saved, because there is no valid KeyField.")
1060
1061             # Finally, save the new value to the field:
1062             rec[fld] = val
1063
1064
1065     def getRecordStatus(self, row=None, pk=None):
1066         """ Returns a dictionary containing an element for each changed
1067         field in the specified record (or the current record if none is specified).
1068         The field name is the key for each element; the value is a 2-element
1069         tuple, with the first element being the original value, and the second
1070         being the current value. You can specify the record by either the
1071         row number or the PK.
1072         """
1073         ret = {}
1074         if pk is not None:
1075             recs = [r for r in self._records
1076                     if r[self._keyField] == pk]
1077             try:
1078                 rec = recs[0]
1079             except IndexError:
1080                 return ret
1081         else:
1082             if row is None:
1083                 row = self.RowNumber
1084             rec = self._records[row]
1085             pk = self.pkExpression(rec)
1086
1087         mem = self._mementos.get(pk, {})
1088
1089         for k, v in mem.items():
1090             ret[k] = (v, rec[k])
1091         return ret
1092
1093
1094     def _getNewRecordDiff(self, row=None, pk=None):
1095         """ Returns a dictionary containing an element for each field
1096         in the specified record (or the current record if none is specified). You
1097         may specify the record by either row number or PK value.
1098         The field name is the key for each element; the value is a 2-element
1099         tuple, with the first element being the original value, and the second
1100         being the current value.
1101
1102         This is used internally in __saverow, and only applies to new records.
1103         """
1104         ret = {}
1105         if pk is not None:
1106             recs = [r for r in self._records
1107                     if r[self._keyField] == pk]
1108             try:
1109                 rec = recs[0]
1110             except IndexError:
1111                 return ret
1112         else:
1113             if row is None:
1114                 row = self.RowNumber
1115             rec = self._records[row]
1116             pk = self.pkExpression(rec)
1117
1118         for k, v in rec.items():
1119             if k not in (kons.CURSOR_TMPKEY_FIELD,):
1120                 ret[k] = (None, v)
1121         return ret
1122
1123
1124     def getCurrentRecord(self):
1125         """Returns the current record (as determined by self.RowNumber)
1126         as a dict, or None if the RowNumber is not a valid record.
1127         """
1128         try:
1129             ret = self.getDataSet(rowStart=self.RowNumber, rows=1)[0]
1130         except IndexError:
1131             ret = None
1132         return ret
1133
1134
1135     def getDataSet(self, flds=(), rowStart=0, rows=None, returnInternals=False):
1136         """ Get the entire data set encapsulated in a dDataSet object.
1137
1138         If the optional 'flds' parameter is given, the result set will be filtered
1139         to only include the specified fields. rowStart specifies the starting row
1140         to include, and rows is the number of rows to return.
1141         """
1142         ds = []
1143         internals = (kons.CURSOR_TMPKEY_FIELD,)
1144
1145         if rows is None:
1146             rows = self.RowCount
1147         for row, rec in enumerate(self._records):
1148             if row >= rowStart and row < (rowStart+rows):
1149                 tmprec = rec.copy()
1150                 for k, v in self.VirtualFields.items():
1151                     # only calc requested virtualFields
1152                     if (flds and k in flds) or not flds:
1153                         tmprec.update({k: self.getFieldVal(k, row)})
1154                 if flds:
1155                     # user specified specific fields - get rid of all others
1156                     for k in tmprec.keys():
1157                         if k not in flds:
1158                             del tmprec[k]
1159                 if not flds and not returnInternals:
1160                     # user didn't specify explicit fields and doesn't want internals
1161                     for internal in internals:
1162                         tmprec.pop(internal, None)
1163                 ds.append(tmprec)
1164         return dDataSet(ds)
1165
1166
1167     def appendDataSet(self, ds):
1168         """Appends the rows in the passed dataset to this cursor's dataset. No checking
1169         is done on the dataset columns to make sure that they are correct for this cursor;
1170         it is the responsibility of the caller to make sure that they match. If invalid data is
1171         passed, a dException.FieldNotFoundException will be raised.
1172         """
1173         kf = self.KeyField
1174         if not isinstance(kf, tuple):
1175             kf = (kf, )
1176         for rec in ds:
1177             self.new()
1178             for col, val in rec.items():
1179                 if self.AutoPopulatePK and (col in kf):
1180                     continue
1181                 self.setFieldVal(col, val)
1182
1183
1184     def cloneRecord(self):
1185         """Creates a copy of the current record and adds it to the dataset."""
1186         if not self.RowCount:
1187             raise dException.NoRecordsException(_("No records in the data set."))
1188         rec = self._records[self.RowNumber].copy()
1189         if self.AutoPopulatePK:
1190             kf = self.KeyField
1191             blank = self._getBlankRecord()
1192             if not isinstance(kf, tuple):
1193                 kf = (kf, )
1194             for fld in kf:
1195                 rec[fld] = blank[fld]
1196         self.appendDataSet((rec, ))
1197
1198
1199     def getDataTypes(self):
1200         """Returns the internal _types dict."""
1201         return self._types
1202
1203
1204     def _storeData(self, data, typs):
1205         """Accepts a dataset and type dict from an external source and
1206         uses it as its own. Also resets the lastRequeryTime value.
1207         """
1208         # clear mementos and new record flags:
1209         self._mementos = {}
1210         self._newRecords = {}
1211         self.lastRequeryTime = time.time()
1212         # If None is passed as the data, exit after resetting the flags
1213         if data is None:
1214             return
1215         # Store the values
1216         self._records = data
1217         self._types = typs
1218         # Clear the unsorted list, and then apply the current sort
1219         self.__unsortedRows = []
1220         if self.sortColumn:
1221             try:
1222                 self.sort(self.sortColumn, self.sortOrder)
1223             except dException.NoRecordsException:
1224                 # No big deal
1225                 pass
1226
1227
1228     def filter(self, fld, expr, op="="):
1229         """Apply a filter to the current records."""
1230         self._records = self._records.filter(fld=fld, expr=expr, op=op)
1231
1232
1233     def filterByExpression(self, expr):
1234         """Allows you to filter by any valid Python expression."""
1235         self._records = self._records.filterByExpression(expr)
1236
1237
1238     def removeFilter(self):
1239         """Remove the most recently applied filter."""
1240         self._records = self._records.removeFilter()
1241
1242
1243     def removeFilters(self):
1244         """Remove all applied filters, going back to the original data set."""
1245         self._records = self._records.removeFilters()
1246
1247
1248     def replace(self, field, valOrExpr, scope=None):
1249         """Replaces the value of the specified field with the given value
1250         or expression. All records matching the scope are affected; if
1251         no scope is specified, all records are affected.
1252
1253         'valOrExpr' will be treated as a literal value, unless it is prefixed
1254         with an equals sign. All expressions will therefore be a string
1255         beginning with '='. Literals can be of any type.
1256
1257         NOTE: this does NOT work with the memento framework for
1258         determining modified records. It is strongly recommended that
1259         instead of calling this directly that the bizobj.replace() method
1260         be used in any programming.
1261         """
1262         # Make sure that the data set object has any necessary references
1263         self._records.Cursor = self
1264         self._records.Bizobj = self._bizobj
1265         self._records.replace(field, valOrExpr, scope=scope)
1266
1267
1268     def first(self):
1269         """ Move the record pointer to the first record of the data set."""
1270         if self.RowCount > 0:
1271             self.RowNumber = 0
1272         else:
1273             raise dException.NoRecordsException(_("No records in data set"))
1274
1275
1276     def prior(self):
1277         """ Move the record pointer back one position in the recordset."""
1278         if self.RowCount > 0:
1279             if self.RowNumber > 0:
1280                 self.RowNumber -= 1
1281             else:
1282                 raise dException.BeginningOfFileException(
1283                         _("Already at the beginning of the data set."))
1284         else:
1285             raise dException.NoRecordsException(
1286                     _("No records in data set"))
1287
1288
1289     def next(self):
1290         """ Move the record pointer forward one position in the recordset."""
1291         if self.RowCount > 0:
1292             if self.RowNumber < (self.RowCount-1):
1293                 self.RowNumber += 1
1294             else:
1295                 raise dException.EndOfFileException(
1296                         _("Already at the end of the data set."))
1297         else:
1298             raise dException.NoRecordsException(_("No records in data set"))
1299
1300
1301     def last(self):
1302         """ Move the record pointer to the last record in the recordset."""
1303         if self.RowCount > 0:
1304             self.RowNumber = self.RowCount-1
1305         else:
1306             raise dException.NoRecordsException(_("No records in data set"))
1307
1308
1309     def save(self, allRows=False, includeNewUnchanged=False):
1310         """ Save any changes to the current record back to the data store."""
1311         # Make sure that there is data to save
1312         if self.RowCount <= 0:
1313             raise dException.NoRecordsException(_("No data to save"))
1314         # Make sure that there is a PK
1315         self.checkPK()
1316
1317         def saverow(row):
1318             try:
1319                 self.__saverow(row)
1320             except dException.DBQueryException, e:
1321                 # Error was encountered. Raise an exception so that the
1322                 # calling bizobj can rollback the transaction if necessary
1323                 try:
1324                     errMsg = ustr(e).decode(self.Encoding)
1325                 except UnicodeError:
1326                     errMsg = unicode(e)
1327                 dabo.dbActivityLog.write(
1328                         _("DBQueryException encountered in save(): %s") % errMsg)
1329                 raise e
1330             except StandardError, e:
1331                 errMsg = ustr(e)
1332                 if "connect" in errMsg.lower():
1333                     dabo.dbActivityLog.write(
1334                             _("Connection Lost exception encountered in saverow(): %s") % errMsg)
1335                     raise dException.ConnectionLostException(errMsg)
1336                 else:
1337                     # Error was encountered. Raise an exception so that the
1338                     # calling bizobj can rollback the transaction if necessary
1339                     raise
1340
1341         self._syncAuxProperties()
1342
1343         if allRows:
1344             # This branch doesn't happen when called from dBizobj (not sure if
1345             # we really need the allRows arg at all).
1346             rows = self.getChangedRows(includeNewUnchanged=includeNewUnchanged)
1347         else:
1348             # This branch results in redundant isChanged() call when called from
1349             # dBizobj.saveAll(), but it needs to be here because dBizobj.save()
1350             # doesn't check it.
1351             rows = []
1352             if self.isChanged(allRows=False, includeNewUnchanged=includeNewUnchanged):
1353                 rows = [self.RowNumber]
1354         for row in rows:
1355             saverow(row)
1356
1357
1358     def __saverow(self, row):
1359         rec = self._records[row]
1360         recKey = self.pkExpression(rec)
1361         newrec = kons.CURSOR_TMPKEY_FIELD in rec
1362
1363         newPKVal = None
1364         if newrec and self.AutoPopulatePK:
1365             # Some backends do not provide a means to retrieve
1366             # auto-generated PKs; for those, we need to create the
1367             # PK before inserting the record so that we can pass it on
1368             # to any linked child records. NOTE: if you are using
1369             # compound PKs, this cannot be done.
1370             newPKVal = self.pregenPK()
1371             if newPKVal and not self._compoundKey:
1372                 self.setFieldVal(self.KeyField, newPKVal, row)
1373
1374         if newrec:
1375             diff = self._getNewRecordDiff(row)
1376         else:
1377             diff = self.getRecordStatus(row)
1378         aq = self.AutoQuoteNames
1379         if diff:
1380             if newrec:
1381                 flds = ""
1382                 vals = []
1383                 kf = self.KeyField
1384                 for kk, vv in diff.items():
1385                     if self.AutoPopulatePK:
1386                         if self._compoundKey:
1387                             skipIt = (kk in kf)
1388                         else:
1389                             # Skip the key field, unless we pre-generated its value above.
1390                             skipIt = (kk == self.KeyField) and not newPKVal
1391                         if skipIt:
1392                             # we don't want to include the PK in the insert
1393                             continue
1394                     if kk in self.getNonUpdateFields():
1395                         # Skip it.
1396                         continue
1397                     if self._nullDefaults and vv == (None, None):
1398                         # Skip these, too
1399                         continue
1400                     # Append the field and its value.
1401                     flds += ", " + self.BackendObject.encloseNames(kk, aq)
1402                     # add value to expression
1403                     fieldType = [ds[1] for ds in self.DataStructure if ds[0] == kk][0]
1404                     vals.append(vv[1])
1405
1406                 # Trim leading comma-space from the 'flds' string
1407                 flds = flds[2:]
1408                 if not flds:
1409                     # Some backends (sqlite) require non-empty field clauses. We already
1410                     # know that we are expecting the backend to generate the PK, so send
1411                     # NULL as the PK Value:
1412                     flds = self.KeyField
1413                     vals = "NULL"
1414                 nms = self.BackendObject.encloseNames(self.Table, aq)
1415                 placeHolders = len(vals) * [self.ParamPlaceholder]
1416                 sql = "insert into %s (%s) values (%s) " % (nms, flds, ",".join(placeHolders))
1417                 params = tuple(vals)
1418             else:
1419                 pkWhere = self.makePkWhere(row)
1420                 updClause, params = self.makeUpdClause(diff)
1421                 sql = "update %s set %s where %s" % (self.BackendObject.encloseNames(self.Table, aq),
1422                         updClause, pkWhere)
1423             #run the update
1424             aux = self.AuxCursor
1425             res = aux.execute(sql, params)
1426
1427             if newrec and self.AutoPopulatePK and (newPKVal is None):
1428                 # Call the database backend-specific code to retrieve the
1429                 # most recently generated PK value.
1430                 newPKVal = aux.getLastInsertID()
1431                 if newPKVal and not self._compoundKey:
1432                     self.setFieldVal(self.KeyField, newPKVal, row)
1433
1434             if newrec and self._nullDefaults:
1435                 # We need to retrieve any new default values
1436                 aux = self.AuxCursor
1437                 if not isinstance(self.KeyField, tuple):
1438                     keyFIelds = [self.KeyField]
1439                 else:
1440                     keyFIelds = self.KeyField
1441                 wheres = []
1442                 for kf in keyFIelds:
1443                     fld = self.BackendObject.encloseNames(kf, self.AutoQuoteNames)
1444                     val = self.getFieldVal(kf)
1445                     if isinstance(val, basestring):
1446                         val = "'" + val.encode(self.Encoding) + "' "
1447                     elif isinstance(val, (datetime.date, datetime.datetime)):
1448                         val = self.formatDateTime(val)
1449                     else:
1450                         val = ustr(val)
1451                     wheres.append("%s = %s" % (fld, val))
1452                 where = " and ".join(wheres)
1453                 aux.execute("select * from %s where %s" % (self.Table, where))
1454                 try:
1455                     data = aux.getDataSet()[0]
1456                     for fld, val in data.items():
1457                         try:
1458                             self.setFieldVal(fld, val)
1459                         except dException.FieldNotFoundException:
1460                             # Field is not in the dataset
1461                             pass
1462                 except IndexError:
1463                     # For some reason we could not retrieve the matching PK record
1464                     pass
1465
1466             self._clearMemento(row)
1467             if newrec:
1468                 self._clearNewRecord(row=row, pkVal=recKey)
1469             else:
1470                 if not res:
1471                     # Different backends may cause res to be None
1472                     # even if the save is successful.
1473                     self.BackendObject.noResultsOnSave()
1474
1475
1476     def _clearMemento(self, row=None):
1477         """Erase the memento for the passed row, or current row if none passed."""
1478         if row is None:
1479             row = self.RowNumber
1480         rec = self._records[row]
1481
1482         try:
1483             pk = self.getPK(row)
1484             del self._mementos[pk]
1485         except KeyError:
1486             # didn't exist
1487             pass
1488
1489
1490     def _clearNewRecord(self, row=None, pkVal=None):
1491         """Erase the new record flag for the passed row, or current row if none passed."""
1492         # If pkVal passed, delete that reference:
1493         if pkVal is not None:
1494             try:
1495                 del self._newRecords[pkVal]
1496                 if row is None:
1497                     # We deleted based on pk, don't delete flag for the current row.
1498                     return
1499             except KeyError:
1500                 pass
1501
1502         if row is None:
1503             row = self.RowNumber
1504         rec = self._records[row]
1505
1506         try:
1507             pk = self.getPK(row)
1508             del self._newRecords[pk]
1509         except KeyError:
1510             # didn't exist
1511             pass
1512         # Remove the temp key field column, if still present.
1513         rec.pop(kons.CURSOR_TMPKEY_FIELD, None)
1514
1515
1516
1517     def getDataDiff(self, allRows=False):
1518         """Create a compact representation of all the modified records
1519         for this cursor.
1520         """
1521         diff = []
1522         def rowDiff(pk):
1523             newrec = pk in self._newRecords
1524             if newrec:
1525                 ret = self._getNewRecordDiff(pk=pk)
1526             else:
1527                 ret = self.getRecordStatus(pk=pk)
1528             ret[self._keyField] = pk
1529             ret[kons.CURSOR_TMPKEY_FIELD] = newrec
1530             return ret
1531
1532         if allRows:
1533             for pk in self._mementos.keys():
1534                 diff.append(rowDiff(pk))
1535         else:
1536             pk = self.getPK()
1537             if pk in self._mementos:
1538                 diff.append(rowDiff(pk))
1539         return diff
1540
1541
1542     def pregenPK(self):
1543         """Various backend databases require that you manually
1544         generate new PKs if you need to refer to their values afterward.
1545         This method will call the backend to generate and
1546         retrieve a new PK if the backend supports this. We use the
1547         auxiliary cursor so as not to alter the current data.
1548         """
1549         return self.BackendObject.pregenPK(self.AuxCursor)
1550
1551
1552     def _getBlankRecord(self):
1553         """Returns a record template, with each field set to the 'blank' value
1554         for that data type.
1555         """
1556         if not self._blank:
1557             self.__setStructure()
1558         return self._blank.copy()
1559
1560
1561     def new(self):
1562         """Add a new record to the data set."""
1563         blank = self._getBlankRecord()
1564         self._records = dDataSet(self._records + (blank,))
1565         # Adjust the RowCount and position
1566         self.RowNumber = self.RowCount - 1
1567
1568
1569     def cancel(self, allRows=False, ignoreNoRecords=None):
1570         """ Revert any changes to the data set back to the original values."""
1571         if ignoreNoRecords is None:
1572             ignoreNoRecords = True
1573         if self.RowCount == 0:
1574             if ignoreNoRecords:
1575                 # Nothing to do!
1576                 return
1577             else:
1578                 raise dException.NoRecordsException(_("No data to cancel."))
1579
1580         # Faster to deal with 2 specific cases: all rows or just current row
1581         if allRows:
1582             try:
1583                 recs = self._records.UnfilteredDataSet
1584             except AttributeError:
1585                 # Not a dDataSet
1586                 recs = self._records
1587
1588             if self._newRecords:
1589                 recs = list(recs)
1590                 delrecs_ids = self._newRecords.keys()
1591                 delrecs_idx = []
1592                 for rec_id in delrecs_ids:
1593                     # Remove any memento associated with the canceled new record, and
1594                     # append to the list of indexes to delete.
1595                     row, rec = self._getRecordByPk(rec_id)
1596                     self._clearMemento(row)
1597                     delrecs_idx.append(self._records._index(rec))
1598                 delrecs_idx.sort(reverse=True)
1599                 for idx in delrecs_idx:
1600                     del recs[idx]
1601                 self._newRecords = {}
1602                 self._records = dDataSet(recs)
1603                 if self.RowNumber >= self.RowCount:
1604                     self.RowNumber = self.RowCount - 1
1605
1606             for rec_pk, mem in self._mementos.items():
1607                 row, rec = self._getRecordByPk(rec_pk)
1608                 for fld, val in mem.items():
1609                     self._records[row][fld] = val
1610             self._mementos = {}
1611
1612         else:
1613             row = self.RowNumber
1614             rec = self._records[row]
1615             recKey = self.pkExpression(rec)
1616             if recKey in self._newRecords:
1617                 # We simply need to remove the row, and clear the memento and newrec flag.
1618                 self._clearMemento(row)
1619                 self._clearNewRecord(row)
1620                 recs = list(self._records)
1621                 del recs[recs.index(rec)]
1622                 self._records = dDataSet(recs)
1623                 if self.RowNumber >= self.RowCount:
1624                     self.RowNumber = self.RowCount - 1
1625                 return
1626
1627             # Not a new record: need to manually replace the old values:
1628             for fld, val in self._mementos.get(recKey, {}).items():
1629                 self._records[row][fld] = val
1630             self._clearMemento(row)
1631
1632
1633     def delete(self, delRowNum=None):
1634         """Delete the specified row, or the currently active row."""
1635         if self.RowNumber < 0 or self.RowCount == 0:
1636             # No query has been run yet
1637             raise dException.NoRecordsException(_("No record to delete"))
1638         if delRowNum is None:
1639             # assume that it is the current row that is to be deleted
1640             delRowNum = self.RowNumber
1641
1642         rec = self._records[delRowNum]
1643         pk = self.pkExpression(rec)
1644         if pk in self._newRecords:
1645             res = True
1646             del self._newRecords[pk]
1647         else:
1648             pkWhere = self.makePkWhere()
1649             # some backends(PostgreSQL) don't return information about number of deleted rows
1650             # try to fetch it before
1651             sql = "select count(*) as cnt from %s where %s" % (self.Table, pkWhere)
1652             aux = self.AuxCursor
1653             aux.execute(sql)
1654             res = aux.getFieldVal('cnt')
1655             if res:
1656                 sql = "delete from %s where %s" % (self.Table, pkWhere)
1657                 aux.execute(sql)
1658
1659         if not res:
1660             # Nothing was deleted
1661             self.BackendObject.noResultsOnDelete()
1662         else:
1663             # Delete the record from the current dataset
1664             if pk in self._mementos:
1665                 del self._mementos[pk]
1666             self._removeRow(delRowNum)
1667
1668
1669     def _removeRow(self, row):
1670         ## Since record sets are tuples and thus immutable, we need to do this
1671         ## little dance to remove a row.
1672         lRec = list(self._records)
1673         del lRec[row]
1674         self._records = dDataSet(lRec)
1675         self.RowNumber = min(self.RowNumber, self.RowCount-1)
1676
1677
1678     def flush(self):
1679         """ Some backends need to be prompted to flush changes
1680         to disk even without starting a transaction. This is the method
1681         to call to accomplish this.
1682         """
1683         self.BackendObject.flush(self)
1684
1685
1686     def setDefaults(self, vals):
1687         """Set the default field values for newly added records. The
1688         'vals' parameter is a dictionary of fields and their default values.
1689         If vals is None, the defaults for all but the KeyField will be set to
1690         None, and their values will not be included in the insert statement
1691         when saved unless the user changes them to some non-null
1692         value.
1693         """
1694         rec = self._records[self.RowNumber]
1695         keyField = self.KeyField
1696         keyFieldSet = False
1697         self._nullDefaults = (vals is None)
1698
1699         def setDefault(field, val):
1700             if field in rec:
1701                 # If it is a function, execute it to get the value, else use literal.
1702                 if callable(val):
1703                     val = val()
1704                 elif isinstance(val, tuple) and val and callable(val[0]):
1705                     # This is a tuple consisting of a function and zero to many parameters
1706                     fnc = val[0]
1707                     prms = val[1:]
1708                     val = fnc(*prms)
1709                 self.setFieldVal(field, val)
1710             else:
1711                 raise dException.FieldNotFoundException(
1712                         _("Can't set default value for nonexistent field '%s'.") % field)
1713
1714         if self._nullDefaults:
1715             for field in rec.keys():
1716                 if field == keyField:
1717                     continue
1718                 self.setFieldVal(field, None)
1719         else:
1720             if keyField in vals.keys():
1721                 # Must set the pk default value first, for mementos to be filled in
1722                 # correctly.
1723                 setDefault(keyField, vals[keyField])
1724                 keyFieldSet = True
1725
1726             for field, val in vals.items():
1727                 if field == keyField and keyFieldSet:
1728                     continue
1729                 setDefault(field, val)
1730
1731
1732     def __setStructure(self):
1733         """Set the structure of a newly-added record."""
1734         for field in self.DataStructure:
1735             field_alias = field[0]
1736             field_type = field[1]
1737             field_ispk = field[2]
1738             table_name = field[3]
1739             field_name = field[4]
1740             field_scale = field[5]
1741
1742             typ = dabo.db.getPythonType(field_type)
1743             # Handle the non-standard cases
1744             if typ is Decimal:
1745                 newval = Decimal()
1746                 # If the backend reports a decimal scale, use it. Scale refers to the
1747                 # number of decimal places.
1748                 scale = field_scale
1749                 if scale is None:
1750                     scale = 2
1751                 ex = "0.%s" % ("0"*scale)
1752                 newval = newval.quantize(Decimal(ex))
1753             elif typ is datetime.datetime:
1754                 newval = datetime.datetime.min
1755             elif typ is datetime.date:
1756                 newval = datetime.date.min
1757             elif typ is None:
1758                 newval = None
1759             else:
1760                 try:
1761                     newval = typ()
1762                 except Exception, e:
1763                     dabo.errorLog.write(_("Failed to create newval for field '%s'") % field_alias)
1764                     dabo.errorLog.write(ustr(e))
1765                     newval = u""
1766
1767             self._blank[field_alias] = newval
1768
1769         # Mark the calculated and derived fields.
1770         self.__setNonUpdateFields()
1771
1772
1773     def getChangedRows(self, includeNewUnchanged=False):
1774         """Returns a list of rows with changes."""
1775         chKeys = self._mementos.keys()
1776         if includeNewUnchanged:
1777             # We need to also count all new records
1778             chKeys = dict.fromkeys(chKeys + self._newRecords.keys()).keys()
1779         return map(self._getRowByPk, chKeys)
1780
1781
1782     def _getRecordByPk(self, pk, raiseRowNotFound=True):
1783         """Find the record with the passed primary key; return (row, record)."""
1784         kf = self.KeyField
1785         if kf:
1786             for idx, rec in enumerate(self._records):
1787                 key = self.getFieldVal(kf, row=idx)
1788                 if key == pk:
1789                     return (idx, rec)
1790         if raiseRowNotFound:
1791             tbl, rc = self.Table, self.RowCount
1792             raise dException.RowNotFoundException(_("PK '%(pk)s' not found in table '%(tbl)s' (RowCount: %(rc)s)") % locals())
1793         return (None, None)
1794
1795
1796     def _getRowByPk(self, pk):
1797         """Find the record with the passed primary key value; return row number."""
1798         row, rec = self._getRecordByPk(pk)
1799         return row
1800
1801
1802     def hasPK(self, pk):
1803         """Return True if the passed pk is present in the dataset."""
1804         kf = self.KeyField
1805         return bool([v[kf] for v in self._records if v[kf] == pk])
1806
1807
1808     def moveToPK(self, pk):
1809         """ Find the record with the passed primary key, and make it active.
1810
1811         If the record is not found, the position is set to the first record.
1812         """
1813         row, rec = self._getRecordByPk(pk, raiseRowNotFound=False)
1814         if row is None:
1815             row = 0
1816         self.RowNumber = row
1817
1818
1819     def moveToRowNum(self, rownum):
1820         """ Move the record pointer to the specified row number.
1821
1822         If the specified row does not exist, the pointer remains where it is,
1823         and an exception is raised.
1824         """
1825         if (rownum >= self.RowCount) or (rownum < 0):
1826             rc = self.RowCount
1827             raise dException.dException(_("Invalid row specified: %(rownum)s. RowCount=%(rc)s") % locals())
1828         self.RowNumber = rownum
1829
1830
1831     def seek(self, val, fld=None, caseSensitive=True, near=False, movePointer=True):
1832         """ Find the first row where the field value matches the passed value.
1833
1834         Returns the row number of the first record that matches the passed
1835         value in the designated field, or -1 if there is no match. If 'near' is
1836         True, a match will happen on the row whose value is the greatest value
1837         that is less than the passed value. If 'caseSensitive' is set to False,
1838         string comparisons are done in a case-insensitive fashion.
1839         """
1840         ret = -1
1841         if fld is None:
1842             # Default to the current sort order field
1843             fld = self.sortColumn
1844         if self.RowCount <= 0:
1845             # Nothing to seek within
1846             return ret
1847         # Make sure that this is a valid field
1848         if not fld:
1849             raise dException.FieldNotFoundException(_("No field specified for seek()"))
1850
1851         simpleKey = ("," not in fld)
1852         if simpleKey:
1853             flds = [fld]
1854         else:
1855             flds = [f.strip() for f in fld.split(",")]
1856         badflds = []
1857         for fldname in flds:
1858             if (fldname not in self._records[0]) and (fldname not in self.VirtualFields):
1859                 badflds.append(fldname)
1860         if badflds:
1861             raise dException.FieldNotFoundException(_("Non-existent field(s) '%s'") % ", ".join(badflds))
1862
1863         # Copy the specified field vals and their row numbers to a list, and
1864         # add those lists to the sort list
1865         sortList = []
1866         for row in xrange(0, self.RowCount):
1867             if simpleKey:
1868                 rowval = self.getFieldVal(fld, row=row)
1869             else:
1870                 rowval = tuple([self.getFieldVal(f, row=row) for f in flds])
1871             sortList.append([rowval, row])
1872
1873         if simpleKey:
1874             # Determine if we are seeking string values
1875             field_type = self._types.get(fld, type(sortList[0][0]))
1876             compString = issubclass(field_type, basestring)
1877         else:
1878             compString = False
1879
1880         if simpleKey and not compString:
1881             # coerce val to be the same type as the field type
1882             if issubclass(field_type, int):
1883                 try:
1884                     val = int(val)
1885                 except ValueError:
1886                     val = int(0)
1887
1888             elif issubclass(field_type, long):
1889                 try:
1890                     val = long(val)
1891                 except ValueError:
1892                     val = long(0)
1893
1894             elif issubclass(field_type, float):
1895                 try:
1896                     val = float(val)
1897                 except ValueError:
1898                     val = float(0)
1899
1900         if compString and not caseSensitive:
1901             sortList.sort(key=caseInsensitiveSortKey)
1902         else:
1903             sortList.sort()
1904
1905         if compString and not caseSensitive:
1906             # Change all of the first elements to lower case
1907             def safeLower(val):
1908                 try:
1909                     return val.lower()
1910                 except AttributeError:
1911                     return val
1912             searchList = [safeLower(first) for first, second in sortList]
1913             try:
1914                 matchVal = val.lower()
1915             except AttributeError:
1916                 # this is a string colum, but seeking a null value.
1917                 matchVal = val
1918         else:
1919             matchVal = val
1920             searchList = [first for first, second in sortList]
1921
1922         # See if we have an exact match before we look for 'near' values
1923         try:
1924             idx = searchList.index(matchVal)
1925             ret = sortList[idx][1]
1926         except ValueError:
1927             if near:
1928                 # Find the first row greater than the match value
1929                 numSmaller = len([testVal for testVal in searchList
1930                         if testVal < matchVal])
1931                 try:
1932                     ret = sortList[numSmaller][1]
1933                 except IndexError:
1934                     ret = 0
1935         if movePointer and ret > -1:
1936             # Move the record pointer
1937             self.RowNumber = ret
1938         return ret
1939
1940
1941     def checkPK(self):
1942         """ Verify that the field(s) specified in the KeyField prop exist."""
1943         # First, make sure that there is *something* in the field
1944         kf = self.KeyField
1945         if not kf:
1946             raise dException.MissingPKException(
1947                     _("checkPK failed; no primary key specified"))
1948
1949         if isinstance(kf, basestring):
1950             kf = [kf]
1951         # Make sure that there is a field with that name in the data set
1952         try:
1953             for fld in kf:
1954                 self._records[0][fld]
1955         except KeyError:
1956             raise dException.MissingPKException(
1957                     _("Primary key field does not exist in the data set: ") + fld)
1958
1959
1960     def makePkWhere(self, row=None):
1961         """ Create the WHERE clause used for updates, based on the pk field.
1962
1963         Optionally pass in a row number, otherwise use the current record.
1964         """
1965         if not self.KeyField:
1966             # Cannot update without a KeyField
1967             return "1 = 0"
1968         bo = self.BackendObject
1969         tblPrefix = bo.getWhereTablePrefix(self.Table,
1970                     autoQuote=self.AutoQuoteNames)
1971         if not row:
1972             row = self.RowNumber
1973         rec = self._records[row]
1974
1975         if self._compoundKey:
1976             keyFields = [fld for fld in self.KeyField]
1977         else:
1978             keyFields = [self.KeyField]
1979         recKey = self.pkExpression(rec)
1980         mem = self._mementos.get(recKey, {})
1981
1982         def getPkVal(fld):
1983             try:
1984                 return mem[fld]
1985             except KeyError:
1986                 return rec[fld]
1987
1988         ret = []
1989         for fld in keyFields:
1990             fldSafe = bo.encloseNames(fld, self.AutoQuoteNames)
1991             if ret:
1992                 ret.append(" AND ")
1993             pkVal = getPkVal(fld)
1994             if isinstance(pkVal, basestring):
1995                 ret.extend([tblPrefix, fldSafe, "='", pkVal.encode(self.Encoding), "' "])
1996             elif isinstance(pkVal, (datetime.date, datetime.datetime)):
1997                 ret.extend([tblPrefix, fldSafe, "=", self.formatDateTime(pkVal), " "])
1998             else:
1999                 ret.extend([tblPrefix, fldSafe, "=", ustr(pkVal), " "])
2000         return "".join(ret)
2001
2002
2003     def makeUpdClause(self, diff):
2004         """Create the 'set field=val' section of the Update statement. Return a 2-tuple
2005         containing the sql portion as the first element, and the parameters for the
2006         values as the second.
2007         """
2008         retSql = []
2009         retParams = []
2010         bo = self.BackendObject
2011         aq = self.AutoQuoteNames
2012         tblPrefix = bo.getUpdateTablePrefix(self.Table, autoQuote=aq)
2013         nonup = self.getNonUpdateFields()
2014         for fld, val in diff.items():
2015             old_val, new_val = val
2016             # Skip the fields that are not to be updated.
2017             if fld in nonup:
2018                 continue
2019             fieldType = [ds[1] for ds in self.DataStructure if ds[0] == fld][0]
2020             nms = bo.encloseNames(fld, aq)
2021             retSql.append("%s%s = %s" % (tblPrefix, nms, self.ParamPlaceholder))
2022             #thisVal =self.formatForQuery(new_val, fieldType)
2023             retParams.append(new_val)
2024         return (", ".join(retSql), tuple(retParams))
2025
2026
2027     def processFields(self, txt):
2028         return self.BackendObject.processFields(txt)
2029
2030
2031     def escQuote(self, val):
2032         """ Escape special characters in SQL strings. """
2033         ret = val
2034         if isinstance(val, basestring):
2035             ret = self.BackendObject.escQuote(val)
2036         return ret
2037
2038
2039     def getTables(self, includeSystemTables=False):
2040         """ Return a tuple of tables in the current database."""
2041         return self.BackendObject.getTables(self.AuxCursor, includeSystemTables)
2042
2043
2044     def getTableRecordCount(self, tableName):
2045         """ Get the number of records in the backend table."""
2046         return self.BackendObject.getTableRecordCount(tableName, self.AuxCursor)
2047
2048
2049     def getFields(self, tableName=None):
2050         """ Get field information about the backend table.
2051
2052         Returns a list of 3-tuples, where the 3-tuple's elements are:
2053             0: the field name (string)
2054             1: the field type ('I', 'N', 'C', 'M', 'B', 'D', 'T')
2055             2: boolean specifying whether this is a pk field.
2056         """
2057         if tableName is None:
2058             # Use the default
2059             tableName = self.Table
2060         key = "%s:::%s" % (tableName, self.CurrentSQL)
2061         try:
2062             return self._fieldStructure[key]
2063         except KeyError:
2064             flds = self.BackendObject.getFields(tableName, self.AuxCursor)
2065             self._fieldStructure[key] = flds
2066             return flds
2067
2068
2069     def getFieldInfoFromDescription(self):
2070         """ Get field information from the cursor description.
2071
2072         Returns a tuple of 3-tuples, where the 3-tuple's elements are:
2073             0: the field name (string)
2074             1: the field type ('I', 'N', 'C', 'M', 'B', 'D', 'T'), or None.
2075             2: boolean specifying whether this is a pk field, or None.
2076         """
2077         return self.BackendObject.getFieldInfoFromDescription(self.descriptionClean)
2078
2079
2080     def getLastInsertID(self):
2081         """ Return the most recently generated PK """
2082         ret = None
2083         if self.BackendObject:
2084             ret = self.BackendObject.getLastInsertID(self)
2085         return ret
2086
2087
2088     def formatForQuery(self, val, fieldType=None):
2089         """ Format any value for the backend """
2090         ret = val
2091         if self.BackendObject:
2092             ret = self.BackendObject.formatForQuery(val, fieldType)
2093         return ret
2094
2095
2096     def formatDateTime(self, val):
2097         """ Format DateTime values for the backend """
2098         ret = val
2099         if self.BackendObject:
2100             ret = self.BackendObject.formatDateTime(val)
2101         return ret
2102
2103
2104     def formatNone(self):
2105         """ Format None values for the backend """
2106         if self.BackendObject:
2107             return self.BackendObject.formatNone()
2108
2109
2110     def beginTransaction(self):
2111         """ Begin a SQL transaction."""
2112         ret = None
2113         if self.BackendObject:
2114             ret = self.BackendObject.beginTransaction(self.AuxCursor)
2115         return ret
2116
2117
2118     def commitTransaction(self):
2119         """ Commit a SQL transaction."""
2120         ret = None
2121         if self.BackendObject:
2122             ret = self.BackendObject.commitTransaction(self.AuxCursor)
2123         return ret
2124
2125
2126     def rollbackTransaction(self):
2127         """ Roll back (revert) a SQL transaction."""
2128         ret = None
2129         if self.BackendObject:
2130             ret = self.BackendObject.rollbackTransaction(self.AuxCursor)
2131         return ret
2132
2133
2134     def createTable(self, tabledef):
2135         """Create a table based on the table definition."""
2136         self.BackendObject.createJustTable(tabledef, self)
2137
2138
2139     def createIndexes(self, tabledef):
2140         """Create indexes based on the table definition."""
2141         self.BackendObject.createJustIndexes(tabledef, self)
2142
2143
2144     def createTableAndIndexes(self, tabledef):
2145         """Create a table and its indexes based on the table definition."""
2146         self.BackendObject.createTableAndIndexes(tabledef, self)
2147
2148
2149     ###     SQL Builder methods     ########
2150     def getFieldClause(self):
2151         """ Get the field clause of the sql statement."""
2152         return self.sqlManager._fieldClause
2153
2154
2155     def setFieldClause(self, clause):
2156         """ Set the field clause of the sql statement."""
2157         self.sqlManager._fieldClause = self.sqlManager.BackendObject.setFieldClause(clause)
2158
2159
2160     def addField(self, exp, alias=None):
2161         """ Add a field to the field clause."""
2162         sm = self.sqlManager
2163         beo = sm.BackendObject
2164         if beo:
2165             sm._fieldClause = beo.addField(sm._fieldClause, exp, alias,
2166                     autoQuote=self.AutoQuoteNames)
2167         return sm._fieldClause
2168
2169
2170     def getFromClause(self):
2171         """ Get the from clause of the sql statement."""
2172         return self.sqlManager._fromClause
2173
2174
2175     def setFromClause(self, clause):
2176         """ Set the from clause of the sql statement."""
2177         self.sqlManager._fromClause = self.sqlManager.BackendObject.setFromClause(clause,
2178                     autoQuote=self.AutoQuoteNames)
2179
2180
2181     def addFrom(self, exp, alias=None):
2182         """ Add a table to the sql statement. For joins, use
2183         the addJoin() method.
2184         """
2185         if self.sqlManager.BackendObject:
2186             self.sqlManager._fromClause = self.sqlManager.BackendObject.addFrom(self.sqlManager._fromClause,
2187                     exp, alias, autoQuote=self.AutoQuoteNames)
2188         return self.sqlManager._fromClause
2189
2190
2191     def getJoinClause(self):
2192         """ Get the join clause of the sql statement."""
2193         return self.sqlManager._joinClause
2194
2195
2196     def setJoinClause(self, clause):
2197         """ Set the join clause of the sql statement."""
2198         self.sqlManager._joinClause = self.sqlManager.BackendObject.setJoinClause(clause,
2199                     autoQuote=self.AutoQuoteNames)
2200
2201
2202     def addJoin(self, tbl, joinCondition, joinType=None):
2203         """ Add a joined table to the sql statement."""
2204         if self.sqlManager.BackendObject:
2205             self.sqlManager._joinClause = self.sqlManager.BackendObject.addJoin(tbl,
2206                     joinCondition, self.sqlManager._joinClause, joinType,
2207                     autoQuote=self.AutoQuoteNames)
2208         return self.sqlManager._joinClause
2209
2210
2211
2212     def getWhereClause(self):
2213         """ Get the where clause of the sql statement."""
2214         return self.sqlManager._whereClause
2215
2216
2217     def setWhereClause(self, clause):
2218         """ Set the where clause of the sql statement."""
2219         self.sqlManager._whereClause = self.sqlManager.BackendObject.setWhereClause(clause,
2220                     autoQuote=self.AutoQuoteNames)
2221
2222
2223     def addWhere(self, exp, comp="and"):
2224         """ Add an expression to the where clause."""
2225         if self.sqlManager.BackendObject:
2226             self.sqlManager._whereClause = self.sqlManager.BackendObject.addWhere(
2227                     self.sqlManager._whereClause, exp, comp, autoQuote=self.AutoQuoteNames)
2228         return self.sqlManager._whereClause
2229
2230
2231     def prepareWhere(self, clause):
2232         """ Modifies WHERE clauses as needed for each backend. """
2233         return self.sqlManager.BackendObject.prepareWhere(clause,
2234                     autoQuote=self.AutoQuoteNames)
2235
2236
2237     def setChildFilter(self, fld):
2238         """ This method sets the appropriate WHERE filter for dependent child queries."""
2239
2240         def getTableAlias(fromClause):
2241             if not fromClause.strip():
2242                 return None
2243
2244             joinStrings = ["left join", "right join", "outer join", "inner join", "join"]
2245             foundAlias = None
2246             for joinString in joinStrings:
2247                 at = fromClause.lower().find(joinString)
2248                 if at >= 0:
2249                     foundAlias = fromClause[:at].strip()
2250                     break
2251             if not foundAlias:
2252                 # The alias is the last 'word' in the FROM clause
2253                 foundAlias = fromClause.strip().split()[-1]
2254             return foundAlias
2255
2256         alias = getTableAlias(self.sqlManager._fromClause)
2257         if not alias:
2258             # Use the old way (pre 2180) of using the Table (DataSource) property.
2259             alias = self.Table
2260         filtExpr = " %s.%s = %s " % (alias, fld, self.ParamPlaceholder)
2261         self.setChildFilterClause(filtExpr)
2262
2263
2264     def setNonMatchChildFilterClause(self):
2265         """ Called when the parent has no records, which implies that the child
2266         cannot have any, either.
2267         """
2268         self.setChildFilterClause(" 1 = 0 ")
2269
2270
2271     def getChildFilterClause(self):
2272         """ Get the child filter part of the sql statement."""
2273         return self.sqlManager._childFilterClause
2274
2275
2276     def setChildFilterClause(self, clause):
2277         """ Set the child filter clause of the sql statement."""
2278         self.sqlManager._childFilterClause = self.sqlManager.BackendObject.setChildFilterClause(clause)
2279
2280
2281     def getGroupByClause(self):
2282         """ Get the group-by clause of the sql statement."""
2283         return self.sqlManager._groupByClause
2284
2285
2286     def setGroupByClause(self, clause):
2287         """ Set the group-by clause of the sql statement."""
2288         self.sqlManager._groupByClause = self.sqlManager.BackendObject.setGroupByClause(clause)
2289
2290
2291     def addGroupBy(self, exp):
2292         """ Add an expression to the group-by clause."""
2293         if self.sqlManager.BackendObject:
2294             self.sqlManager._groupByClause = self.sqlManager.BackendObject.addGroupBy(self.sqlManager._groupByClause,
2295                     exp, autoQuote=self.AutoQuoteNames)
2296         return self.sqlManager._groupByClause
2297
2298
2299     def getOrderByClause(self):
2300         """ Get the order-by clause of the sql statement."""
2301         return self.sqlManager._orderByClause
2302
2303
2304     def setOrderByClause(self, clause):
2305         """ Set the order-by clause of the sql statement."""
2306         self.sqlManager._orderByClause = self.sqlManager.BackendObject.setOrderByClause(clause)
2307
2308
2309     def addOrderBy(self, exp):
2310         """ Add an expression to the order-by clause."""
2311         if self.sqlManager.BackendObject:
2312             self.sqlManager._orderByClause = self.sqlManager.BackendObject.addOrderBy(self.sqlManager._orderByClause,
2313                     exp, autoQuote=self.AutoQuoteNames)
2314         return self.sqlManager._orderByClause
2315
2316
2317     def getLimitClause(self):
2318         """ Get the limit clause of the sql statement."""
2319         return self.sqlManager._limitClause
2320
2321
2322     def setLimitClause(self, clause):
2323         """ Set the limit clause of the sql statement."""
2324         self.sqlManager._limitClause = clause
2325
2326     # For simplicity's sake, create aliases
2327     setLimit, getLimit = setLimitClause, getLimitClause
2328
2329
2330
2331     def getLimitWord(self):
2332         """ Return the word to use in the db-specific limit clause."""
2333         ret = "limit"
2334         if self.sqlManager.BackendObject:
2335             ret = self.sqlManager.BackendObject.getLimitWord()
2336         return ret
2337
2338
2339     def getLimitPosition(self):
2340         """ Return the position to place the limit clause.
2341
2342         For currently-supported dbapi's, the return values of 'top' or 'bottom'
2343         are sufficient.
2344         """
2345         ret = "bottom"
2346         if self.sqlManager.BackendObject:
2347             ret = self.sqlManager.BackendObject.getLimitPosition()
2348         return ret
2349
2350
2351     def getSQL(self):
2352         """ Get the complete SQL statement from all the parts."""
2353         fieldClause = self.sqlManager._fieldClause
2354         fromClause = self.sqlManager._fromClause
2355         joinClause = self.sqlManager._joinClause
2356         whereClause = self.sqlManager._whereClause
2357         childFilterClause = self.sqlManager._childFilterClause
2358         groupByClause = self.sqlManager._groupByClause
2359         orderByClause = self.sqlManager._orderByClause
2360         limitClause = self.sqlManager._limitClause
2361
2362         if not fieldClause:
2363             fieldClause = "*"
2364
2365         if not fromClause:
2366             fromClause = self.Table
2367
2368         if childFilterClause:
2369             # Prepend it to the where clause
2370             if whereClause:
2371                 childFilterClause += "\nand "
2372             whereClause = childFilterClause + " " + whereClause
2373
2374         if fromClause:
2375             fromClause = "  from " + fromClause
2376         else:
2377             fromClause = "  from " + self.sqlManager.Table
2378         if joinClause:
2379             joinClause = " " + joinClause
2380         if whereClause:
2381             whereClause = " where " + whereClause
2382         if groupByClause:
2383             groupByClause = " group by " + groupByClause
2384         if orderByClause:
2385             orderByClause = " order by " + orderByClause
2386         if limitClause:
2387             limitClause = " %s %s" % (self.sqlManager.getLimitWord(), limitClause)
2388         elif limitClause is None:
2389             # The limit clause was specifically disabled.
2390             limitClause = ""
2391         else:
2392             limitClause = " %s %s" % (self.sqlManager.getLimitWord(), self.sqlManager._defaultLimit)
2393
2394         return self.sqlManager.BackendObject.formSQL(fieldClause, fromClause, joinClause,
2395                 whereClause, groupByClause, orderByClause, limitClause)
2396
2397
2398     def getStructureOnlySql(self):
2399         """Creates a SQL statement that will not return any records."""
2400         holdWhere = self.sqlManager._whereClause
2401         self.sqlManager.setWhereClause("")
2402         holdLimit = self.sqlManager._limitClause
2403         self.sqlManager.setLimitClause(1)
2404         ret = self.sqlManager.getSQL()
2405         self.sqlManager.setWhereClause(holdWhere)
2406         self.sqlManager.setLimitClause(holdLimit)
2407         return ret
2408
2409
2410     def executeSQL(self, *args, **kwargs):
2411         self.sqlManager.execute(self.sqlManager.getSQL(), *args, **kwargs)
2412     ###     end - SQL Builder methods     ########
2413
2414
2415     def getWordMatchFormat(self):
2416         return self.sqlManager.BackendObject.getWordMatchFormat()
2417
2418
2419     def oldVal(self, fieldName, row=None):
2420         """Returns the value of the field as it existed after the last requery."""
2421         if self.RowCount < 1:
2422             raise dabo.dException.NoRecordsException
2423         if row is None:
2424             row = self.RowNumber
2425         rec = self._records[row]
2426         pk = self.pkExpression(rec)
2427         mem = self._mementos.get(pk, None)
2428         if mem and (fieldName in mem):
2429             return mem[fieldName]
2430         return self.getFieldVal(fieldName, row)
2431
2432
2433     def _qMarkToParamPlaceholder(self, sql):
2434         """Given SQL with ? placeholders, convert to the placeholder for the current backend.
2435
2436         Allows for all UserSQL to be written with ? as the placeholder.
2437         """
2438         boPlaceholder = self.BackendObject.paramPlaceholder
2439         if boPlaceholder in sql:
2440             # Better not change the sql, because the ? might have a different meaning.
2441             return sql
2442         return sql.replace("?", "%s" % self.BackendObject.paramPlaceholder)
2443
2444
2445     def _setTableForRemote(self, tbl):
2446         """Used when running as a remote application. We don't want to trigger
2447         the methods to query the database for field information.
2448         """
2449         self._table = self.AuxCursor._table = self.sqlManager._table = "%s" % tbl
2450
2451
2452     ## Property getter/setter methods ##
2453     def _getAutoSQL(self):
2454         return self.getSQL()
2455
2456
2457     def _getAutoPopulatePK(self):
2458         try:
2459             return self._autoPopulatePK and bool(self.KeyField)
2460         except AttributeError:
2461             return True
2462
2463     def _setAutoPopulatePK(self, autopop):
2464         self._autoPopulatePK = self.AuxCursor._autoPopulatePK = bool(autopop)
2465
2466
2467     def _getAutoQuoteNames(self):
2468         return self._autoQuoteNames
2469
2470     def _setAutoQuoteNames(self, val):
2471         self._autoQuoteNames = self.AuxCursor._autoQuoteNames = val
2472
2473
2474     def _getAuxCursor(self):
2475         isnew = self.__auxCursor is None
2476         if isnew:
2477             if self._cursorFactoryClass is not None:
2478                 if self._cursorFactoryFunc is not None:
2479                     self.__auxCursor = self._cursorFactoryFunc(self._cursorFactoryClass)
2480         if not self.__auxCursor:
2481             self.__auxCursor = self.BackendObject.getCursor(self.__class__)
2482         self.__auxCursor.BackendObject = self.BackendObject
2483         self.__auxCursor._isAuxiliary = True
2484         if isnew:
2485             ac = self.__auxCursor
2486             ac._autoPopulatePK = self._autoPopulatePK
2487             ac._autoQuoteNames = self._autoQuoteNames
2488             ac._dataStructure = self._dataStructure
2489             if self.BackendObject:
2490                 ac._encoding = self.Encoding
2491             ac._isPrefCursor = self._isPrefCursor
2492             ac._keyField = self._keyField
2493             ac._table = self._table
2494         return self.__auxCursor
2495
2496
2497     def _getBackendObject(self):
2498         return self.__backend
2499
2500     def _setBackendObject(self, obj):
2501         self.__backend = obj
2502         if obj and obj._cursor is None:
2503             obj._cursor = self
2504         if self.__auxCursor:
2505             self.__auxCursor.__backend = obj
2506
2507
2508     def _getCurrentSQL(self):
2509         if self.UserSQL:
2510             return self.UserSQL
2511         return self.AutoSQL
2512
2513
2514     def _getDescrip(self):
2515         return self.__backend.getDescription(self)
2516
2517
2518     def _getDataStructure(self):
2519         val = getattr(self, "_dataStructure", None)
2520         if val is None:
2521             # Get the information from the backend. Note that elements 3 and 4 get
2522             # guessed-at values.
2523             val = getattr(self, "_savedStructureDescription", [])
2524             if not val:
2525                 if self.BackendObject is None:
2526                     # Nothing we can do. We are probably an AuxCursor
2527                     pass
2528                 else:
2529                     ds = self.BackendObject.getStructureDescription(self)
2530                     gf_names = [gf[0] for gf in self.getFields(self.Table)]
2531                     for field in ds:
2532                         field_name, field_type, pk = field[0], field[1], field[2]
2533                         try:
2534                             field_scale = field[5]
2535                         except IndexError:
2536                             field_scale = None
2537                         if field_name in gf_names:
2538                             table_name = self.Table
2539                         else:
2540                             table_name = "_foreign_table_"
2541                         val.append((field_name, field_type, pk, table_name, field_name, field_scale))
2542                 self._savedStructureDescription = val
2543             self._dataStructure = val
2544         return tuple(val)
2545
2546     def _setDataStructure(self, val):
2547         # Go through the sequence, raising exceptions or adding default values as
2548         # appropriate.
2549         val = list(val)
2550         for idx, field in enumerate(val):
2551             field_alias = field[0]
2552             field_type = field[1]
2553             try:
2554                 field_pk = field[2]
2555             except IndexError:
2556                 field_pk = False
2557             try:
2558                 table_name = field[3]
2559             except IndexError:
2560                 table_name = self.Table
2561             try:
2562                 field_name = field[4]
2563             except IndexError:
2564                 field_name = field_alias
2565             try:
2566                 field_scale = field[5]
2567             except IndexError:
2568                 field_scale = None
2569             val[idx] = (field_alias, field_type, field_pk, table_name, field_name, field_scale)
2570         self._dataStructure = self.AuxCursor._dataStructure = tuple(val)
2571
2572
2573     def _getEncoding(self):
2574         return self.BackendObject.Encoding
2575
2576     def _setEncoding(self, val):
2577         self.BackendObject.Encoding = val
2578
2579
2580     def _getIsAdding(self):
2581         """ Return True if the current record is a new record."""
2582         if self.RowCount <= 0:
2583             return False
2584         try:
2585             getattr(self.Record, kons.CURSOR_TMPKEY_FIELD)
2586             return True
2587         except dException.FieldNotFoundException:
2588             return False
2589
2590
2591     def _getIsPrefCursor(self):
2592         return self._isPrefCursor
2593
2594     def _setIsPrefCursor(self, val):
2595         self._isPrefCursor = self.AuxCursor._isPrefCursor = val
2596
2597
2598     def _getKeyField(self):
2599         return self._keyField
2600
2601     def _setKeyField(self, kf):
2602         if "," in kf:
2603             flds = [f.strip() for f in kf.split(",")]
2604             self._keyField = tuple(flds)
2605             self._compoundKey = True
2606         else:
2607             self._keyField = ustr(kf)
2608             self._compoundKey = False
2609         self.AuxCursor._keyField = self._keyField
2610         self.AuxCursor._compoundKey = self._compoundKey
2611         self._keyFieldSet = self.AuxCursor._keyFieldSet = (self._hasValidKeyField)
2612
2613
2614     def _getLastSQL(self):
2615         try:
2616             v = self._lastSQL
2617         except AttributeError:
2618             v = self._lastSQL = None
2619         return v
2620
2621
2622     def _getParamPlaceholder(self):
2623         if self._paramPlaceholder:
2624             ret = self._paramPlaceholder
2625         else:
2626             ret = self._paramPlaceholder = self.BackendObject.paramPlaceholder
2627         return ret
2628
2629
2630     def _getRecord(self):
2631         try:
2632             ret = self._cursorRecord
2633         except AttributeError:
2634             class CursorRecord(object):
2635                 def __init__(self, _cursor):
2636                     self._cursor = _cursor
2637                     super(CursorRecord, self).__init__()
2638
2639                 def __getattr__(self, att):
2640                     return self._cursor.getFieldVal(att)
2641
2642                 def __setattr__(self, att, val):
2643                     if att in ("_cursor"):
2644                         super(CursorRecord, self).__setattr__(att, val)
2645                     else:
2646                         self._cursor.setFieldVal(att, val)
2647
2648                 def __getitem__(self, key):
2649                     return self.__getattr__(key)
2650
2651                 def __setitem__(self, key, val):
2652                     return self.__setattr__(key, val)
2653
2654             ## The rest of this block add a property to the Record object
2655             ## for each field, the sole purpose being to have the field
2656             ## names appear in the command window intellisense dropdown.
2657             def getFieldProp(field_name):
2658                 def fget(self):
2659                     return self._cursor.getFieldVal(field_name)
2660                 def fset(self, val):
2661                     self._cursor.setFieldVal(field_name, val)
2662                 return property(fget, fset)
2663
2664             field_aliases = [ds[0] for ds in self.DataStructure]
2665             field_aliases.extend(self.VirtualFields.keys())
2666             for field_alias in field_aliases:
2667                 setattr(CursorRecord, field_alias, getFieldProp(field_alias))
2668             ret = self._cursorRecord = CursorRecord(self)
2669         return ret
2670
2671
2672     def _getRowCount(self):
2673         try:
2674             ret = len(self._records)
2675         except AttributeError:
2676             ret = -1
2677         return ret
2678
2679
2680     def _getRowNumber(self):
2681         try:
2682             ret = min(self.__rownumber, self._getRowCount()-1)
2683         except AttributeError:
2684             ret = -1
2685         return ret
2686
2687
2688     def _setRowNumber(self, num):
2689         rn = max(0, num)
2690         maxrow = max(0, (self.RowCount-1) )
2691         self.__rownumber = min(rn, maxrow)
2692
2693
2694     def _getTable(self):
2695         return self._table
2696
2697     def _setTable(self, table):
2698         self._table = self.AuxCursor._table = self.sqlManager._table = "%s" % table
2699         if table and not self._keyFieldSet:
2700             flds = self.getFields(table)
2701             if flds is None:
2702                 return
2703             # Get the PK field, if any
2704             try:
2705                 self._keyField = [fld[0] for fld in flds
2706                         if fld[2] ][0]
2707             except IndexError:
2708                 pass
2709
2710
2711     def _getUserSQL(self):
2712         return self._userSQL
2713
2714     def _setUserSQL(self, val):
2715         if val:
2716             val = self._qMarkToParamPlaceholder(val)
2717         self._userSQL = val
2718
2719
2720     def _getVirtualFields(self):
2721         return self._virtualFields
2722
2723     def _setVirtualFields(self, val):
2724         assert isinstance(val, dict)
2725         self._virtualFields = val
2726
2727
2728     AutoPopulatePK = property(_getAutoPopulatePK, _setAutoPopulatePK, None,
2729             _("When inserting a new record, does the backend populate the PK field?"))
2730
2731     AutoQuoteNames = property(_getAutoQuoteNames, _setAutoQuoteNames, None,
2732             _("""When True (default), table and column names are enclosed with
2733             quotes during SQL creation.  (bool)"""))
2734
2735     AutoSQL = property(_getAutoSQL, None, None,
2736             _("Returns the SQL statement automatically generated by the sql manager."))
2737
2738     AuxCursor = property(_getAuxCursor, None, None,
2739             _("""Auxiliary cursor object that handles queries that would otherwise
2740             affect the main cursor's data set.  (dCursorMixin subclass)"""))
2741
2742     BackendObject = property(_getBackendObject, _setBackendObject, None,
2743             _("Returns a reference to the object defining backend-specific behavior (dBackend)"))
2744
2745     CurrentSQL = property(_getCurrentSQL, None, None,
2746             _("Returns the current SQL that will be run, which is one of UserSQL or AutoSQL."))
2747
2748     DataStructure = property(_getDataStructure, _setDataStructure, None,
2749             _("""Returns the structure of the cursor in a tuple of 6-tuples.
2750
2751                 0: field alias (str)
2752                 1: data type code (str)
2753                 2: pk field (bool)
2754                 3: table name (str)
2755                 4: field name (str)
2756                 5: field scale (int or None)
2757
2758                 This information will try to come from a few places, in order:
2759                 1) The explicitly-set DataStructure property
2760                 2) The backend table method"""))
2761
2762     Encoding = property(_getEncoding, _setEncoding, None,
2763             _("Encoding type used by the Backend  (string)") )
2764
2765     FieldDescription = property(_getDescrip, None, None,
2766             _("Tuple of field names and types, as returned by the backend  (tuple)") )
2767
2768     IsAdding = property(_getIsAdding, None, None,
2769             _("Returns True if the current record is new and unsaved"))
2770
2771     IsPrefCursor = property(_getIsPrefCursor, _setIsPrefCursor, None,
2772             _("""Returns True if this cursor is used for managing internal
2773             Dabo preferences and settings. Default=False.  (bool)"""))
2774
2775     LastSQL = property(_getLastSQL, None, None,
2776             _("Returns the last executed SQL statement."))
2777
2778     KeyField = property(_getKeyField, _setKeyField, None,
2779             _("""Name of field that is the PK. If multiple fields make up the key,
2780             separate the fields with commas. (str)"""))
2781
2782     ParamPlaceholder = property(_getParamPlaceholder, None, None,
2783             _("""The character(s) used to indicate a parameter in an SQL statement.
2784             This can be different for different backend systems. Read-only.  (str)"""))
2785
2786     Record = property(_getRecord, None, None,
2787             _("""Represents a record in the data set. You can address individual
2788             columns by referring to 'self.Record.fieldName' (read-only) (no type)"""))
2789
2790     RowNumber = property(_getRowNumber, _setRowNumber, None,
2791             _("Current row in the recordset."))
2792
2793     RowCount = property(_getRowCount, None, None,
2794             _("Current number of rows in the recordset. Read-only."))
2795
2796     Table = property(_getTable, _setTable, None,
2797             _("The name of the table in the database that this cursor is updating."))
2798
2799     UserSQL = property(_getUserSQL, _setUserSQL, None,
2800             _("SQL statement to run. If set, the automatic SQL builder will not be used."))
2801
2802     VirtualFields = property(_getVirtualFields, _setVirtualFields, None,
2803             _("""A dictionary mapping virtual_field_name to a function to call.
2804
2805             The specified function will be called when getFieldVal() is called on
2806             the specified field name."""))