Contact: fumanchu@aminus.org

Log in as guest/geniusql to create tickets

root/trunk/geniusql/adapters.py

Revision 310 (checked in by lakin, 4 months ago)

geniusql - fixing whitespcae

  • Property svn:eol-style set to native
Line 
1 """Adapters from Python to SQL (and back) for the geniusql package.
2
3 Adaptation is tricky because semantic adaptation and (server-specific)
4 syntactic adaptation need to be taken care of for every value in both
5 directions. For example, when we convert a datetime.date to SQL, we
6 must both convert the Python value to a string (for example, of the
7 form '2004-03-01') and apply server-specific formatting (for example,
8 '#2004-03-01#' for Microsoft Access).
9
10 This is an extremely thorny issue and really requires the user to manually
11 form and apply custom adapters completely by hand. However, in the vast
12 majority of cases, a reasonable set of default adapters can be generated
13 by Geniusql. For example, a Column of pytype "datetime.date" can default
14 to a DateAdapter (but an MSAccess_Date adapter if necessary), based
15 entirely on the Python type.
16
17 This is different than each column or expression's dbtype, which should be
18 parameterizable to the hilt so that the user can tweak the defaults easily.
19 That is, the user should be able to write things like:
20
21     col = schema.column(str)
22     col.dbtype.encoding = 'ASCII'
23
24 ...rather than passing all such settings as args to the column() call,
25 or forcing the user to select a DatabaseType subclass with the desired
26 characteristics. In short, each Column.adapter is an object that may be
27 shared among multiple Column (or SQLExpression, etc) objects, whereas
28 each Column.dbtype should be an isolated DatabaseType instance (not
29 shared with any other object).
30 """
31
32 import datetime
33 try:
34     import cPickle as pickle
35 except ImportError:
36     import pickle
37
38 from geniusql import typerefs
39
40
41 import sys
42 # Determine max bytes for int on this system.
43 maxint_bytes = 1
44 while True:
45     # Signed values have half the max of unsigned (hence the "-1").
46     if sys.maxint <= 2 ** ((maxint_bytes * 8) - 1):
47         break
48     maxint_bytes += 1
49 # Determine max binary digits for float on this system. Crude but effective.
50 maxfloat_digits = 2
51 while True:
52     L = (2 ** (maxfloat_digits + 1)) - 1
53     if int(float(L)) != L:
54         break
55     maxfloat_digits += 1
56 del L, sys
57
58
59
60 # ------------------------------- Adapters ------------------------------- #
61
62
63 def localtime_offset():
64     """Return (neg, h, m) representing the offset from UTC to local time.
65
66     neg: If True, the (h, m) values are negative (for example, if the local
67         DST timezone is west of UTC).
68     """
69     import time
70     if time.daylight and time.localtime().tm_isdst:
71         offset = time.altzone
72     else:
73         offset = time.timezone
74     h, m = divmod(abs(offset), 3600)
75     m, s = divmod(m, 60)
76     # "altzone...is negative if the local DST timezone is east of UTC",
77     # which is the opposite of what we want.
78     neg = (offset >= 0)
79     return neg, h, m
80
81
82 class Adapter(object):
83     """Logic to convert Python values to database values.
84
85     Adapters encapsulate all of the logic to express Python values
86     in SQL, and to translate retrieved database values to Python.
87
88     Simple adapters are not difficult to construct (just remember to
89     convert Python None to SQL "NULL"). More complicated adapters can,
90     however, be built. For example, given an existing database schema
91     that stores dates in a VARCHAR field of the form 'YYYYMMDD', you
92     would have to construct a custom Adapter to transform to and from
93     Python datetime.date objects. Although it might be possible to use
94     the default Adapter and do the transformations in Python on your own,
95     that approach would disallow (or cause to fail silently!) many
96     comparisons and binary operations in SQL.
97
98     Therefore, each Adapter possesses its own binary_op and compare_op
99     methods which should return the appropriate SQL. For example:
100
101         return "(CAST %s AS FLOAT) %s %s" % (op1.sql, sqlop, op2.sql)
102
103     This must be performed in the Adapter (as opposed to the DatabaseType
104     or Deparser) in order to support custom transformations like our
105     date example, above:
106
107         sql1 = ("(CASE WHEN NOT ISNULL(%s) THEN "
108                 "(CAST (SUBSTRING(%s, 0, 4) + '-' + "
109                        "SUBSTRING(%s, 4, 2) + '-' + "
110                        "SUBSTRING(%s, 6, 2)) AS DATE)"
111                 " END)"
112                 % op1.sql)
113     """
114
115     def push(self, value, dbtype):
116         """Coerce the given Python value to SQL."""
117         raise NotImplementedError
118
119     def pull(self, value, dbtype):
120         """Coerce the given database value to a Python value."""
121         raise NotImplementedError
122
123     def binary_op(self, op1, op, sqlop, op2):
124         """Return the SQL for a binary operation (or raise TypeError).
125
126         op1 and op2 will be SQLExpression objects.
127         op will be a value from codewalk.binary_repr. Use it to switch
128             based on the operator (since sqlop will be provider-specific).
129         sqlop will be the matching SQL for the given operator.
130         """
131         return "%s %s %s" % (op1.sql, sqlop, op2.sql)
132
133     def compare_op(self, op1, op, sqlop, op2):
134         """Return the SQL for a comparison operation (or raise TypeError).
135
136         op1 and op2 will be SQLExpression objects.
137         op will be a value from opcode.cmp_op. Use it to switch
138             based on the operator (since sqlop will be provider-specific).
139         sqlop will be the matching SQL for the given operator.
140         """
141         return "%s %s %s" % (op1.sql, sqlop, op2.sql)
142
143
144 class LIKE_Mixin(object):
145
146     # Notice these are ordered pairs. Escape \ before introducing new ones.
147     # Values in these two lists should be strings encoded with self.encoding.
148     like_escapes = [("%", r"\%"), ("_", r"\_")]
149
150     def escape_like(self, sql):
151         """Prepare a string value for use in a LIKE comparison."""
152         # Notice we strip leading and trailing quote-marks.
153         sql = sql.strip("'\"")
154         for pat, repl in self.like_escapes:
155             sql = sql.replace(pat, repl)
156         return sql
157
158     def like_op(self, op1, op2, ignore_case=False,
159                 start_only=False, end_only=False):
160         """Return the SQL for 'op1 LIKE op2' (or raise TypeError).
161
162         op1 and op2 will be SQLExpression objects.
163
164         If 'ignore_case' is False (the default), then the LIKE comparison
165         will be performed in a case-sensitive manner; otherwise (if
166         ignore_case is True), the LIKE comparison will be performed in
167         a case-INsensitive manner.
168
169         If 'start_only' is True, then op2 will be matched only at the start
170         of op1. If False (the default), then op2 will be matched anywhere.
171
172         If 'end_only' is True, then op2 will be matched only at the end
173         of op1. If False (the default), then op2 will be matched anywhere.
174
175         If both 'start_only' and 'end_only' are True, then op2 will only
176         match op1 if they are identical.
177         """
178         likeexpr = self.escape_like(op2.sql)
179         if start_only:
180             start = ''
181         else:
182             start = '%'
183         if end_only:
184             end = ''
185         else:
186             end = '%'
187
188         if ignore_case:
189             return "LOWER(" + op1.sql + ") LIKE '" + start + likeexpr.lower() + end + "'"
190         else:
191             return op1.sql + " LIKE '" + start + likeexpr + end + "'"
192
193
194 class bool_to_SQL92BIT(Adapter):
195
196     def push(self, value, dbtype):
197         if value is None:
198             return 'NULL'
199         if value:
200             return '1'
201         return '0'
202
203     def pull(self, value, dbtype):
204         # sqlite 2 will return a string, either '0' or '1';
205         # sqlite 3 will return an int.
206         # This construction should handle both.
207         if value is None:
208             return None
209         return bool(int(value))
210
211
212 class bool_to_SQL99BOOLEAN(Adapter):
213
214     def push(self, value, dbtype):
215         if value is None:
216             return 'NULL'
217         if value:
218             return 'TRUE'
219         return 'FALSE'
220
221     def pull(self, value, dbtype):
222         if value is None:
223             return None
224         if value in ('false', 'False'):
225             return False
226         return bool(value)
227
228
229 # The great thing about these 3 date coercers is that you can use
230 # them with (VAR)CHAR/TEXT columns just as well as with DATETIME, etc.
231 # and comparisons will still work!
232 class datetime_to_SQL92TIMESTAMP(Adapter):
233
234     def push(self, value, dbtype):
235         if value is None:
236             return 'NULL'
237         return ("'%04d-%02d-%02d %02d:%02d:%02d'" %
238                 (value.year, value.month, value.day,
239                  value.hour, value.minute, value.second))
240
241     def pull(self, value, dbtype):
242         if value is None:
243             return None
244         if isinstance(value, datetime.datetime):
245             return value
246         chunks = (value[0:4], value[5:7], value[8:10],
247                   value[11:13], value[14:16], value[17:19],
248                   value[20:26] or 0)
249         return datetime.datetime(*map(int, chunks))
250
251
252 class date_to_SQL92DATE(Adapter):
253
254     def push(self, value, dbtype):
255         if value is None:
256             return 'NULL'
257         return "'%04d-%02d-%02d'" % (value.year, value.month, value.day)
258
259     def pull(self, value, dbtype):
260         if value is None:
261             return None
262         # These are in order for a reason: datetime is a subclass of date!
263         if isinstance(value, datetime.datetime):
264             # Psycopg might do this when adding date + timedelta, for instance.
265             return value.date()
266         elif isinstance(value, datetime.date):
267             return value
268
269         chunks = (value[0:4], value[5:7], value[8:10])
270         return datetime.date(*map(int, chunks))
271
272
273 class time_to_SQL92TIME(Adapter):
274
275     def push(self, value, dbtype):
276         if value is None:
277             return 'NULL'
278         return "'%02d:%02d:%02d'" % (value.hour, value.minute, value.second)
279
280     def pull(self, value, dbtype):
281         if value is None:
282             return None
283         if isinstance(value, datetime.time):
284             return value
285         chunks = (value[0:2], value[3:5], value[6:8])
286         return datetime.time(*map(int, chunks))
287
288
289 class timedelta_to_SQL92DECIMAL(Adapter):
290     """Adapter for storing datetime.timedelta values in whole seconds.
291
292     SQL-92 defines an INTERVAL type, but few commercial databases
293     implement it in a reasonable manner. This adapter stores the
294     value (days * 86400) + seconds in a DECIMAL field instead,
295     which should work with most databases. Note that a custom
296     binary_op method MUST be written for each DB which subclasses
297     this adapter; there is no default because each RDBMS implements
298     date (and especially date interval) arithmetic in its own way.
299
300     This adapter uses whole seconds only to avoid problems many
301     databases exhibit when comparing two FLOATs for equality in SQL.
302     """
303
304     def push(self, value, dbtype):
305         if value is None:
306             return 'NULL'
307         dec_val = (value.days * 86400) + value.seconds
308         return repr(dec_val)
309
310     def pull(self, value, dbtype):
311         if value is None:
312             return None
313         days, seconds = divmod(long(value), 86400)
314         return datetime.timedelta(int(days), int(seconds))
315
316
317 class float_to_SQL92REAL(Adapter):
318     """Adapter from Python float to SQL92-compliant REAL."""
319
320     def push(self, value, dbtype):
321         if value is None:
322             return 'NULL'
323         # Very important we use repr here so we get all 17 decimal digits.
324         return repr(value)
325
326     def pull(self, value, dbtype):
327         if value is None:
328             return None
329         return float(value)
330
331 class float_to_SQL92DOUBLE(float_to_SQL92REAL):
332     """Adapter from Python float to SQL92-compliant DOUBLE."""
333     pass
334
335
336 class int_to_SQL92INTEGER(Adapter):
337
338     # INTEGER is usually 2 bytes.
339     def __init__(self, bytehint=4):
340         if maxint_bytes >= bytehint:
341             self.pytype = int
342         else:
343             self.pytype = long
344
345     def push(self, value, dbtype):
346         if value is None:
347             return 'NULL'
348         return str(value)
349
350     def pull(self, value, dbtype):
351         if value is None:
352             return None
353         return self.pytype(value)
354
355
356 class int_to_SQL92SMALLINT(int_to_SQL92INTEGER):
357
358     # SMALLINT is usually 2 bytes.
359     def __init__(self, bytehint=2):
360         if maxint_bytes >= bytehint:
361             self.pytype = int
362         else:
363             self.pytype = long
364
365
366 class str_to_SQL92VARCHAR(Adapter, LIKE_Mixin):
367
368     # Default escapes for string values.
369     escapes = [("'", "''"), ("\\", r"\\")]
370
371     def push(self, value, dbtype):
372         if value is None:
373             return 'NULL'
374         # This is re-used by unicode_to_SQL92VARCHAR, below
375         if not isinstance(value, str):
376             value = value.encode(dbtype.encoding)
377         for pat, repl in self.escapes:
378             value = value.replace(pat, repl)
379         return "'" + value + "'"
380
381     def pull(self, value, dbtype):
382         if value is None:
383             return None
384         if isinstance(value, unicode):
385             return value.encode(dbtype.encoding)
386         else:
387             return str(value)
388
389
390 class unicode_to_SQL92VARCHAR(str_to_SQL92VARCHAR, LIKE_Mixin):
391
392     def pull(self, value, dbtype):
393         if value is None:
394             return None
395         if isinstance(value, unicode):
396             return value
397         if isinstance(value, (basestring, buffer)):
398             return unicode(value, dbtype.encoding)
399         return unicode(value)
400
401
402 class Pickler(Adapter, LIKE_Mixin):
403
404     # Default escapes for string values.
405     escapes = [("'", "''"), ("\\", r"\\")]
406
407     def push(self, value, dbtype):
408         if value is None:
409             return 'NULL'
410         # dumps with protocol 0 uses the 'raw-unicode-escape' encoding.
411         # We can't use protocol 1 or 2 (which would use UTF-8) because
412         # that introduces null bytes into the SQL, which is a no-no.
413         value = pickle.dumps(value)
414
415         # Now, take pains to re-encode it with dbtype.encoding. As far
416         # as I know, Firebird is the only DB that really needs the value
417         # re-encoded, but the others seem to survive with this step.
418         value = unicode(value, 'raw-unicode-escape').encode(dbtype.encoding)
419
420         for pat, repl in self.escapes:
421             value = value.replace(pat, repl)
422         return "'" + value + "'"
423
424     def pull(self, value, dbtype):
425         if value is None:
426             return None
427         # Coerce to str for pickle.loads restriction.
428         if isinstance(value, unicode):
429             value = value.encode('raw-unicode-escape')
430         else:
431             # Now, take pains to re-decode it with dbtype.encoding. As far
432             # as I know, MySQL is the only DB that really needs the value
433             # re-decoded, but the others seem to survive with this step.
434             value = unicode(value, dbtype.encoding).encode('raw-unicode-escape')
435         return pickle.loads(value)
436
437
438 def normalize_decimal(value):
439     """Return the given decimal value, normalized for SQL.
440
441     Normalization is by stripping trailing zeros after the decimal point.
442     This is critical to allow comparisons between "1", "1.", and "1.0".
443     """
444     value = str(value)
445     if "." in value:
446         value = value.rstrip('0')
447     else:
448         value += "."
449     return "'%s'" % value
450
451
452 class number_to_TEXT(Adapter):
453     """Adapt a numeric Python type (int|long|float) to a TEXT dbtype."""
454
455     def __init__(self, pytype):
456         self.pytype = pytype
457
458     def push(self, value, dbtype):
459         if value is None:
460             return 'NULL'
461         if issubclass(self.pytype, float):
462             return "'%r'" % value
463         return "'%s'" % str(value)
464
465     def pull(self, value, dbtype):
466         if value is None:
467             return None
468         return self.pytype(value)
469
470     def binary_op(self, op1, op, sqlop, op2):
471         """Return the SQL for a binary operation (or raise TypeError).
472
473         op1 and op2 will be SQLExpression objects.
474         op will be a value from codewalk.binary_repr.
475         sqlop will be the matching SQL for the given operator.
476         """
477         raise TypeError("Numbers stored in TEXT columns cannot be operated upon.")
478
479     def compare_op(self, op1, op, sqlop, op2):
480         """Return the SQL for a comparison operation (or raise TypeError).
481
482         op1 and op2 will be SQLExpression objects.
483         op will be a value from opcode.cmp_op.
484         sqlop will be the matching SQL for the given operator.
485         """
486         if sqlop not in ('=', '!='):
487             raise TypeError("Numbers stored in TEXT columns cannot be "
488                             "compared except for (in)equality.")
489         if op1.value is None:
490             val1 = op1.sql
491         else:
492             val1 = "'%s'" % op1.value
493         if op2.value is None:
494             val2 = op2.sql
495         else:
496             val2 = "'%s'" % op2.value
497         return "%s %s %s" % (val1, sqlop, val2)
498
499
500 class number_to_SQL92DECIMAL(Adapter):
501     """Adapt a numeric Python type (int|long|float) to SQL92DECIMAL."""
502
503     def __init__(self, pytype):
504         self.pytype = pytype
505
506     def push(self, value, dbtype):
507         if value is None:
508             return 'NULL'
509         if issubclass(self.pytype, float):
510             # Make sure we get all 17 decimal digits.
511             return "'" + repr(value) + "'"
512         return str(value)
513
514     def pull(self, value, dbtype):
515         if value is None:
516             return None
517         return self.pytype(value)
518
519
520 if typerefs.decimal:
521     class decimal_to_SQL92DECIMAL(Adapter):
522         def push(self, value, dbtype):
523             if value is None:
524                 return 'NULL'
525             return str(value)
526
527         if hasattr(typerefs.decimal, "Decimal"):
528             _decimal_type = typerefs.decimal.Decimal
529         else:
530             _decimal_type = typerefs.decimal
531
532         def pull(self, value, dbtype):
533             if value is None:
534                 return None
535             # pywin32 build 205 began support for returning
536             # COM Currency objects as decimal objects.
537             # See http://pywin32.cvs.sourceforge.net/pywin32/pywin32/CHANGES.txt?view=markup
538             if not isinstance(value, self._decimal_type):
539                 return self._decimal_type(str(value))
540             return value
541
542     class decimal_to_TEXT(decimal_to_SQL92DECIMAL):
543         def push(self, value, dbtype):
544             if value is None:
545                 return 'NULL'
546             return normalize_decimal(value)
547
548         def binary_op(self, op1, op, sqlop, op2):
549             raise TypeError("Numbers stored in TEXT columns cannot be operated upon.")
550
551         def compare_op(self, op1, op, sqlop, op2):
552             if sqlop not in ('=', '!='):
553                 raise TypeError("Numbers stored in TEXT columns cannot be "
554                                 "compared except for (in)equality.")
555             if op1.value is None:
556                 val1 = op1.sql
557             else:
558                 val1 = normalize_decimal(op1.value)
559             if op2.value is None:
560                 val2 = op2.sql
561             else:
562                 val2 = normalize_decimal(op2.value)
563             return "%s %s %s" % (val1, sqlop, val2)
564
565     class decimal_to_SQL92REAL(Adapter):
566         """Adapter from Python decimal to SQL92-compliant REAL."""
567
568         def push(self, value, dbtype):
569             if value is None:
570                 return 'NULL'
571             return str(value)
572
573         if hasattr(typerefs.decimal, "Decimal"):
574             _decimal_type = typerefs.decimal.Decimal
575         else:
576             _decimal_type = typerefs.decimal
577
578         def pull(self, value, dbtype):
579             if value is None:
580                 return None
581             if isinstance(value, float):
582                 value = repr(value)
583             return self._decimal_type(value)
584
585     class decimal_to_SQL92DOUBLE(decimal_to_SQL92REAL):
586         """Adapter from Python decimal to SQL92-compliant DOUBLE."""
587         pass
588
589
590 if typerefs.fixedpoint:
591     class fixedpoint_to_SQL92DECIMAL(Adapter):
592         def push(self, value, dbtype):
593             if value is None:
594                 return 'NULL'
595             return str(value)
596         def pull(self, value, dbtype):
597             if value is None:
598                 return None
599             if (isinstance(value, basestring) or
600                 (typerefs.decimal and
601                  isinstance(value, typerefs.decimal.Decimal))):
602                 # Unicode really screws up fixedpoint; for example:
603                 # >>> fixedpoint.FixedPoint(u'111111111111111111111111111.1')
604                 # FixedPoint('111111111111111104952008704.00', 2)
605                 value = str(value)
606
607                 scale = 0
608                 atoms = value.rsplit(".", 1)
609                 if len(atoms) > 1:
610                     scale = len(atoms[-1])
611                 return typerefs.fixedpoint.FixedPoint(value, scale)
612             else:
613                 return typerefs.fixedpoint.FixedPoint(value)
614
615     class fixedpoint_to_TEXT(fixedpoint_to_SQL92DECIMAL):
616         def push(self, value, dbtype):
617             if value is None:
618                 return 'NULL'
619             if not isinstance(value, typerefs.fixedpoint.FixedPoint):
620                 value = typerefs.fixedpoint.FixedPoint(value)
621             return normalize_decimal(value)
622
623         def binary_op(self, op1, op, sqlop, op2):
624             raise TypeError("Numbers stored in TEXT columns cannot be operated upon.")
625
626         def compare_op(self, op1, op, sqlop, op2):
627             if sqlop not in ('=', '!='):
628                 raise TypeError("Numbers stored in TEXT columns cannot be "
629                                 "compared except for (in)equality.")
630             if op1.value is None:
631                 val1 = op1.sql
632             else:
633                 val1 = normalize_decimal(op1.value)
634             if op2.value is None:
635                 val2 = op2.sql
636             else:
637                 val2 = normalize_decimal(op2.value)
638             return "%s %s %s" % (val1, sqlop, val2)
639
640     class fixedpoint_to_SQL92REAL(Adapter):
641         """Adapter from Python fixedpoint to SQL92-compliant REAL."""
642
643         def push(self, value, dbtype):
644             if value is None:
645                 return 'NULL'
646             return str(value)
647
648         def pull(self, value, dbtype):
649             if value is None:
650                 return None
651             if isinstance(value, float):
652                 value = repr(value)
653             return typerefs.fixedpoint.FixedPoint(value)
654
655     class fixedpoint_to_SQL92DOUBLE(fixedpoint_to_SQL92REAL):
656         """Adapter from Python fixedpoint to SQL92-compliant DOUBLE."""
657         pass
658
Note: See TracBrowser for help on using the browser.