Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

I think I've seen this ORM somewhere before...

root/trunk/storage/storeado.py

Revision 47 (checked in by fumanchu, 8 years ago)

1. AdapterToSQL now separates bool constants from bool expressions.
2. storage.db now has automatic connection pooling.
3. Fixed storeado against SQL Server (MSDE).
4. Added multithreading tests to zoo_fixture.

Line 
1 ##import sys
2 ### Put COM in free-threaded mode. This first thread will have
3 ### CoInitializeEx called automatically when pythoncom is imported.
4 ##sys.coinit_flags = 0
5 ##import pythoncom
6
7 import win32com.client
8 import pywintypes
9 import datetime
10 try:
11     import cPickle as pickle
12 except ImportError:
13     import pickle
14
15 try:
16     import fixedpoint
17 except ImportError:
18     pass
19
20 import dejavu
21 from dejavu import storage, logic
22 from dejavu.storage import db
23 from recur import sane_time
24
25 adOpenForwardOnly = 0
26 adOpenKeyset = 1
27 adOpenDynamic = 2
28 adOpenStatic = 3
29
30 adLockReadOnly = 1
31 adLockPessimistic = 2
32 adLockOptimistic = 3
33 adLockBatchOptimistic = 4
34
35 adUseClient = 3
36
37 # 12/30/1899, the zero-Date for ADO = 693594
38 zeroHour = datetime.date(1899, 12, 30).toordinal()
39
40
41 def time_from_com(com_date):
42     """Return a valid (day, datetime.time) from a COM date or time object."""
43     hour, mins = divmod(86400 * (float(com_date) % 1), 3600)
44     mins, sec = divmod(mins, 60)
45     # Must do both int() and round() or we'll be up to 1 second off.
46     hour = int(round(hour))
47     mins = int(round(mins))
48     sec = int(round(sec))
49     return sane_time(0, hour, mins, sec)
50
51
52 class AdapterFromADO(db.AdapterFromDB):
53     """Coerce incoming values from ADO to Dejavu datatypes."""
54    
55     def coerce_datetime_datetime(self, value, coltype):
56         # Illegal Date/Time values will crash the
57         # app when using value.Format(). Therefore,
58         # grab the value and figure the date ourselves.
59         # Use 1-second resolution only.
60         if isinstance(value, basestring):
61             return datetime.datetime(int(value[0:4]), int(value[4:6]),
62                                      int(value[6:8]))
63         else:
64             # For some reason, we need both float and int.
65             aDate = datetime.date.fromordinal(int(float(value)) + zeroHour)
66             day, aTime = time_from_com(value)
67             return datetime.datetime.combine(aDate, aTime)
68    
69     def coerce_datetime_date(self, value, coltype):
70         # See coerce_datetime
71         if isinstance(value, basestring):
72             return datetime.date(int(value[0:4]), int(value[4:6]),
73                                  int(value[6:8]))
74         else:
75             return datetime.date.fromordinal(int(float(value)) + zeroHour)
76    
77     def coerce_datetime_time(self, value, coltype):
78         # See coerce_datetime
79         day, aTime = time_from_com(value)
80         return aTime
81    
82     def coerce_fixedpoint_FixedPoint(self, value, coltype):
83         if coltype == 0x06:
84             # Currency
85             value = value[1] / 10000.0
86         return fixedpoint.FixedPoint(value)
87    
88     def coerce_float(self, value, coltype):
89         if coltype == 0x06:
90             # Currency
91             value = value[1] / 10000.0
92         return float(value)
93    
94     def coerce_int(self, value, coltype):
95         if coltype == 0x0b:
96             # Boolean
97             return value != 0
98         return int(value)
99    
100     coerce_bool = coerce_int
101    
102     def coerce_unicode(self, value, coltype):
103         if isinstance(value, unicode):
104             # For some reason, inValue is already a unicode object.
105             return value
106         if isinstance(value, str):
107             try:
108                 return unicode(value, "ISO-8859-1")
109             except UnicodeError:
110                 raise StandardError(type(value))
111         return unicode(value)
112
113
114
115 class AdapterToADOFields(storage.Adapter):
116     """Coerce outgoing values from Dejavu datatypes to ADO.Field types."""
117    
118     def noop(self, value):
119         return value
120    
121     def coerce_bool(self, value):
122         if value:
123             return True
124         return False
125    
126     def coerce_datetime_datetime(self, value):
127         if value is None:
128             return None
129         return self.coerce_datetime_date(value) + self.coerce_datetime_time(value)
130    
131     def coerce_datetime_date(self, value):
132         if value is None:
133             return None
134         return value.toordinal() - zeroHour
135    
136     def coerce_datetime_time(self, value):
137         if value is None:
138             return None
139         return ((value.second + (value.minute * 60) + (value.hour * 3600))
140                 / 86400.0)
141    
142     def pickle(self, value):
143         # We must not use a pickle format other than 0, because binary
144         # strings are not safe for all DB string fields.
145         return pickle.dumps(value)
146    
147     coerce_dict = pickle
148    
149     def coerce_fixedpoint_FixedPoint(self, value):
150         if value is None:
151             return None
152         return float(value)
153    
154     coerce_float = noop
155     coerce_int = noop
156    
157     coerce_list = pickle
158    
159     coerce_long = noop
160     coerce_str = noop
161    
162     coerce_tuple = pickle
163    
164     coerce_unicode = noop
165
166
167 class ADOSQLDecompiler(db.SQLDecompiler):
168    
169     def visit_COMPARE_OP(self, lo, hi):
170         op2, op1 = self.stack.pop(), self.stack.pop()
171         if op1 is db.cannot_represent or op2 is db.cannot_represent:
172             self.stack.append(db.cannot_represent)
173             return
174        
175         op = lo + (hi << 8)
176         if op in (6, 7):     # in, not in
177             # Looking for text in a field. Use Like (reverse terms).
178             # LIKE is case-insensitive in MS SQL Server (and there
179             # doesn't seem to be a way around it). Use icontainedby
180             # and just mark imperfect.
181             value = self.dejavu_icontainedby(op1, op2)
182             if op == 7:
183                 value = "NOT " + value
184             self.stack.append(value)
185             self.imperfect = True
186         elif op1 == 'NULL':
187             if op == 2:
188                 self.stack.append(op2 + " IS NULL")
189             elif op == 3:
190                 self.stack.append(op2 + " IS NOT NULL")
191             else:
192                 raise ValueError("Non-equality Null comparisons not allowed.")
193         elif op2 == 'NULL':
194             if op == 2:
195                 self.stack.append(op1 + " IS NULL")
196             elif op == 3:
197                 self.stack.append(op1 + " IS NOT NULL")
198             else:
199                 raise ValueError("Non-equality Null comparisons not allowed.")
200         else:
201             if (isinstance(op2, db.ConstWrapper)
202                 and isinstance(op2.basevalue, basestring)):
203                 # ADO comparison operators for strings are case-insensitive
204                 # by default. Rather than determine which columns in the DB
205                 # might be case-sensitive, just flag them all as imperfect.
206                 # TODO: might be possible to cast both to varbinary, but
207                 # that may cause problems with unicode columns.
208                 self.imperfect = True
209             self.stack.append(op1 + " " + self.sql_cmp_op[op] + " " + op2)
210    
211     def column_name(self, name):
212         return '%s.[%s]' % (self.tablename, name)
213    
214     # --------------------------- Dispatchees --------------------------- #
215    
216     def attr_startswith(self, tos, arg):
217         self.imperfect = True
218         return tos + " LIKE '" + arg.strip("'\"") + "%'"
219    
220     def attr_endswith(self, tos, arg):
221         self.imperfect = True
222         return tos + " LIKE '%" + arg.strip("'\"") + "'"
223    
224     def containedby(self, op1, op2):
225         self.imperfect = True
226         if isinstance(op1, ConstWrapper):
227             # Looking for text in a field. Use Like (reverse terms).
228             return op2 + " LIKE '%" + op1.strip("'\"") + "%'"
229         else:
230             # Looking for field in (a, b, c)
231             atoms = [self.adapter.coerce(x) for x in op2.basevalue]
232             return op1 + " IN (" + ", ".join(atoms) + ")"
233    
234     def dejavu_icontainedby(self, op1, op2):
235         if isinstance(op1, db.ConstWrapper):
236             # Looking for text in a field. Use Like (reverse terms).
237             # LIKE is already case-insensitive in MS SQL Server;
238             # so don't use LOWER().
239             value = op2 + " LIKE '%" + op1.strip("'\"") + "%'"
240         else:
241             # Looking for field in (a, b, c)
242             atoms = [self.adapter.coerce(x) for x in op2.basevalue]
243             value = op1 + " IN (" + ", ".join(atoms) + ")"
244         return value
245    
246     def dejavu_istartswith(self, x, y):
247         # Like is already case-insensitive in ADO; so don't use LOWER().
248         y = y.strip("'\"")
249         return x + " LIKE '" + y + "%'"
250    
251     def dejavu_iendswith(self, x, y):
252         # Like is already case-insensitive in ADO; so don't use LOWER().
253         y = y.strip("'\"")
254         return x + " LIKE '%" + y + "'"
255    
256     def dejavu_ieq(self, x, y):
257         # = is already case-insensitive in ADO.
258         return x + " = " + y
259    
260     def dejavu_now(self):
261         return "getdate()"
262    
263     def dejavu_today(self):
264         return "DATEADD(dd, DATEDIFF(dd,0,getdate()), 0)"
265    
266     def func__builtin___len(self, x):
267         return "Len(" + x + ")"
268
269
270 class StoreMultiIteratorADO(object):
271     """Iterator for populating Units (from multiple classes) from storage."""
272    
273     def __init__(self, store, unitClass, expr, pairs):
274         self.store  = store
275         self.unitClass = unitClass
276         self.expr = expr
277         self.pairs = pairs
278         self.fieldTypes = []
279        
280         sel = store.multiselect(unitClass, expr, pairs)
281         self.sql, self.imperfect, self.columns = sel
282    
283     def populate_unit(self, unit, row):
284         """Populate a Unit from a database row."""
285         consume = AdapterFromADO().consume
286         cls = unit.__class__
287         for key in cls.properties():
288             try:
289                 col = self.columns.index((cls, key))
290             except ValueError, x:
291                 x.args += (cls, key)
292                 raise x
293             else:
294                 consume(unit, key, self.data[col][row], self.fieldTypes[col])
295    
296     def load_data(self):
297         anRS = self.store.fetch(self.sql)
298        
299         for col, x in enumerate(anRS.Fields):
300             self.fieldTypes.append(x.Type)
301        
302         self.data = []
303         if not(anRS.BOF and anRS.EOF):
304             # We tried .MoveNext() and lots of Fields.Item() calls.
305             # Using GetRows() beats that time by about 2/3.
306             self.data = anRS.GetRows()
307         anRS.Close()
308    
309     def units(self):
310         self.load_data()
311         if len(self.data) > 0:
312             for row in range(len(self.data[0])):
313                 unit = self.unitClass()
314                 self.populate_unit(unit, row)
315                 # If our SQL is imperfect, don't yield it to the
316                 # caller unless it passes evaluate().
317                 if (not self.imperfect) or self.expr.evaluate(unit):
318                     cls, expr = self.pairs[0]
319                     farUnit = cls()
320                     self.populate_unit(farUnit, row)
321                     if farUnit.ID is None:
322                         yield unit, None
323                     elif ((not self.imperfect) or expr is None
324                           or expr.evaluate(farUnit)):
325                         yield unit, farUnit
326
327
328
329 class StorageManagerADO(db.StorageManagerDB):
330     """StoreManager to save and retrieve Units via ADO 2.7.
331     
332     You must run makepy on ADO 2.7 before installing.
333     """
334    
335     close_connection_method = 'Close'
336     decompiler = ADOSQLDecompiler
337     fromAdapter = AdapterFromADO()
338    
339     def connatoms(self):
340         atoms = {}
341         for pair in self.connstring.split(";"):
342             if pair:
343                 k, v = pair.split("=", 1)
344                 atoms[k.upper().strip()] = v.strip()
345         return atoms
346    
347     def identifier(self, *atoms):
348         ident = ''.join(map(str, atoms))
349         return '[' + ident + ']'
350    
351     def _get_conn(self):
352         conn = win32com.client.Dispatch(r'ADODB.Connection')
353         try:
354             conn.Open(self.connstring)
355             return conn
356         except pywintypes.com_error, x:
357             if x.args[2][5] == -2147467259:
358                 msg = x.args[2][2]
359                 if (
360                     # SQL Server: "Cannot open database requested in login
361                     # 'dejavu_test'. Login fails."
362                     msg.startswith("Cannot open database") or
363                     # MSAccess: "Could not find file
364                     # 'C:\Python23\Lib\site-packages\dejavu\storage\zoo.mdb'."
365                     msg.startswith("Could not find file")):
366                     if self.CreateIfMissing:
367                         self.create_database()
368                         conn.Open(self.connstring)
369                         return conn
370             raise
371    
372     def _join(self, path=[]):
373         if not path:
374             return u''
375         firstcls = path.pop(0)
376         if not path:
377             return firstcls.__name__
378        
379         spath = self.arena.associations.shortest_path(firstcls, path[0])
380         spath.pop(0)
381         cls = spath[0]
382        
383         p = self.prefix
384         i = self.identifier
385         left = i(p, firstcls.__name__)
386         right = i(p, cls.__name__)
387         if len(spath) == 1:
388             child = right
389         else:
390             child = u"(%s)" % self._join(spath)
391         leftkey, rightkey = firstcls._associations[cls]
392        
393         return (u"%s LEFT JOIN %s ON %s.%s = %s.%s" %
394                 (left, child, left, leftkey, right, rightkey))
395    
396     def multiselect(self, firstcls, firstexpr, pairs):
397         firstwhere, imp = self.where(firstcls, firstexpr)
398         cols = [(firstcls, k) for k in firstcls.properties()]
399        
400         if len(pairs) != 1:
401             raise ValueError("Multiselect does not yet work on multiple pairs.")
402        
403         for cls, expr in pairs:
404             if expr is None:
405                 expr = logic.Expression(lambda x: True)
406            
407             j = self._join([firstcls, cls])
408            
409             w, new_imp = self.where(cls, expr)
410             imp |= new_imp
411             if w and w != "TRUE":
412                 w = " WHERE %s AND %s" % (w, firstwhere)
413             else:
414                 w = " WHERE %s" % firstwhere
415            
416             cols += [(cls, k) for k in cls.properties()]
417             colnames = ["%s.%s" % (self.identifier(self.prefix, colcls.__name__),
418                                    self.identifier(k))
419                         for colcls, k in cols]
420            
421             statement = "SELECT %s FROM %s%s" % (u', '.join(colnames), j, w)
422            
423             return statement, imp, cols
424    
425     def execute(self, query, conn=None):
426         if conn is None:
427             conn = self.connection()
428         try:
429             conn.Execute(query)
430         except pywintypes.com_error, x:
431             x.args += (query, )
432             conn = None
433             raise x
434    
435     def fetch(self, query, conn=None):
436         """fetch(query, conn=None) -> rowdata, columns."""
437         if conn is None:
438             conn = self.connection()
439        
440         res = win32com.client.Dispatch(r'ADODB.Recordset')
441         # Uncomment the following to get .Recordcount
442         # res.CursorLocation = adUseClient
443         try:
444             # 'conn' will be a ConnectionWrapper object, which .Open
445             # won't accept. Pass the unwrapped connection instead.
446             res.Open(query, conn.conn, adOpenForwardOnly, adLockReadOnly)
447         except pywintypes.com_error, x:
448             try:
449                 res.Close()
450             except:
451                 pass
452             x.args += (query, )
453             raise x
454        
455         columns = [(x.Name, x.Type) for x in res.Fields]
456        
457         data = []
458         if not(res.BOF and res.EOF):
459             # We tried .MoveNext() and lots of Fields.Item() calls.
460             # Using GetRows() beats that time by about 2/3.
461             data = res.GetRows()
462             # Convert cols x rows -> rows x cols
463             data = zip(*data)
464         res.Close()
465        
466         return data, columns
467
468
469 ###########################################################################
470 ##                                                                       ##
471 ##                             SQL Server                                ##
472 ##                                                                       ##
473 ###########################################################################
474
475
476 class AdapterToADOSQL_SQLServer(db.AdapterToSQL):
477    
478     escapes = [("'", "''"), ("%", "[%]"), ("_", "[_]")]
479    
480     # These are not the same as coerce_bool (which is used on one side of
481     # a comparison). Instead, these are used when the whole (sub)expression
482     # is True or False, e.g. "WHERE TRUE", or "WHERE TRUE and 'a'.'b' = 3".
483     bool_true = "(1=1)"
484     bool_false = "(1=0)"
485    
486     def coerce_bool(self, value):
487         if value:
488             return '(1=1)'
489         return '(1=0)'
490
491
492 class FieldTypeAdapter_SQLServer(db.FieldTypeAdapter):
493    
494     numeric_max_precision = 38
495    
496     def coerce_datetime_datetime(self, cls, key):
497         return u"DATETIME"
498    
499     def coerce_datetime_date(self, cls, key):
500         return u"DATETIME"
501    
502     def coerce_datetime_time(self, cls, key):
503         return u"DATETIME"
504    
505     def coerce_str(self, cls, key):
506         # The bytes hint does not reflect the usual 4-byte base for varchar.
507         prop = getattr(cls, key)
508         bytes = int(prop.hints.get(u'bytes', '0'))
509         if bytes == 0:
510             # Okay, what the @#$%& is wrong with Redmond??!?! We can't even
511             # compare TEXT or NTEXT fields??!? Fine. We'll deny such, and
512             # warn the deployer with less swearing.
513             import warnings
514             warnings.warn("You have defined a string property without "
515                           "limiting its length. Microsoft SQL Server does "
516                           "not allow comparisons on string fields larger "
517                           "than 8000 characters. Some of your data may be "
518                           "truncated.")
519             bytes = 8000
520         # 8000 *bytes* is the absolute upper limit, based on T_SQL docs for
521         # varchar/varbinary. If there are further fields defined for the
522         # class, or the codepage uses a double-byte character set, we still
523         # might exceed the max size (8060) for a record. We could calc the
524         # total requested record size, and adjust accordingly. Meh.
525         return u"VARCHAR(%s)" % bytes
526
527
528 class StorageManagerADO_SQLServer(StorageManagerADO):
529    
530     typeAdapter = FieldTypeAdapter_SQLServer()
531     toAdapter = AdapterToADOSQL_SQLServer()
532    
533     def __init__(self, name, arena, allOptions={}):
534         db.StorageManagerDB.__init__(self, name, arena, allOptions)
535        
536         self.connstring = allOptions[u'Connect']
537         atoms = self.connatoms()
538         self.dbname = atoms[u'INITIAL CATALOG']
539         self.cursorType = int(allOptions.get(u'CursorType', adOpenDynamic))
540         self.lockType = int(allOptions.get(u'LockType', adLockOptimistic))
541    
542     def create_database(self):
543         # This method hasn't been tested yet for SQL server.
544         adoconn = win32com.client.Dispatch(r'ADODB.Connection')
545         atoms = self.connatoms()
546         atoms['INITIAL CATALOG'] = "tempdb"
547         adoconn.Open("; ".join(["%s=%s" % (k, v) for k, v in atoms.iteritems()]))
548         adoconn.Execute("CREATE DATABASE %s" % self.identifier(self.dbname))
549         adoconn.Close()
550    
551     def drop_database(self):
552         adoconn = win32com.client.Dispatch(r'ADODB.Connection')
553         atoms = self.connatoms()
554         atoms['INITIAL CATALOG'] = "tempdb"
555         adoconn.Open("; ".join(["%s=%s" % (k, v) for k, v in atoms.iteritems()]))
556         adoconn.Execute("DROP DATABASE %s;" % self.identifier(self.dbname))
557         adoconn.Close()
558
559
560 ###########################################################################
561 ##                                                                       ##
562 ##                             MS Access                                 ##
563 ##                                                                       ##
564 ###########################################################################
565
566
567 class ADOSQLDecompiler_MSAccess(ADOSQLDecompiler):
568     sql_cmp_op = ('<', '<=', '=', '<>', '>', '>=', 'in', 'not in')
569    
570     def dejavu_now(self):
571         return "Now()"
572    
573     def dejavu_today(self):
574         return "DateValue(Now())"
575    
576     def dejavu_year(self, x):
577         return "Year(" + x + ")"
578
579
580 class FieldTypeAdapter_MSAccess(db.FieldTypeAdapter):
581    
582     numeric_max_precision = 28
583    
584     def coerce_str(self, cls, key):
585         # The bytes hint shall not reflect the usual 4-byte base for varchar.
586         prop = getattr(cls, key)
587         bytes = int(prop.hints.get(u'bytes', '0'))
588         if bytes and bytes <= 255:
589             # 255 chars is the upper limit for TEXT / VARCHAR in MS Access.
590             return u"VARCHAR(%s)" % bytes
591         else:
592             # MEMO is 1 gigabyte when set programatically (only 64K when set
593             # in Access UI). But then, 1 GB is the limit for the whole DB.
594             return u"MEMO"
595
596
597 class AdapterToADOSQL_MSAccess(db.AdapterToSQL):
598     """Coerce Expression constants to ADO SQL."""
599    
600     escapes = [("'", "''"), ("%", "[%]"), ("_", "[_]")]
601    
602     def coerce_datetime_datetime(self, value):
603         return (u'#%s/%s/%s %02d:%02d:%02d#' %
604                 (value.month, value.day, value.year,
605                  value.hour, value.minute, value.second))
606    
607     def coerce_datetime_date(self, value):
608         return u'#%s/%s/%s#' % (value.month, value.day, value.year)
609    
610     def coerce_datetime_time(self, value):
611         return u'#%02d:%02d:%02d#' % (value.hour, value.minute, value.second)
612
613
614 class StorageManagerADO_MSAccess(StorageManagerADO):
615     # Jet Connections and Recordsets are always free-threaded.
616    
617     decompiler = ADOSQLDecompiler_MSAccess
618     typeAdapter = FieldTypeAdapter_MSAccess()
619     toAdapter = AdapterToADOSQL_MSAccess()
620    
621     def __init__(self, name, arena, allOptions={}):
622         db.StorageManagerDB.__init__(self, name, arena, allOptions)
623        
624         self.connstring = allOptions[u'Connect']
625         atoms = self.connatoms()
626         self.dbname = atoms.get(u'DATA SOURCE') or atoms.get(u'DATA SOURCE NAME')
627         self.cursorType = int(allOptions.get(u'CursorType', adOpenDynamic))
628         self.lockType = int(allOptions.get(u'LockType', adLockOptimistic))
629         self.pool = None
630    
631     def create_database(self):
632         # By not providing an Engine Type, it defaults to 5 = Access 2000.
633         cat = win32com.client.Dispatch(r'ADOX.Catalog')
634         cat.Create(self.connstring)
635         cat.ActiveConnection.Close()
636    
637     def drop_database(self):
638         import os
639         # This should accept relative or absolute paths
640         if os.path.exists(self.dbname):
641             os.remove(self.dbname)
642
643
644 if __name__ == '__main__':
645     # Auto generate .py support for ADO 2.7
646     print 'Please wait while support for ADO 2.7 is verified...'
647     CLSID = '{EF53050B-882E-4776-B643-EDA472E8E3F2}'
648     win32com.client.gencache.EnsureModule(CLSID, 0, 2, 7)
649
Note: See TracBrowser for help on using the browser.