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 353 (checked in by fumanchu, 6 years ago)

New behavior for the 'in' operator: if op2 is empty, emit "FALSE" instead of "val IN ()". This makes the SQL faster as well as avoiding varying DB syntax for empty lists.

  • Property svn:eol-style set to native
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
6 import pythoncom
7 Empty = pythoncom.Empty
8 clsctx = pythoncom.CLSCTX_SERVER
9
10 import win32com.client
11
12 # InvokeTypes args (always pass as *args)
13 BOF = (1002, 0, 2, (11, 0), ())
14 EOF = (1006, 0, 2, (11, 0), ())
15 Recordset_Fields = (0, 0, 2, (9, 0), ())
16 # This assumes no arguments passed to GetRows
17 Recordset_GetRows = (1016, 0, 1, (12, 0), ((3, 49), (12, 17), (12, 17)), -1, Empty, Empty)
18 Recordset_Close = (1014, 0, 1, (24, 0), (),)
19 Fields_Count = (1, 0, 2, (3, 0), ())
20 Field_Name = (1100, 0, 2, (8, 0), ())
21 Field_Type = (1102, 0, 2, (3, 0), ())
22 Field_Properties = (500, 0, 2, (9, 0), ())
23 Property_Value = (0, 0, 2, (12, 0), ())
24
25 import pywintypes
26 import datetime
27 import time
28
29 try:
30     import cPickle as pickle
31 except ImportError:
32     import pickle
33
34 import threading
35 import warnings
36
37 import dejavu
38 from dejavu import errors, logic, storage
39 from dejavu.storage import db
40
41 adOpenForwardOnly = 0
42 adOpenKeyset = 1
43 adOpenDynamic = 2
44 adOpenStatic = 3
45
46 adLockReadOnly = 1
47 adLockPessimistic = 2
48 adLockOptimistic = 3
49 adLockBatchOptimistic = 4
50
51 adSchemaColumns = 4
52 adSchemaIndexes = 12
53 adSchemaTables = 20
54 adSchemaPrimaryKeys = 28
55
56 adUseClient = 3
57
58 # 12/30/1899, the zero-Date for ADO = 693594
59 zeroHour = datetime.date(1899, 12, 30).toordinal()
60
61 dbtypes = {
62     0: 'EMPTY',                     2: 'SMALLINT',
63     3: 'INTEGER',                   4: 'SINGLE',
64     5: 'DOUBLE',                    6: 'CURRENCY',
65     7: 'DATE',                      8: 'BSTR',
66     9: 'IDISPATCH',                 10: 'ERROR',
67     11: 'BOOLEAN',                  12: 'VARIANT',
68     13: 'IUNKNOWN',                 14: 'DECIMAL',
69     16: 'TINYINT',                  17: 'UNSIGNEDTINYINT',
70     18: 'UNSIGNEDSMALLINT',         19: 'UNSIGNEDINT',
71     20: 'BIGINT',                   21: 'UNSIGNEDBIGINT',
72     72: 'GUID',                     128: 'BINARY',
73     129: 'CHAR',                    130: 'WCHAR',
74     131: 'NUMERIC',                 132: 'USERDEFINED',
75     133: 'DBDATE',                  134: 'DBTIME',
76     135: 'DBTIMESTAMP',             200: 'VARCHAR',
77     201: 'LONGVARCHAR',             202: 'VARWCHAR',
78     203: 'LONGVARWCHAR',            204: 'VARBINARY',
79     205: 'LONGVARBINARY'
80 }
81
82 DBCOLUMNFLAGS_WRITE = 0x4
83 DBCOLUMNFLAGS_WRITEUNKNOWN = 0x8
84 DBCOLUMNFLAGS_ISFIXEDLENGTH = 0x10
85 DBCOLUMNFLAGS_ISNULLABLE = 0x20
86 DBCOLUMNFLAGS_MAYBENULL = 0x40
87 DBCOLUMNFLAGS_ISLONG = 0x80
88 DBCOLUMNFLAGS_ISROWID = 0x100
89 DBCOLUMNFLAGS_ISROWVER = 0x200
90 DBCOLUMNFLAGS_CACHEDEFERRED = 0x1000
91
92
93 def time_from_com(com_date):
94     """Return a valid datetime.time from a COM date or time object."""
95     hour, minute = divmod(86400 * (float(com_date) % 1), 3600)
96     minute, second = divmod(minute, 60)
97     # Must do both int() and round() or we'll be up to 1 second off.
98     hour = int(round(hour))
99     minute = int(round(minute))
100     second = int(round(second))
101    
102     while second > 59:
103         second -= 60
104         minute += 1
105     while second < 0:
106         second += 60
107         minute -= 1
108     while minute > 59:
109         minute -= 60
110         hour += 1
111     while minute < 0:
112         minute += 60
113         hour -= 1
114     while hour > 23:
115         hour -= 24
116         day += 1
117     while hour < 0:
118         hour += 24
119    
120     return datetime.time(hour, minute, second)
121
122 class AdapterFromADO(db.AdapterFromDB):
123     """Coerce incoming values from ADO to Dejavu datatypes."""
124    
125     encoding = 'ISO-8859-1'
126    
127     def coerce_any_to_datetime_datetime(self, value):
128         # Illegal Date/Time values will crash the
129         # app when using value.Format(). Therefore,
130         # grab the value and figure the date ourselves.
131         # Use 1-second resolution only.
132         if isinstance(value, basestring):
133             if value:
134                 try:
135                     return datetime.datetime(int(value[0:4]), int(value[4:6]),
136                                              int(value[6:8]))
137                 except Exception:
138                     raise ValueError("'%s' %s" % (value, type(value)))
139             else:
140                 return None
141         else:
142             # For some reason, we need both float and int.
143             aDate = datetime.date.fromordinal(int(float(value)) + zeroHour)
144             return datetime.datetime.combine(aDate, time_from_com(value))
145    
146     def coerce_any_to_datetime_date(self, value):
147         # See coerce_any_to_datetime
148         if isinstance(value, basestring):
149             if value:
150                 try:
151                     return datetime.date(int(value[0:4]), int(value[4:6]),
152                                          int(value[6:8]))
153                 except Exception:
154                     raise ValueError("'%s' %s" % (value, type(value)))
155             else:
156                 return None
157         else:
158             return datetime.date.fromordinal(int(float(value)) + zeroHour)
159    
160     def coerce_any_to_datetime_time(self, value):
161         # See coerce_any_to_datetime
162         return time_from_com(value)
163    
164     def coerce_any_to_decimal_Decimal(self, value):
165         # pywin32 build 205 began support for returning
166         # COM Currency objects as decimal objects.
167         # See http://pywin32.cvs.sourceforge.net/pywin32/pywin32/CHANGES.txt?view=markup
168         if not isinstance(value, db.decimal.Decimal):
169             value = str(value)
170             value = db.decimal.Decimal(str(value))
171         return value
172    
173     def coerce_CURRENCY_to_float(self, value):
174         if isinstance(value, tuple):
175             # See http://groups.google.com/group/comp.lang.python/
176             #           browse_frm/thread/fed03c64735c9e9c
177             value = map(long, value)
178             return ((value[1] & 0xFFFFFFFFL) | (value[0] << 32)) / 1e4
179         return float(value)
180    
181     def coerce_CURRENCY_to_decimal_Decimal(self, value):
182         # pywin32 build 205 began support for returning
183         # COM Currency objects as decimal objects.
184         # See http://pywin32.cvs.sourceforge.net/pywin32/pywin32/CHANGES.txt?view=markup
185         if not isinstance(value, db.decimal.Decimal):
186             # See http://groups.google.com/group/comp.lang.python/
187             #           browse_frm/thread/fed03c64735c9e9c
188             value = map(long, value)
189             value = (value[1] & 0xFFFFFFFFL) | (value[0] << 32)
190             return db.decimal.Decimal(value) / 10000
191         return value
192    
193     def coerce_CURRENCY_to_fixedpoint_FixedPoint(self, value):
194         if isinstance(value, db.decimal.Decimal):
195             value = str(value)
196             scale = 0
197             atoms = value.rsplit(".", 1)
198             if len(atoms) > 1:
199                 scale = len(atoms[-1])
200             return db.fixedpoint.FixedPoint(value, scale)
201         else:
202             # See http://groups.google.com/group/comp.lang.python/
203             #           browse_frm/thread/fed03c64735c9e9c
204             value = map(long, value)
205             value = (value[1] & 0xFFFFFFFFL) | (value[0] << 32)
206             return db.fixedpoint.FixedPoint(value, 4) / 1e4
207    
208     def coerce_any_to_unicode(self, value):
209         if isinstance(value, unicode):
210             # For some reason, inValue is already a unicode object.
211             return value
212         if isinstance(value, (basestring, buffer)):
213             try:
214                 return unicode(value, self.encoding)
215             except UnicodeError:
216                 raise StandardError(type(value))
217         return unicode(value)
218
219
220
221 class ADOSQLDecompiler(db.SQLDecompiler):
222    
223     def visit_COMPARE_OP(self, lo, hi):
224         op2, op1 = self.stack.pop(), self.stack.pop()
225         if op1 is db.cannot_represent or op2 is db.cannot_represent:
226             self.stack.append(db.cannot_represent)
227             return
228        
229         op = lo + (hi << 8)
230         if op in (6, 7):     # in, not in
231             # Looking for text in a field. Use Like (reverse terms).
232             # LIKE is case-insensitive in MS SQL Server (and there
233             # doesn't seem to be a way around it). Use icontainedby
234             # and just mark imperfect.
235             value = self.dejavu_icontainedby(op1, op2)
236             if op == 7:
237                 value = "NOT " + value
238             self.stack.append(value)
239             self.imperfect = True
240         elif op1 == 'NULL':
241             if op in (2, 8):    # '==', is
242                 self.stack.append(op2 + " IS NULL")
243             elif op in (3, 9):  # '!=', 'is not'
244                 self.stack.append(op2 + " IS NOT NULL")
245             else:
246                 raise ValueError("Non-equality Null comparisons not allowed.")
247         elif op2 == 'NULL':
248             if op in (2, 8):    # '==', 'is'
249                 self.stack.append(op1 + " IS NULL")
250             elif op in (3, 9):  # '!=', 'is not'
251                 self.stack.append(op1 + " IS NOT NULL")
252             else:
253                 raise ValueError("Non-equality Null comparisons not allowed.")
254         else:
255             if (isinstance(op2, db.ConstWrapper)
256                 and isinstance(op2.basevalue, basestring)):
257                 atom = self._compare_strings(op1, op, op2)
258                 if atom:
259                     self.stack.append(atom)
260                     return
261             self.stack.append(op1 + " " + self.sql_cmp_op[op] + " " + op2)
262    
263     def _compare_strings(self, op1, op, op2):
264         # ADO comparison operators for strings are case-insensitive
265         # by default. Rather than determine which columns in the DB
266         # might be case-sensitive, just flag them all as imperfect.
267         # TODO: might be possible to cast both to varbinary, but
268         # that may cause problems with unicode columns.
269         self.imperfect = True
270    
271    
272     # --------------------------- Dispatchees --------------------------- #
273    
274     def attr_startswith(self, tos, arg):
275         self.imperfect = True
276         return tos + " LIKE '" + self.adapter.escape_like(arg) + "%'"
277    
278     def attr_endswith(self, tos, arg):
279         self.imperfect = True
280         return tos + " LIKE '%" + self.adapter.escape_like(arg) + "'"
281    
282     def containedby(self, op1, op2):
283         self.imperfect = True
284         if isinstance(op1, ConstWrapper):
285             # Looking for text in a field. Use Like (reverse terms).
286             return op2 + " LIKE '%" + self.adapter.escape_like(op1) + "%'"
287         else:
288             # Looking for field in (a, b, c)
289             atoms = [self.adapter.coerce(x) for x in op2.basevalue]
290             if atoms:
291                 return op1 + " IN (" + ", ".join(atoms) + ")"
292             else:
293                 # Nothing will match the empty list, so return none.
294                 return self.adapter.bool_false
295    
296     def dejavu_icontainedby(self, op1, op2):
297         if isinstance(op1, db.ConstWrapper):
298             # Looking for text in a field. Use Like (reverse terms).
299             # LIKE is already case-insensitive in MS SQL Server;
300             # so don't use LOWER().
301             value = op2 + " LIKE '%" + self.adapter.escape_like(op1) + "%'"
302         else:
303             # Looking for field in (a, b, c)
304             atoms = [self.adapter.coerce(x) for x in op2.basevalue]
305             if atoms:
306                 return op1 + " IN (" + ", ".join(atoms) + ")"
307             else:
308                 # Nothing will match the empty list, so return none.
309                 return self.adapter.bool_false
310         return value
311    
312     def dejavu_istartswith(self, x, y):
313         # Like is already case-insensitive in ADO; so don't use LOWER().
314         return x + " LIKE '" + self.adapter.escape_like(y) + "%'"
315    
316     def dejavu_iendswith(self, x, y):
317         # Like is already case-insensitive in ADO; so don't use LOWER().
318         return x + " LIKE '%" + self.adapter.escape_like(y) + "'"
319    
320     def dejavu_ieq(self, x, y):
321         # = is already case-insensitive in ADO.
322         return x + " = " + y
323    
324     def dejavu_now(self):
325         return "getdate()"
326    
327     def dejavu_today(self):
328         return "DATEADD(dd, DATEDIFF(dd,0,getdate()), 0)"
329    
330     def dejavu_year(self, x):
331         return "DATEPART(year, " + x + ")"
332    
333     def dejavu_month(self, x):
334         return "DATEPART(month, " + x + ")"
335    
336     def dejavu_day(self, x):
337         return "DATEPART(day, " + x + ")"
338    
339     def func__builtin___len(self, x):
340         return "Len(" + x + ")"
341
342
343 class ADOColumnSet(db.ColumnSet):
344    
345     def _rename(self, oldcol, newcol):
346         conn = self.table.db.connection()
347         try:
348             cat = win32com.client.Dispatch(r'ADOX.Catalog')
349             cat.ActiveConnection = conn
350             cat.Tables(self.table.name).Columns(oldcol.name).Name = newcol.name
351         finally:
352             conn = None
353             cat = None
354
355
356 def connatoms(connstring):
357     atoms = {}
358     for pair in connstring.split(";"):
359         if pair:
360             k, v = pair.split("=", 1)
361             atoms[k.upper().strip()] = v.strip()
362     return atoms
363
364
365 class ADODatabase(db.Database):
366    
367     decompiler = ADOSQLDecompiler
368     adapterfromdb = AdapterFromADO()
369     columnsetclass = ADOColumnSet
370     # the amount of time to try to close the db connection
371     # before raising an exception
372     shutdowntimeout = 1 # sec.
373    
374     def _get_tables(self, conn=None):
375         # cols will be
376         # [(u'TABLE_CATALOG', 202), (u'TABLE_SCHEMA', 202), (u'TABLE_NAME', 202),
377         # (u'TABLE_TYPE', 202), (u'TABLE_GUID', 72), (u'DESCRIPTION', 203),
378         # (u'TABLE_PROPID', 19), (u'DATE_CREATED', 7), (u'DATE_MODIFIED', 7)]
379         data, _ = self.fetch(adSchemaTables, conn=conn, schema=True)
380         return [db.Table(self, str(row[2]), self.quote(str(row[2])))
381                 for row in data]
382    
383     def _get_columns(self, tablename, conn=None):
384         # coldefs will be:
385         # [(u'TABLE_CATALOG', 202), (u'TABLE_SCHEMA', 202), (u'TABLE_NAME', 202),
386         # (u'COLUMN_NAME', 202), (u'COLUMN_GUID', 72), (u'COLUMN_PROPID', 19),
387         # (u'ORDINAL', 19), (u'PK_NAME', 202)]
388         data, _ = self.fetch(adSchemaPrimaryKeys, conn=conn, schema=True)
389         pknames = [row[3] for row in data if tablename == row[2]]
390        
391         # columns will be
392         # [(u'TABLE_CATALOG', 202), (u'TABLE_SCHEMA', 202), (u'TABLE_NAME', 202),
393         # (u'COLUMN_NAME', 202), (u'COLUMN_GUID', 72), (u'COLUMN_PROPID', 19),
394         # (u'ORDINAL_POSITION', 19), (u'COLUMN_HASDEFAULT', 11),
395         # (u'COLUMN_DEFAULT', 203), (u'COLUMN_FLAGS', 19), (u'IS_NULLABLE', 11),
396         # (u'DATA_TYPE', 18), (u'TYPE_GUID', 72), (u'CHARACTER_MAXIMUM_LENGTH', 19),
397         # (u'CHARACTER_OCTET_LENGTH', 19), (u'NUMERIC_PRECISION', 18),
398         # (u'NUMERIC_SCALE', 2), (u'DATETIME_PRECISION', 19),
399         # (u'CHARACTER_SET_CATALOG', 202), (u'CHARACTER_SET_SCHEMA', 202),
400         # (u'CHARACTER_SET_NAME', 202), (u'COLLATION_CATALOG', 202),
401         # (u'COLLATION_SCHEMA', 202), (u'COLLATION_NAME', 202),
402         # (u'DOMAIN_CATALOG', 202), (u'DOMAIN_SCHEMA', 202),
403         # (u'DOMAIN_NAME', 202), (u'DESCRIPTION', 203)]
404         data, _ = self.fetch(adSchemaColumns, conn=conn, schema=True)
405        
406         cols = []
407         for row in data:
408             # I tried passing criteria to OpenSchema, but passing None is
409             # not the same as passing pythoncom.Empty (which errors).
410             if row[2] != tablename:
411                 continue
412            
413             dbtype = dbtypes[row[11]]
414             default = row[8]
415             if default is not None:
416                 deftype = self.python_type(dbtype)
417                 if issubclass(deftype, (int, long)):
418                     # We may have stuck extraneous quotes in the default
419                     # value when using numeric defaults with MSAccess.
420                     if default.startswith("'") and default.endswith("'"):
421                         default = default[1:-1]
422                 default = deftype(default)
423            
424             name = str(row[3])
425             c = db.Column(name, self.quote(name), dbtype, default,
426                           key=name in pknames)
427            
428             # This only works for SQL Server. The MSAccessDatabase will
429             # wrap this method and override autoincrement.
430             colflags = int(row[9])
431             if ((colflags & DBCOLUMNFLAGS_ISFIXEDLENGTH)
432                 and not (colflags & DBCOLUMNFLAGS_WRITE)):
433                 c.autoincrement = True
434            
435             if dbtype in ("SMALLINT", "INTEGER", "TINYINT",
436                           "UNSIGNEDTINYINT", "UNSIGNEDSMALLINT",
437                           "UNSIGNEDINT", "BIGINT", "UNSIGNEDBIGINT"):
438                 c.hints['bytes'] = row[15]
439             elif dbtype in ("SINGLE", "DOUBLE"):
440                 c.hints['precision'] = row[15]
441                 c.hints['scale'] = row[16]
442             elif dbtype == "CURRENCY":
443                 # CURRENCY allows 15 places to the left of the decimal point,
444                 # and 4 places to the right.
445                 c.hints['precision'] = 19
446                 c.hints['scale'] = 4
447             elif dbtype in ("DECIMAL", "NUMERIC"):
448                 c.hints['precision'] = row[15]
449                 c.hints['scale'] = row[16]
450                 c.dbtype = "%s(%s, %s)" % (dbtype, row[15], row[16])
451             elif dbtype in ("BSTR", "VARIANT", "BINARY", "CHAR",
452                             "VARCHAR", "VARBINARY", "WCHAR", "VARWCHAR"):
453                 if row[13]:
454                     # row[13] will be a float
455                     c.hints['bytes'] = b = int(row[13])
456                 else:
457                     # I'm kinda guessing on this. If we use "MEMO" in an
458                     # MSAccess CREATE statement, it comes back as "WCHAR",
459                     # and seems to support over 65536 bytes.
460                     c.hints['bytes'] = b = (2 ** 31) - 1
461                 c.dbtype = "%s(%s)" % (c.dbtype, b)
462             elif dbtype in ("LONGVARCHAR", "LONGVARBINARY", "LONGVARWCHAR"):
463                 if row[13]:
464                     # row[13] will be a float
465                     c.hints['bytes'] = b = int(row[13])
466                     c.dbtype = "%s(%s)" % (c.dbtype, b)
467                 else:
468                     c.hints['bytes'] = 65535
469            
470             cols.append(c)
471         return cols
472    
473     def _get_indices(self, tablename=None, conn=None):
474         # cols will be
475         # [(u'TABLE_CATALOG', 202), (u'TABLE_SCHEMA', 202), (u'TABLE_NAME', 202),
476         # (u'INDEX_CATALOG', 202), (u'INDEX_SCHEMA', 202), (u'INDEX_NAME', 202),
477         # (u'PRIMARY_KEY', 11), (u'UNIQUE', 11), (u'CLUSTERED', 11), (u'TYPE', 18),
478         # (u'FILL_FACTOR', 3), (u'INITIAL_SIZE', 3), (u'NULLS', 3),
479         # (u'SORT_BOOKMARKS', 11), (u'AUTO_UPDATE', 11), (u'NULL_COLLATION', 3),
480         # (u'ORDINAL_POSITION', 19), (u'COLUMN_NAME', 202), (u'COLUMN_GUID', 72),
481         # (u'COLUMN_PROPID', 19), (u'COLLATION', 2), (u'CARDINALITY', 21),
482         # (u'PAGES', 3), (u'FILTER_CONDITION', 202), (u'INTEGRATED', 11)]
483         data, _ = self.fetch(adSchemaIndexes, conn=conn, schema=True)
484         indices = []
485         for row in data:
486             # I tried passing criteria to OpenSchema, but passing None is
487             # not the same as passing pythoncom.Empty (which errors).
488             if tablename and row[2] != tablename:
489                 continue
490             i = db.Index(row[5], self.quote(row[5]), row[2], row[17], row[7])
491             indices.append(i)
492         return indices
493    
494     def python_type(self, dbtype):
495         """Return a Python type which can store values of the given dbtype."""
496         if dbtype in ("DATE", "DBDATE"):
497             return datetime.date
498         elif dbtype == "DBTIME":
499             return datetime.time
500         elif dbtype in ("DATETIME", "DBTIMESTAMP"):
501             return datetime.datetime
502         elif dbtype in ("SMALLINT", "INTEGER", "TINYINT",
503                         "UNSIGNEDTINYINT", "UNSIGNEDSMALLINT",
504                         "UNSIGNEDINT"):
505             return int
506         elif dbtype in ("BIT", "BOOLEAN"):
507             return bool
508         elif dbtype in ("BIGINT", "UNSIGNEDBIGINT", "LONG"):
509             return long
510         elif dbtype in ("SINGLE", "DOUBLE", "DOUBLE PRECISION", "REAL"):
511             return float
512        
513         for t in ("DECIMAL", "NUMERIC", "CURRENCY"):
514             if dbtype.startswith(t):
515                 if db.decimal:
516                     return db.decimal.Decimal
517                 elif db.fixedpoint:
518                     return db.fixedpoint.FixedPoint
519        
520         for t in ("BSTR", "VARIANT", "BINARY", "CHAR", "MEMO", "TEXT",
521                   "VARCHAR", "LONGVARCHAR", "VARBINARY", "LONGVARBINARY"):
522             if dbtype.startswith(t):
523                 return str
524        
525         for t in ("WCHAR", "VARWCHAR", "LONGVARWCHAR"):
526             if dbtype.startswith(t):
527                 return unicode
528        
529         raise TypeError("Database type %r could not be converted "
530                         "to a Python type." % dbtype)
531    
532     def _rename(self, oldtable, newtable):
533         conn = self.connection()
534         try:
535             cat = win32com.client.Dispatch(r'ADOX.Catalog')
536             cat.ActiveConnection = conn
537             cat.Tables(oldtable.name).Name = newtable.name
538         finally:
539             conn = None
540             cat = None
541    
542     def quote(self, name):
543         """Return name, quoted for use in an SQL statement."""
544         return '[' + name + ']'
545    
546     def _get_conn(self):
547         conn = win32com.client.Dispatch(r'ADODB.Connection')
548         conn.Open(self.Connect)
549         return conn
550    
551     def _del_conn(self, conn):
552         for trial in xrange(self.shutdowntimeout * 10):
553             try:
554                 # This may raise "Operation cannot be performed while executing asynchronously"
555                 # if a prior operation has not yet completed.
556                 conn.Close()
557                 return
558             except pywintypes.com_error, e:
559                 try:
560                     ecode = e.args[2][-1]
561                 except IndexError:
562                     ecode = None
563                 if ecode == -2146824577:
564                     # "Operation cannot be performed while executing asynchronously"
565                     # Try again...
566                     time.sleep(0.1)
567                     continue
568                 raise
569    
570     def start(self):
571         """Start a transaction. Not needed if self.implicit_trans is True."""
572         self.execute("BEGIN TRANSACTION;", self.get_transaction(new=True))
573    
574     def execute(self, query, conn=None):
575         if conn is None:
576             conn = self.connection()
577         if isinstance(query, unicode):
578             query = query.encode(self.adaptertosql.encoding)
579        
580         self.log(query)
581         try:
582             if isinstance(conn, db.ConnectionWrapper):
583                 # 'conn' is a ConnectionWrapper object, which .Open
584                 # won't accept. Pass the unwrapped connection instead.
585                 conn = conn.conn
586            
587             # Call Execute directly, skipping win32com overhead.
588             conn._oleobj_.InvokeTypes(6, 0, 1, (9, 0),
589                                       ((8, 1), (16396, 18), (3, 49)),
590                                       query, pythoncom.Missing, -1)
591         except pywintypes.com_error, x:
592             x.args += (query, )
593             conn = None
594             raise
595    
596     def fetch(self, query, conn=None, schema=False):
597         """fetch(query, conn=None) -> rowdata, columns."""
598         if conn is None:
599             conn = self.connection()
600        
601         try:
602             if schema:
603                 # Call OpenSchema(query) directly, skipping win32com overhead.
604                 res = conn._oleobj_.InvokeTypes(19, 0, 1, (9, 0),
605                                                 ((3, 1), (12, 17), (12, 17)),
606                                                 query, Empty, Empty)
607             else:
608                 self.log(query)
609                 if isinstance(conn, db.ConnectionWrapper):
610                     # 'conn' is a ConnectionWrapper object, which .Open
611                     # won't accept. Pass the unwrapped connection instead.
612                     conn = conn.conn
613                
614                 # Call conn.Open(query) directly, skipping win32com overhead.
615                 res, rows_affected = conn._oleobj_.InvokeTypes(6, 0, 1, (9, 0),
616                                                 ((8, 1), (16396, 18), (3, 49)),
617                                                 # *args =
618                                                 query, pythoncom.Missing, -1)
619         except pywintypes.com_error, x:
620             try:
621                 # Close
622                 res.InvokeTypes(*Recordset_Close)
623             except:
624                 pass
625             res = None
626             x.args += (query, )
627             conn = None
628             # "raise x" here or we could get the traceback of the inner try.
629             raise x
630        
631         # Using xrange(Count) is slightly faster than "for x in resFields".
632         resFields = res.InvokeTypes(*Recordset_Fields)
633         fieldcount = resFields.InvokeTypes(*Fields_Count)
634         columns = []
635         for i in xrange(fieldcount):
636             # Wow. Calling this directly (instead of resFields(i))
637             # results in a 29% speedup for a 1-row fetch() of 48 fields.
638             x = resFields.InvokeTypes(0, 0, 2, (9, 0), ((12, 1),), i)
639            
640             # Wow. Calling these directly (instead of x.Name, x.Type)
641             # results in a 40% speedup for a 1-row fetch() of 48 fields.
642             name = x.InvokeTypes(*Field_Name)
643             typ = x.InvokeTypes(*Field_Type)
644             columns.append((name, typ))
645        
646         data = []
647         if not (res.InvokeTypes(*BOF) and res.InvokeTypes(*EOF)):
648             # We tried .MoveNext() and lots of Fields.Item() calls.
649             # Using GetRows() beats that time by about 2/3.
650             # Inlining GetRows results in a 14% speedup for fetch().
651             data = res.InvokeTypes(*Recordset_GetRows)
652            
653             # Convert cols x rows -> rows x cols
654             data = zip(*data)
655         try:
656             # Close
657             res.InvokeTypes(*Recordset_Close)
658         except:
659             pass
660         conn = None
661        
662         return data, columns
663
664
665 class StorageManagerADO(db.StorageManagerDB):
666     """StoreManager to save and retrieve Units via ADO 2.7.
667     
668     You must run makepy on ADO 2.7 before installing.
669     """
670    
671     databaseclass = ADODatabase
672    
673     def version(self):
674         adoconn = win32com.client.Dispatch(r'ADODB.Connection')
675         v = adoconn.Version
676         adoconn = None
677         return "ADO Version: %s" % v
678
679
680
681 ###########################################################################
682 ##                                                                       ##
683 ##                             SQL Server                                ##
684 ##                                                                       ##
685 ###########################################################################
686
687
688 class ADOSQLDecompiler_SQLServer(ADOSQLDecompiler):
689    
690     def _compare_strings(self, op1, op, op2):
691         # ADO comparison operators for strings are case-insensitive.
692         if op < 6:
693             # Some operations on strings can be emulated with the
694             # Convert function.
695             return ("Convert(binary, %s) %s Convert(binary, %s)" %
696                     (op1, self.sql_cmp_op[op], op2))
697         else:
698             return ADOSQLDecompiler._compare_strings(self, op1, op, op2)
699
700
701 class AdapterToADOSQL_SQLServer(db.AdapterToSQL):
702    
703     encoding = 'ISO-8859-1'
704    
705     escapes = [("'", "''")]
706     like_escapes = [("[", "[[]"), ("%", "[%]"), ("_", "[_]"),
707                     ("?", "[?]"), ("#", "[#]")]
708    
709     # These are not the same as coerce_bool_to_any (which is used on one side of
710     # a comparison). Instead, these are used when the whole (sub)expression
711     # is True or False, e.g. "WHERE TRUE", or "WHERE TRUE and 'a'.'b' = 3".
712     bool_true = "(1=1)"
713     bool_false = "(1=0)"
714    
715     def coerce_bool_to_any(self, value):
716         if value:
717             return '1'
718         return '0'
719
720
721 class TypeAdapter_SQLServer(db.TypeAdapter):
722    
723     # Hm. Docs say 38, but I can't seem to get more than 12 working.
724     # They must mean 38 binary digits; math.log(2 ** 38, 10) = 11.4+
725     numeric_max_precision = 12
726     numeric_max_bytes = 6
727    
728     def coerce_bool(self, col):
729         return "BIT"
730    
731     def coerce_datetime_datetime(self, col):
732         return "DATETIME"
733    
734     def coerce_datetime_date(self, col):
735         return "DATETIME"
736    
737     def coerce_datetime_time(self, col):
738         return "DATETIME"
739    
740     def int_type(self, bytes):
741         """Return a datatype which can handle the given number of bytes."""
742         if bytes <= 2:
743             return "SMALLINT"
744         elif bytes <= 4:
745             return "INTEGER"
746         elif bytes <= 8:
747             # BIGINT is usually 8 bytes
748             return "BIGINT"
749         else:
750             # Anything larger than 8 bytes, use decimal/numeric.
751             # For PostgreSQL, "The actual storage requirement is two bytes
752             # for each group of four decimal digits, plus eight bytes
753             # overhead." Note we omit the overhead in our calculation.
754             return "NUMERIC(%s, 0)" % (bytes * 2)
755    
756     def coerce_str(self, col):
757         # The bytes hint does not reflect the usual 4-byte base for varchar.
758         bytes = int(col.hints.get('bytes', 255))
759        
760         if bytes == 0 or bytes > 8000:
761             # Okay, what the @#$%& is wrong with Redmond??!?! We can't even
762             # compare TEXT or NTEXT fields??!? Fine. We'll deny such, and
763             # warn the deployer with less swearing and exclamation points.
764             warnings.warn("You have defined a string property without "
765                           "limiting its length. Microsoft SQL Server does "
766                           "not allow comparisons on string fields larger "
767                           "than 8000 characters. Some of your data may be "
768                           "truncated.", errors.StorageWarning)
769             bytes = 8000
770        
771         # 8000 *bytes* is the absolute upper limit, based on T_SQL docs for
772         # varchar/varbinary. If there are further fields defined for the
773         # class, or the codepage uses a double-byte character set, we still
774         # might exceed the max size (8060) for a record. We could calc the
775         # total requested record size, and adjust accordingly. Meh.
776         return "VARCHAR(%s)" % bytes
777
778
779 class SQLServerColumnSet(ADOColumnSet):
780    
781     def __setitem__(self, key, column):
782         t = self.table
783        
784         dbtype = column.dbtype
785         if column.autoincrement:
786             if dbtype not in ("BOOLEAN", "SMALLINT", "INTEGER", "BIGINT"):
787                 raise ValueError("SQL Server does not allow IDENTITY "
788                                  "columns of type %r" % dbtype)
789             dbtype = "%s IDENTITY(%s, 1) NOT NULL" % (dbtype, column.default)
790         else:
791             default = column.default or ""
792             if default:
793                 default = t.db.adaptertosql.coerce(default, dbtype)
794                 dbtype = "%s DEFAULT %s" % (dbtype, default)
795        
796         t.db.lock("Adding property. Transactions not allowed.")
797         try:
798             # SQL Server doesn't use the "COLUMN" keyword with "ADD"
799             t.db.execute("ALTER TABLE %s ADD %s %s;" %
800                          (t.qname, column.qname, dbtype))
801             dict.__setitem__(self, key, column)
802         finally:
803             t.db.unlock()
804    
805     def _rename(self, oldcol, newcol):
806         t = self.table
807         t.db.execute("EXEC sp_rename '%s.%s', '%s', 'COLUMN'" %
808                      (t.name, oldcol.name, newcol.name))
809
810
811 class SQLServerDatabase(ADODatabase):
812    
813     decompiler = ADOSQLDecompiler_SQLServer
814     columnsetclass = SQLServerColumnSet
815     adaptertosql = AdapterToADOSQL_SQLServer()
816     typeadapter = TypeAdapter_SQLServer()
817    
818     def create_database(self):
819         self.lock("Creating database. Transactions not allowed.")
820         try:
821             # This method hasn't been tested yet for SQL server (only MSDE).
822             adoconn = win32com.client.Dispatch(r'ADODB.Connection')
823             atoms = connatoms(self.Connect)
824             atoms['INITIAL CATALOG'] = "tempdb"
825             adoconn.Open("; ".join(["%s=%s" % (k, v) for k, v in atoms.iteritems()]))
826             adoconn.Execute("CREATE DATABASE %s" % self.qname)
827             adoconn.Close()
828             self.clear()
829         finally:
830             self.unlock()
831    
832     def drop_database(self):
833         self.lock("Dropping database. Transactions not allowed.")
834         try:
835             # Must shut down all connections to avoid
836             # "being accessed by other users" error.
837             self.connection.shutdown()
838            
839             adoconn = win32com.client.Dispatch(r'ADODB.Connection')
840             atoms = connatoms(self.Connect)
841             atoms['INITIAL CATALOG'] = "tempdb"
842             adoconn.Open("; ".join(["%s=%s" % (k, v) for k, v in atoms.iteritems()]))
843             adoconn.Execute("DROP DATABASE %s;" % self.qname)
844             adoconn.Close()
845             self.clear()
846         finally:
847             self.unlock()
848    
849     def __setitem__(self, key, table):
850         if key in self:
851             del self[key]
852        
853         fields = []
854         pk = []
855         for col in table.columns.itervalues():
856             dbtype = col.dbtype
857             if col.autoincrement:
858                 if dbtype not in ("BOOLEAN", "SMALLINT", "INTEGER", "BIGINT"):
859                     raise ValueError("SQL Server does not allow IDENTITY "
860                                      "columns of type %r" % dbtype)
861                 dbtype = "%s IDENTITY(%s, 1) NOT NULL" % (dbtype, col.default)
862             else:
863                 default = col.default or ""
864                 if default:
865                     default = self.adaptertosql.coerce(default, dbtype)
866                     dbtype = "%s DEFAULT %s" % (dbtype, default)
867             fields.append('%s %s' % (col.qname, dbtype))
868            
869             if col.key:
870                 pk.append(col.qname)
871        
872         if pk:
873             pk = ", PRIMARY KEY (%s)" % ", ".join(pk)
874         else:
875             pk = ""
876        
877         self.lock("Creating storage. Transactions not allowed.")
878         try:
879             self.execute('CREATE TABLE %s (%s%s);' %
880                          (table.qname, ", ".join(fields), pk))
881            
882             for index in table.columns.indices.itervalues():
883                 self.execute('CREATE INDEX %s ON %s (%s);' %
884                              (index.qname, table.qname,
885                               self.quote(index.colname)))
886            
887             dict.__setitem__(self, key, table)
888         finally:
889             self.unlock()
890
891
892 class StorageManagerADO_SQLServer(StorageManagerADO):
893    
894     databaseclass = SQLServerDatabase
895    
896     def __init__(self, arena, allOptions={}):
897         atoms = connatoms(allOptions['Connect'])
898         allOptions['name'] = atoms.get('INITIAL CATALOG') or atoms.get('DSN')
899         db.StorageManagerDB.__init__(self, arena, allOptions)
900    
901     def _seq_UnitSequencerInteger(self, unit):
902         """Reserve a unit using the table's AUTOINCREMENT field."""
903         cls = unit.__class__
904         t = self.db[cls.__name__]
905        
906         fields = []
907         values = []
908         for key in cls.properties:
909             col = t.columns[key]
910             if col.autoincrement:
911                 # Skip this field, since we're using IDENTITY
912                 continue
913             val = self.db.adaptertosql.coerce(getattr(unit, key), col.dbtype)
914             fields.append(col.qname)
915             values.append(val)
916        
917         transconn = self.db.get_transaction()
918        
919         fields = ", ".join(fields)
920         values = ", ".join(values)
921         self.db.execute('INSERT INTO %s (%s) VALUES (%s);' %
922                         (t.qname, fields, values), transconn)
923        
924         # Grab the new ID. This is threadsafe because db.reserve has a mutex.
925         # For some reason, using SCOPE_IDENTITY or IDENTITY failed (returned
926         # None) when retrieving ID's just after a 99-thread-test ran. Moving
927         # the multithreading test fixed it. IDENT_CURRENT worked regardless.
928         data, _ = self.db.fetch("SELECT IDENT_CURRENT('%s');" % t.qname,
929                                 transconn)
930         setattr(unit, cls.identifiers[0], data[0][0])
931
932
933
934 ###########################################################################
935 ##                                                                       ##
936 ##                             MS Access                                 ##
937 ##                                                                       ##
938 ###########################################################################
939
940
941 class ADOSQLDecompiler_MSAccess(ADOSQLDecompiler):
942     sql_cmp_op = ('<', '<=', '=', '<>', '>', '>=', 'in', 'not in')
943    
944     def _compare_strings(self, op1, op, op2):
945         # ADO comparison operators for strings are case-insensitive.
946         if op < 6:
947             # Some operations on strings can be emulated with the
948             # StrComp function. Oddly enough, "StrComp(x, y) op 0"
949             # is the same as "x op y" in most cases.
950             return "StrComp(%s, %s) %s 0" % (op1, op2, self.sql_cmp_op[op])
951         else:
952             return ADOSQLDecompiler._compare_strings(self, op1, op, op2)
953    
954     def dejavu_now(self):
955         return "Now()"
956    
957     def dejavu_today(self):
958         return "DateValue(Now())"
959    
960     def dejavu_year(self, x):
961         return "Year(" + x + ")"
962    
963     def dejavu_month(self, x):
964         return "Month(" + x + ")"
965
966     def dejavu_day(self, x):
967         return "Day(" + x + ")"
968
969 class TypeAdapter_MSAccess(db.TypeAdapter):
970    
971     # Hm. Docs say 28/38, but I can't seem to get more than 12 working.
972     numeric_max_precision = 12
973     numeric_max_bytes = 6
974    
975     def coerce_bool(self, col): return "BIT"
976    
977     def coerce_datetime_datetime(self, col): return "DATETIME"
978     def coerce_datetime_date(self, col): return "DATETIME"
979     def coerce_datetime_time(self, col): return "DATETIME"
980    
981     def int_type(self, bytes):
982         if bytes <= 2:
983             return "INTEGER"
984         elif bytes <= 4:
985             return "LONG"
986         else:
987             # Anything larger than 4 bytes, use decimal/numeric.
988             return "DECIMAL"
989    
990     def coerce_str(self, col):
991         # The bytes hint shall not reflect the usual 4-byte base for varchar.
992         bytes = int(col.hints.get('bytes', 255))
993        
994         # 255 chars is the upper limit for TEXT / VARCHAR in MS Access.
995         if bytes == 0 or bytes > 255:
996             # MEMO is 1 GB max when set programatically (only 64K when set
997             # in Access UI). But then, 1 GB is the limit for the whole DB.
998             # Note that OpenSchema will return a DATA_TYPE of "WCHAR".
999             return "MEMO"
1000        
1001         return "VARCHAR(%s)" % bytes
1002
1003
1004
1005 class AdapterToADOSQL_MSAccess(db.AdapterToSQL):
1006     """Coerce Expression constants to ADO SQL."""
1007    
1008     encoding = 'ISO-8859-1'
1009    
1010     escapes = [("'", "''")]
1011     like_escapes = [("[", "[[]"), ("%", "[%]"), ("_", "[_]"),
1012                     ("?", "[?]"), ("#", "[#]")]
1013    
1014     def coerce_datetime_datetime_to_any(self, value):
1015         return ('#%s/%s/%s %02d:%02d:%02d#' %
1016                 (value.month, value.day, value.year,
1017                  value.hour, value.minute, value.second))
1018    
1019     def coerce_datetime_date_to_any(self, value):
1020         return '#%s/%s/%s#' % (value.month, value.day, value.year)
1021    
1022     def coerce_datetime_time_to_any(self, value):
1023         return '#%02d:%02d:%02d#' % (value.hour, value.minute, value.second)
1024
1025
1026 class MSAccessColumnSet(ADOColumnSet):
1027    
1028     def __setitem__(self, key, column):
1029         t = self.table
1030        
1031         dbtype = column.dbtype
1032         if column.autoincrement:
1033             dbtype = "AUTOINCREMENT(%s, 1)" % column.default
1034         else:
1035             default = column.default or ""
1036             if default:
1037                 defspec = t.db.adaptertosql.coerce(default, dbtype)
1038                 if isinstance(default, (int, long)):
1039                     # Crazy quote hack to get a numeric default to work.
1040                     defspec = "'%s'" % defspec
1041                 dbtype = "%s DEFAULT %s" % (dbtype, defspec)
1042        
1043         t.db.lock("Adding property. Transactions not allowed.")
1044         try:
1045             # Microsoft Access doesn't use the "COLUMN" keyword with "ADD"
1046             t.db.execute("ALTER TABLE %s ADD %s %s;" %
1047                          (t.qname, column.qname, dbtype))
1048             dict.__setitem__(self, key, column)
1049         finally:
1050             t.db.unlock()
1051
1052
1053 class MSAccessDatabase(ADODatabase):
1054    
1055     decompiler = ADOSQLDecompiler_MSAccess
1056     adaptertosql = AdapterToADOSQL_MSAccess()
1057     typeadapter = TypeAdapter_MSAccess()
1058    
1059     columnsetclass = MSAccessColumnSet
1060    
1061     poolsize = 0
1062    
1063     def connect(self):
1064         # MS Access can't use a pool, because there doesn't seem
1065         # to be a commit timeout. See http://support.microsoft.com/kb/200300
1066         # for additional synchronization issues.
1067         self.connection = db.SingleConnection(self._get_conn, self._del_conn)
1068    
1069     def _get_columns(self, tablename, conn=None):
1070         cols = ADODatabase._get_columns(self, tablename, conn)
1071         if conn is None:
1072             conn = self.connection()
1073        
1074         try:
1075             # Horrible hack to get autoincrement property
1076             query = "SELECT * FROM %s WHERE FALSE" % self.quote(tablename)
1077             if isinstance(conn, db.ConnectionWrapper):
1078                 # 'conn' is a ConnectionWrapper object, which .Open
1079                 # won't accept. Pass the unwrapped connection instead.
1080                 conn = conn.conn
1081            
1082             # Call conn.Open(query) directly, skipping win32com overhead.
1083             res, rows_affected = conn._oleobj_.InvokeTypes(6, 0, 1, (9, 0),
1084                                             ((8, 1), (16396, 18), (3, 49)),
1085                                             # *args =
1086                                             query, pythoncom.Missing, -1)
1087         except pywintypes.com_error, x:
1088             try:
1089                 res.InvokeTypes(*Recordset_Close)
1090             except:
1091                 pass
1092             res = None
1093             x.args += (query, )
1094             conn = None
1095             # "raise x" here or we could get the traceback of the inner try.
1096             raise x
1097        
1098         resFields = res.InvokeTypes(*Recordset_Fields)
1099         for c in cols:
1100             f = resFields.InvokeTypes(0, 0, 2, (9, 0), ((12, 1),), c.name)
1101             fprops = f.InvokeTypes(*Field_Properties)
1102             fprop = fprops.InvokeTypes(0, 0, 2, (9, 0), ((12, 1), ), "ISAUTOINCREMENT")
1103             c.autoincrement = fprop.InvokeTypes(*Property_Value)
1104        
1105         try:
1106             res.InvokeTypes(*Recordset_Close)
1107         except:
1108             pass
1109         conn = None
1110        
1111         return cols
1112    
1113     def python_type(self, dbtype):
1114         if dbtype == "LONG":
1115             return int
1116         return ADODatabase.python_type(self, dbtype)
1117    
1118     def create_database(self):
1119         self.lock("Creating database. Transactions not allowed.")
1120         try:
1121             # By not providing an Engine Type, it defaults to 5 = Access 2000.
1122             cat = win32com.client.Dispatch(r'ADOX.Catalog')
1123             cat.Create(self.Connect)
1124             cat.ActiveConnection.Close()
1125             self.clear()
1126         finally:
1127             self.unlock()
1128    
1129     def drop_database(self):
1130         self.lock("Dropping database. Transactions not allowed.")
1131         try:
1132             # Must shut down our only connection to avoid
1133             # "Permission denied" error on os.remove call below.
1134             self.connection.shutdown()
1135            
1136             import os
1137             # This should accept relative or absolute paths
1138             if os.path.exists(self.name):
1139                 os.remove(self.name)
1140             self.clear()
1141         finally:
1142             self.unlock()
1143    
1144     def __setitem__(self, key, table):
1145         if key in self:
1146             del self[key]
1147        
1148         fields = []
1149         pk = []
1150         for col in table.columns.itervalues():
1151             dbtype = col.dbtype
1152             if col.autoincrement:
1153                 dbtype = "AUTOINCREMENT(%s, 1)" % col.default
1154             else:
1155                 default = col.default or ""
1156                 if default:
1157                     default = self.adaptertosql.coerce(default, dbtype)
1158                     dbtype = "%s DEFAULT %s" % (dbtype, default)
1159             fields.append('%s %s' % (col.qname, dbtype))
1160            
1161             if col.key:
1162                 pk.append(col.qname)
1163        
1164         if pk:
1165             pk = ", PRIMARY KEY (%s)" % ", ".join(pk)
1166         else:
1167             pk = ""
1168        
1169         self.lock("Creating storage. Transactions not allowed.")
1170         try:
1171             self.execute('CREATE TABLE %s (%s%s);' %
1172                          (table.qname, ", ".join(fields), pk))
1173            
1174             for index in table.columns.indices.itervalues():
1175                 self.execute('CREATE INDEX %s ON %s (%s);' %
1176                              (index.qname, table.qname,
1177                               self.quote(index.colname)))
1178            
1179             dict.__setitem__(self, key, table)
1180         finally:
1181             self.unlock()
1182
1183
1184 class StorageManagerADO_MSAccess(StorageManagerADO):
1185     # Jet Connections and Recordsets are always free-threaded.
1186    
1187     use_asterisk_to_get_all = True
1188     databaseclass = MSAccessDatabase
1189    
1190     def __init__(self, arena, allOptions={}):
1191         atoms = connatoms(allOptions['Connect'])
1192         allOptions['name'] = (atoms.get('DATA SOURCE') or
1193                               atoms.get('DATA SOURCE NAME') or
1194                               atoms.get('DBQ'))
1195         db.StorageManagerDB.__init__(self, arena, allOptions)
1196    
1197     def _seq_UnitSequencerInteger(self, unit):
1198         """Reserve a unit using the table's AUTOINCREMENT field."""
1199         cls = unit.__class__
1200         t = self.db[cls.__name__]
1201        
1202         fields = []
1203         values = []
1204         for key in cls.properties:
1205             col = t.columns[key]
1206             if col.autoincrement:
1207                 # Skip this field, since we're using AUTOINCREMENT
1208                 continue
1209             val = self.db.adaptertosql.coerce(getattr(unit, key), col.dbtype)
1210             fields.append(col.qname)
1211             values.append(val)
1212        
1213         transconn = self.db.get_transaction()
1214        
1215         fields = ", ".join(fields)
1216         values = ", ".join(values)
1217         self.db.execute('INSERT INTO %s (%s) VALUES (%s);' %
1218                         (t.qname, fields, values), transconn)
1219        
1220         # Grab the new ID. This is threadsafe because db.reserve has a mutex.
1221         data, _ = self.db.fetch("SELECT @@IDENTITY;", transconn)
1222         setattr(unit, cls.identifiers[0], data[0][0])
1223    
1224     def _make_column(self, cls, key):
1225         col = StorageManagerADO._make_column(self, cls, key)
1226         if col.dbtype == "MEMO":
1227             for assoc in cls._associations.itervalues():
1228                 if assoc.nearKey == key:
1229                     warnings.warn("Memo fields cannot be used as join keys. "
1230                                   "You should set %s.%s(hints={'bytes': 255})"
1231                                   % (cls.__name__, key), errors.StorageWarning)
1232         return col
1233
1234
1235 def gen_py():
1236     """Auto generate .py support for ADO 2.7+"""
1237     print 'Please wait while support for ADO 2.7+ is verified...'
1238    
1239     # Microsoft ActiveX Data Objects 2.8 Library
1240     result = win32com.client.gencache.EnsureModule('{2A75196C-D9EB-4129-B803-931327F72D5C}', 0, 2, 8)
1241     if result is not None:
1242         return
1243    
1244     # Microsoft ActiveX Data Objects 2.7 Library
1245     result = win32com.client.gencache.EnsureModule('{EF53050B-882E-4776-B643-EDA472E8E3F2}', 0, 2, 7)
1246     if result is not None:
1247         return
1248    
1249     raise ImportError("ADO 2.7 support could not be imported/cached")
1250
1251
1252 if __name__ == '__main__':
1253     gen_py()
Note: See TracBrowser for help on using the browser.