Contact: fumanchu@aminus.org

Log in as guest/geniusql to create tickets

root/trunk/geniusql/providers/mysql.py

Revision 307 (checked in by lakin, 7 months ago)

fixing the mysql tests. Default to InnoDB so we have transactions and actually deal with unicode.

  • Property svn:eol-style set to native
Line 
1 """
2 Uses the MySQLdb package at:
3 http://sourceforge.net/projects/mysql-python
4
5 From the MySQL manual:
6
7 "If the server SQL mode has ANSI_QUOTES enabled, string literals can be
8 quoted only with single quotes. A string quoted with double quotes will be
9 interpreted as an identifier."
10
11 So use single quotes throughout.
12 """
13
14 # Use _mysql directly to avoid all of the DB-API overhead.
15 # MySQLdb should have installed _mysql.pyd (Windows) or _mysql.so (Unix)
16 # into your site-packages directory.
17 import _mysql
18 import datetime
19
20 import geniusql
21 from geniusql import adapters, dbtypes, conns, deparse, errors, providers, typerefs
22 from geniusql.sqlwriters import INSERT, InsertWriter
23
24
25
26 # ------------------------------ Adapters ------------------------------ #
27
28
29 class MySQL_LIKE_Mixin411(object):
30    
31     like_escapes = [("%", r"\%"), ("_", r"\_")]
32    
33     def escape_like(self, sql):
34         """Prepare a string value for use in a LIKE comparison."""
35         # Notice we strip leading and trailing quote-marks.
36         sql = sql.strip("'\"")
37         for pat, repl in self.like_escapes:
38             sql = sql.replace(pat, repl)
39         return sql
40    
41     def like_op(self, op1, op2, ignore_case=False,
42                 start_only=False, end_only=False):
43         """Return the SQL for 'op1 LIKE op2' (or raise TypeError).
44         
45         op1 and op2 will be SQLExpression objects.
46         
47         If 'ignore_case' is False (the default), then the LIKE comparison
48         will be performed in a case-sensitive manner; otherwise (if
49         ignore_case is True), the LIKE comparison will be performed in
50         a case-INsensitive manner.
51         
52         If 'start_only' is True, then op2 will be matched only at the start
53         of op1. If False (the default), then op2 will be matched anywhere.
54         
55         If 'end_only' is True, then op2 will be matched only at the end
56         of op1. If False (the default), then op2 will be matched anywhere.
57         
58         If both 'start_only' and 'end_only' are True, then op2 will only
59         match op1 if they are identical.
60         """
61         likeexpr = self.escape_like(op2.sql)
62         if start_only:
63             start = ''
64         else:
65             start = '%'
66         if end_only:
67             end = ''
68         else:
69             end = '%'
70        
71         if ignore_case:
72             return ("CONVERT(" + op1.sql + " USING utf8) LIKE '" +
73                     start + likeexpr.lower() + end + "'")
74         else:
75             return op1.sql + " LIKE '" + start + likeexpr + end + "'"
76
77
78
79 class MySQL_String_Mixin(object):
80     like_escapes = [("%", r"\%"), ("_", r"\_")]
81    
82     def push(self, value, dbtype):
83         if value is None:
84             return 'NULL'
85         if not isinstance(value, str):
86             value = value.encode(dbtype.encoding)
87         return "'" + _mysql.escape_string(value) + "'"
88
89
90 class MySQL_str_to_VARCHAR(MySQL_String_Mixin, adapters.str_to_SQL92VARCHAR):
91     pass
92
93 class MySQL_unicode_to_VARCHAR(MySQL_String_Mixin, adapters.unicode_to_SQL92VARCHAR):
94     pass
95
96 class MySQL_str_to_VARCHAR411(MySQL_LIKE_Mixin411, MySQL_str_to_VARCHAR):
97     pass
98
99 class MySQL_unicode_to_VARCHAR411(MySQL_LIKE_Mixin411, MySQL_unicode_to_VARCHAR):
100     pass
101
102 class MySQL_str_to_CHAR(MySQL_String_Mixin, adapters.str_to_SQL92VARCHAR):
103     pass
104
105 class MySQL_unicode_to_CHAR(MySQL_String_Mixin, adapters.unicode_to_SQL92VARCHAR):
106     pass
107
108 class MySQL_str_to_CHAR411(MySQL_LIKE_Mixin411, MySQL_str_to_CHAR):
109     pass
110
111 class MySQL_unicode_to_CHAR411(MySQL_LIKE_Mixin411, MySQL_unicode_to_CHAR):
112     pass
113
114
115 class MySQL_float_adapter(adapters.float_to_SQL92DOUBLE):
116    
117     def compare_op(self, op1, op, sqlop, op2):
118         if op2.dbtype in (FLOAT, DOUBLE):
119             # MySQL provides no reliable method to compare floats in SQL.
120             # Raising TypeError will tell the SQL deparser to mark float
121             # comparisons as imperfect (so they'll be done in Python).
122             raise TypeError("MySQL cannot reliably compare floats: %s" % sql)
123         raise TypeError("unsupported operand type(s) for %s: "
124                         "%r and %r" % (op, op1.pytype, op2.pytype))
125
126
127 def DAY_SECOND(td):
128     """Return "INTERVAL 'D H:M:S' DAY_SECOND" from the given timedelta."""
129     # I figured DAY_SECOND would be best for avoiding
130     # overflows, but I really don't know.
131     h, m = divmod(td.seconds, 3600)
132     m, s = divmod(m, 60)
133     return "INTERVAL '%s %s:%s:%s' DAY_SECOND" % (td.days, h, m, s)
134
135
136 class MySQL_datetime_to_DATETIME(adapters.datetime_to_SQL92TIMESTAMP):
137    
138     def binary_op(self, op1, op, sqlop, op2):
139         if op2.pytype is datetime.date:
140             if op == "-":
141                 # Assume NUMERIC secs (default for datetime.timedelta)
142                 # The MySQL docs say, "TIMEDIFF() returns expr1 - expr2
143                 # expressed as a time value", but the "hours" component
144                 # can increase arbitrarily (e.g. "23165:38:16").
145                 return "TIME_TO_SEC(TIMEDIFF(%s, %s))" % (op1.sql, op2.sql)
146         elif op2.pytype is datetime.timedelta:
147             if op in ("-", "+"):
148                 return "(%s %s %s)" % (op1.sql, sqlop, DAY_SECOND(op2.value))
149         elif op2.pytype is datetime.datetime:
150             if op == "-":
151                 return ("((DATEDIFF(%s, %s) * 86400) + "
152                         "TIME_TO_SEC(%s) - TIME_TO_SEC(%s))"
153                         % (op1.sql, op2.sql, op1.sql, op2.sql))
154         raise TypeError("unsupported operand type(s) for %s: "
155                         "%r and %r" % (op, op1.pytype, op2.pytype))
156
157
158 class MySQL_date_to_DATE(adapters.date_to_SQL92DATE):
159    
160     def binary_op(self, op1, op, sqlop, op2):
161         if op2.pytype is datetime.date:
162             if op == "-":
163                 # Assume NUMERIC secs (default for datetime.timedelta)
164                 return "(DATEDIFF(%s, %s) * 86400)" % (op1.sql, op2.sql)
165         elif op2.pytype is datetime.timedelta:
166             if op in ("-", "+"):
167                 return "%s %s INTERVAL %s DAY" % (op1.sql, sqlop, op2.value.days)
168         raise TypeError("unsupported operand type(s) for %s: "
169                         "%r and %r" % (op, op1.pytype, op2.pytype))
170
171
172 class MySQL_timedelta_to_DECIMAL(adapters.timedelta_to_SQL92DECIMAL):
173    
174     def binary_op(self, op1, op, sqlop, op2):
175         if op2.pytype is datetime.timedelta:
176             return "%s %s %s" % (op1.sql, sqlop, op2.sql)
177         elif op == "+":
178             if op2.pytype is datetime.datetime:
179                 return "%s + %s" % (DAY_SECOND(op1.value), op2.sql)
180             elif op2.pytype is datetime.date:
181                 return "INTERVAL %s DAY + %s" % (op1.value.days, op2.sql)
182         raise TypeError("unsupported operand type(s) for %s: "
183                         "%r and %r" % (op, op1.pytype, op2.pytype))
184
185
186 # ---------------------------- DatabaseTypes ---------------------------- #
187
188
189 # These are 5.0 types
190
191 # TRUE and FALSE only work with 4.1 or better.
192 # We could use the name "BOOLEAN", but it wasn't introduced until 4.1.0.
193 class BOOL(dbtypes.SQL92BIT):
194     # This is actually a synonym for TINYINT(1)
195     synonyms = ['BOOLEAN']
196
197
198 class TINYINT(dbtypes.SQL92SMALLINT):
199     bytes = max_bytes = 1
200     # MySQL allows TINYINT to be signed or unsigned.
201     signed = True
202
203 class SMALLINT(dbtypes.SQL92SMALLINT):
204     bytes = max_bytes = 2
205
206 class MEDIUMINT(dbtypes.SQL92INTEGER):
207     bytes = max_bytes = 3
208
209 class INT(dbtypes.SQL92INTEGER):
210     synonyms = ['INTEGER']
211
212 class BIGINT(dbtypes.SQL92INTEGER):
213     bytes = max_bytes = 8
214
215
216 class DECIMAL(dbtypes.SQL92DECIMAL):
217     synonyms = ['DECIMAL', 'DEC', 'NUMERIC']
218    
219     default_adapters = dbtypes.SQL92DECIMAL.default_adapters.copy()
220     default_adapters[datetime.timedelta] = MySQL_timedelta_to_DECIMAL()
221    
222     # "Before 3.23.6, precision and scale both must be specified explicitly."
223     _precision = 10
224     max_precision = 16
225    
226     # DECIMAL_MAX_SCALE is 30 in every copy of MySQL 5 I can find.
227     # Not sure what the limits are in older versions.
228     max_scale = 30
229    
230     def ddl(self):
231         """Return the type for use in CREATE or ALTER statements."""
232         if self.precision is not None:
233             if self.scale is not None:
234                 return "DECIMAL(%s, %s)" % (self.precision, self.scale)
235             return "DECIMAL(%s)" % self.precision
236         return "DECIMAL"
237
238 class DECIMAL503(DECIMAL):
239     max_precision = 64
240
241 class DECIMAL505(DECIMAL):
242     max_precision = 65
243
244
245 class FLOAT(dbtypes.SQL92REAL):
246     default_adapters = dbtypes.SQL92REAL.default_adapters.copy()
247     default_adapters[float] = MySQL_float_adapter()
248
249 class DOUBLE(dbtypes.SQL92DOUBLE):
250     synonyms = ['REAL', 'DOUBLE PRECISION']
251     default_adapters = dbtypes.SQL92DOUBLE.default_adapters.copy()
252     default_adapters[float] = MySQL_float_adapter()
253
254
255 class DATETIME(dbtypes.SQL92TIMESTAMP):
256     _min = datetime.datetime(1000, 1, 1)
257     _max = datetime.datetime(9999, 12, 31, 23, 59, 59)
258     default_adapters = dbtypes.SQL92TIMESTAMP.default_adapters.copy()
259     default_adapters[datetime.datetime] = MySQL_datetime_to_DATETIME()
260
261 class DATE(dbtypes.SQL92DATE):
262     _min = datetime.date(1000, 1, 1)
263     _max = datetime.date(9999, 12, 31)
264     default_adapters = dbtypes.SQL92DATE.default_adapters.copy()
265     default_adapters[datetime.date] = MySQL_date_to_DATE()
266
267 class TIME(dbtypes.SQL92TIME):
268     pass
269
270 class TIMESTAMP(dbtypes.SQL92TIMESTAMP):
271     # See http://troels.arvin.dk/db/rdbms/#data_types-date_and_time-timestamp
272     # "MySQL has a type called TIMESTAMP, but it is quite different from the
273     # standard [SQL92] TIMESTAMP: It's a 'magic' data type with side effects
274     # in that it's automatically updated to the current date and time if
275     # some criteria are fulfilled."
276     _min = datetime.datetime(1970, 1, 1, 0, 0, 1)
277     _max = datetime.datetime(2038, 1, 9, 3, 14, 7)
278     default_adapters = dbtypes.SQL92TIMESTAMP.default_adapters.copy()
279     default_adapters[datetime.datetime] = MySQL_datetime_to_DATETIME()
280
281
282
283 class CHAR(dbtypes.SQL92CHAR):
284     variable = False
285     bytes = max_bytes = 255
286     default_adapters = dbtypes.SQL92CHAR.default_adapters.copy()
287     default_adapters[str] = MySQL_str_to_CHAR()
288     default_adapters[unicode] = MySQL_unicode_to_CHAR()
289    
290     def ddl(self):
291         """Return the type for use in CREATE or ALTER statements."""
292         return "CHAR(%s)" % self.bytes
293
294 class CHAR411(CHAR):
295     synonyms = ['CHAR']
296     default_adapters = CHAR.default_adapters.copy()
297     default_adapters[str] = MySQL_str_to_CHAR411()
298     default_adapters[unicode] = MySQL_unicode_to_CHAR411()
299
300
301 class VARCHAR(dbtypes.SQL92VARCHAR):
302     synonyms = ['ENUM']
303     variable = True
304     bytes = 255
305     max_bytes = 255
306     default_adapters = dbtypes.SQL92VARCHAR.default_adapters.copy()
307     default_adapters[str] = MySQL_str_to_VARCHAR()
308     default_adapters[unicode] = MySQL_unicode_to_VARCHAR()
309    
310     def ddl(self):
311         """Return the type for use in CREATE or ALTER statements."""
312         return "VARCHAR(%s)" % self.bytes
313
314 class VARCHAR411(VARCHAR):
315     synonyms = ['VARCHAR', 'ENUM']
316     default_adapters = VARCHAR.default_adapters.copy()
317     default_adapters[str] = MySQL_str_to_VARCHAR411()
318     default_adapters[unicode] = MySQL_unicode_to_VARCHAR411()
319
320 class VARCHAR503(VARCHAR411):
321     # "The maximum effective length of a VARCHAR in MySQL 5.0.3 and
322     # later is determined by the maximum row size and the character
323     # set used. The maximum column length is subject to a row size
324     # of 65,532 bytes."
325     max_bytes = 65535
326     synonyms = ['VARCHAR', 'ENUM']
327
328
329 class BINARY(dbtypes.SQL92CHAR):
330     variable = False
331     bytes = max_bytes = 255
332     default_adapters = dbtypes.SQL92VARCHAR.default_adapters.copy()
333     default_adapters[str] = MySQL_str_to_VARCHAR()
334     default_adapters[unicode] = MySQL_unicode_to_VARCHAR()
335    
336     def ddl(self):
337         """Return the type for use in CREATE or ALTER statements."""
338         return "BINARY(%s)" % self.bytes
339
340 class BINARY411(BINARY):
341     synonyms = ['BINARY']
342     default_adapters = BINARY.default_adapters.copy()
343     default_adapters[str] = MySQL_str_to_VARCHAR411()
344     default_adapters[unicode] = MySQL_unicode_to_VARCHAR411()
345
346
347 class VARBINARY(dbtypes.SQL92VARCHAR):
348     variable = True
349     bytes = 255
350     max_bytes = 255
351    
352     def ddl(self):
353         """Return the type for use in CREATE or ALTER statements."""
354         return "VARBINARY(%s)" % self.bytes
355
356 class VARBINARY411(VARBINARY):
357     synonyms = ['VARBINARY']
358     default_adapters = VARBINARY.default_adapters.copy()
359     default_adapters[str] = MySQL_str_to_VARCHAR411()
360     default_adapters[unicode] = MySQL_unicode_to_VARCHAR411()
361
362 class VARBINARY503(VARBINARY411):
363     # "The maximum effective length of a VARCHAR in MySQL 5.0.3 and
364     # later is determined by the maximum row size and the character
365     # set used. The maximum column length is subject to a row size
366     # of 65,532 bytes."
367     max_bytes = 65535
368     synonyms = ['VARBINARY']
369
370
371 class MySQL_TEXT(dbtypes.TEXT):
372     default_adapters = dbtypes.TEXT.default_adapters.copy()
373     default_adapters[str] = MySQL_str_to_VARCHAR()
374     default_adapters[unicode] = MySQL_unicode_to_VARCHAR()
375
376 class MySQL_TEXT411(MySQL_TEXT):
377     default_adapters = MySQL_TEXT.default_adapters.copy()
378     default_adapters[str] = MySQL_str_to_VARCHAR411()
379     default_adapters[unicode] = MySQL_unicode_to_VARCHAR411()
380
381 class TINYBLOB(MySQL_TEXT):
382     bytes = max_bytes = (2 ** 8) - 1
383    
384     def ddl(self):
385         """Return the type for use in CREATE or ALTER statements."""
386         return "TINYBLOB"
387 class TINYBLOB411(MySQL_TEXT411):
388     bytes = max_bytes = (2 ** 8) - 1
389     synonyms = ['TINYBLOB']
390    
391     def ddl(self):
392         """Return the type for use in CREATE or ALTER statements."""
393         return "TINYBLOB"
394
395 class BLOB(MySQL_TEXT):
396     bytes = max_bytes = (2 ** 16) - 1
397    
398     def ddl(self):
399         """Return the type for use in CREATE or ALTER statements."""
400         return "BLOB"
401 class BLOB411(MySQL_TEXT411):
402     bytes = max_bytes = (2 ** 16) - 1
403     synonyms = ['BLOB']
404    
405     def ddl(self):
406         """Return the type for use in CREATE or ALTER statements."""
407         return "BLOB"
408
409 class MEDIUMBLOB(MySQL_TEXT):
410     bytes = max_bytes = (2 ** 24) - 1
411    
412     def ddl(self):
413         """Return the type for use in CREATE or ALTER statements."""
414         return "MEDIUMBLOB"
415 class MEDIUMBLOB411(MySQL_TEXT411):
416     bytes = max_bytes = (2 ** 24) - 1
417     synonyms = ['MEDIUMBLOB']
418    
419     def ddl(self):
420         """Return the type for use in CREATE or ALTER statements."""
421         return "MEDIUMBLOB"
422
423 class LONGBLOB(MySQL_TEXT):
424     bytes = max_bytes = (2 ** 32) - 1
425    
426     def ddl(self):
427         """Return the type for use in CREATE or ALTER statements."""
428         return "LONGBLOB"
429 class LONGBLOB411(MySQL_TEXT411):
430     bytes = max_bytes = (2 ** 32) - 1
431     synonyms = ['LONGBLOB']
432    
433     def ddl(self):
434         """Return the type for use in CREATE or ALTER statements."""
435         return "LONGBLOB"
436
437
438 class TINYTEXT(MySQL_TEXT):
439     bytes = max_bytes = (2 ** 8) - 1
440    
441     def ddl(self):
442         """Return the type for use in CREATE or ALTER statements."""
443         return "TINYTEXT"
444 class TINYTEXT411(MySQL_TEXT411):
445     bytes = max_bytes = (2 ** 8) - 1
446     synonyms = ['TINYTEXT']
447    
448     def ddl(self):
449         """Return the type for use in CREATE or ALTER statements."""
450         return "TINYTEXT"
451
452 class TEXT(MySQL_TEXT):
453     bytes = max_bytes = (2 ** 16) - 1
454    
455     def ddl(self):
456         """Return the type for use in CREATE or ALTER statements."""
457         return "TEXT"
458 class TEXT411(MySQL_TEXT411):
459     bytes = max_bytes = (2 ** 16) - 1
460     synonyms = ['TEXT']
461    
462     def ddl(self):
463         """Return the type for use in CREATE or ALTER statements."""
464         return "TEXT"
465
466 class MEDIUMTEXT(MySQL_TEXT):
467     bytes = max_bytes = (2 ** 24) - 1
468    
469     def ddl(self):
470         """Return the type for use in CREATE or ALTER statements."""
471         return "MEDIUMTEXT"
472 class MEDIUMTEXT411(MySQL_TEXT411):
473     bytes = max_bytes = (2 ** 24) - 1
474     synonyms = ['MEDIUMTEXT']
475    
476     def ddl(self):
477         """Return the type for use in CREATE or ALTER statements."""
478         return "MEDIUMTEXT"
479
480 class LONGTEXT(MySQL_TEXT):
481     bytes = max_bytes = (2 ** 32) - 1
482    
483     def ddl(self):
484         """Return the type for use in CREATE or ALTER statements."""
485         return "LONGTEXT"
486 class LONGTEXT411(MySQL_TEXT411):
487     bytes = max_bytes = (2 ** 32) - 1
488     synonyms = ['LONGTEXT']
489    
490     def ddl(self):
491         """Return the type for use in CREATE or ALTER statements."""
492         return "LONGTEXT"
493
494 # -------------------- MySQL INSERT statements -------------------------- #
495
496 class MySQLINSERT(INSERT):
497     """Provides a different insert statement when no values are provided.
498     """
499    
500     def _get_sql(self):
501         """Return an SQL INSERT statement."""
502         if not self.input:
503             return " ".join(["INSERT INTO", self.fromclause, "VALUES ()"])
504         else:
505             return INSERT._get_sql(self)
506     sql = property(_get_sql, doc="The SQL string for this INSERT statement.")
507
508
509 class MySQLInsertWriter(InsertWriter):
510     """MySQL Database delegate for writing INSERT statements.
511     """
512    
513     statement_class = MySQLINSERT
514
515 class MySQLTypeSet(dbtypes.DatabaseTypeSet):
516    
517     # TRUE and FALSE only work with 4.1 or better.
518     expr_true = "1"
519     expr_false = "0"
520    
521     known_types = {'float': [FLOAT, DOUBLE],
522                    # MySQL VARBINARY/BLOBs will do case-sensitive comparisons.
523                    # They also won't truncate trailing spaces like VARCHAR does.
524                    'varchar': [VARBINARY, TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB],
525                    'char': [BINARY],
526                    'int': [TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT],
527                    'bool': [BOOL],
528                    'datetime': [DATETIME, TIMESTAMP],
529                    'date': [DATE],
530                    'time': [TIME],
531                    'timedelta': [],
532                    'numeric': [DECIMAL],
533                    'other': [CHAR, VARCHAR],
534                    }
535    
536     def __init__(self, version):
537         self.version = version
538        
539         if self.version >= providers.Version("4.1.1"):
540             # TRUE and FALSE only work with 4.1 or better.
541             self.expr_true = "TRUE"
542             self.expr_false = "FALSE"
543         if self.version >= providers.Version("4.1.1"):
544             # Before MySQL 4.1.1, BINARY comparisons could use UPPER()
545             # or LOWER() to perform case-insensitive comparisons. Newer
546             # versions must use CONVERT() to obtain a case-sensitive
547             # encoding, like utf8.
548             self.known_types['varchar'] = [VARBINARY411, TINYBLOB411, BLOB411,
549                                            MEDIUMBLOB411, LONGBLOB411]
550             self.known_types['char'] = [BINARY411]
551             self.known_types['other'] = [CHAR411, VARCHAR411]
552         if self.version >= providers.Version("5.0.3"):
553             self.known_types['numeric'] = [DECIMAL503]
554             self.known_types['other'] = [CHAR411, VARCHAR503]
555         if self.version >= providers.Version("5.0.5"):
556             self.known_types['numeric'] = [DECIMAL505]
557
558
559 class MySQLDeparser(deparse.SQLDeparser):
560    
561     def builtins_today(self):
562         return self.get_expr("CURDATE()", datetime.date)
563
564
565 class MySQLDeparser411(MySQLDeparser):
566    
567     def builtins_icontainedby(self, op1, op2):
568         if op1.value is not None:
569             # Looking for text in a field. Use Like (reverse terms).
570             return self.get_expr(op2.adapter.like_op(
571                 op2, op1, ignore_case=True), bool)
572         else:
573             # Looking for field in (a, b, c).
574             atoms = []
575             for x in op2.value:
576                 adapter = op1.dbtype.default_adapter(type(x))
577                 atoms.append(adapter.push(x, op1.dbtype))
578             return self.get_expr("CONVERT(%s USING utf8) IN (%s)" %
579                                  (op1.sql, ", ".join(atoms)), bool)
580    
581     def builtins_ieq(self, x, y):
582         return self.get_expr("CONVERT(" + x.sql + " USING utf8) = " + y.sql,
583                              bool)
584    
585     def builtins_utcnow(self):
586         return self.get_expr("UTC_TIMESTAMP()", datetime.datetime)
587
588
589 class MySQLIndexSet(geniusql.IndexSet):
590    
591     def __delitem__(self, key):
592         t = self.table
593         # MySQL might rename multiple-column indices to "PRIMARY"
594         for i in t.schema.db._get_indices(t.name):
595             if i.colname == self[key].colname:
596                 t.schema.db.execute_ddl('DROP INDEX %s ON %s;' %
597                                         (i.qname, t.qname))
598
599
600 class MySQLTable(geniusql.Table):
601    
602     def create(self):
603         """Create this table in the database."""
604         db = self.schema.db
605        
606         # Set table.created to True, which should "turn on"
607         # any future ALTER TABLE statements.
608         self.created = True
609        
610         fields = []
611         incr_fields = []
612         pk = []
613         for colkey, col in self.iteritems():
614             fields.append(self.schema.columnclause(col))
615             if col.autoincrement:
616                 if col.initial != 1:
617                     incr_fields.append(col)
618                     if col.initial < 1:
619                         errors.warn("MySQL interprets manually setting an "
620                                     "AUTO_INCREMENT column value to 0 as "
621                                     "'use the next available value in the "
622                                     "sequence'. By setting %s.initial to %r, "
623                                     "there is a slight chance you will "
624                                     "encounter this in the future." %
625                                     (col.name, col.initial))
626            
627             if col.key:
628                 qname = col.qname
629                 dbtype = col.dbtype
630                 if isinstance(dbtype, dbtypes.TEXT):
631                     # MySQL won't allow indexes on a BLOB field without a
632                     # specific index prefix length. We choose 255 just for fun.
633                     qname = "%s(255)" % qname
634                 pk.append(qname)
635        
636         if pk:
637             pk = ", PRIMARY KEY (%s)" % ", ".join(pk)
638         else:
639             pk = ""
640        
641         encoding = db.encoding
642         if encoding:
643             encoding = " CHARACTER SET %s" % encoding
644        
645         engine = db.engine
646         if engine:
647             engine = " ENGINE=%s" % engine
648         db.execute_ddl('CREATE TABLE %s (%s%s)%s%s;' %
649                        (self.qname, ", ".join(fields), pk, engine, encoding))
650        
651         if incr_fields:
652             # Wow, what a hack. We have to INSERT a dummy row to set the
653             # autoincrement initial value(s), and we can't delete it until
654             # after the CREATE INDEX statements (or the counter will revert).
655             fields = ", ".join([col.qname for col in incr_fields])
656             values = ", ".join([str(col.initial - 1) for col in incr_fields])
657             db.execute_ddl("INSERT INTO %s (%s) VALUES (%s);"
658                            % (self.qname, fields, values))
659        
660         for k, index in self.indices.iteritems():
661             dbtype = self[k].dbtype
662             if isinstance(dbtype, dbtypes.TEXT):
663                 # MySQL won't allow indexes on a BLOB field without a
664                 # specific index prefix length. We choose 255 just for fun.
665                 db.execute_ddl('CREATE INDEX %s ON %s (%s(255));' %
666                                (index.qname, self.qname, db.quote(index.colname)))
667             else:
668                 db.execute_ddl('CREATE INDEX %s ON %s (%s);' %
669                                (index.qname, self.qname, db.quote(index.colname)))
670        
671         if incr_fields:
672             db.execute_ddl("DELETE FROM %s" % self.qname)
673    
674     def _rename(self, oldcol, newcol):
675         self.schema.db.execute_ddl("ALTER TABLE %s CHANGE %s %s %s;" %
676                                    (self.qname, oldcol.qname, newcol.qname,
677                                     oldcol.dbtype.ddl()))
678    
679     def _grab_new_ids(self, idkeys, conn):
680         return {idkeys[0]: conn.insert_id()}
681    
682     def drop_primary(self):
683         """Remove any PRIMARY KEY for this Table."""
684         self.schema.db.execute('ALTER TABLE %s DROP PRIMARY KEY;' % self.qname)
685    
686     def set_primary(self):
687         """Set the PRIMARY KEY for this Table."""
688         pk = [column.qname for column in self.itervalues() if column.key]
689         if pk:
690             # For MySQL, we MUST do this in a single statement.
691             self.schema.db.execute("ALTER TABLE %s DROP PRIMARY KEY, "
692                                    "ADD PRIMARY KEY (%s);" %
693                                    (self.qname, ", ".join(pk)))
694         else:
695             self.drop_primary()
696    
697     def insert(self, **kwargs):
698         """Insert a row and return it, including any new identifiers."""
699         # MySQL interprets "INSERT INTO x (ID) VALUES (0)" to mean
700         # "use the next available number in the sequence" if
701         # x is AUTO_INCREMENT.
702         for key, col in self.iteritems():
703             if col.autoincrement and kwargs.get(key, None) == 0:
704                 raise ValueError("MySQL does not allow manually setting an "
705                                  "AUTO_INCREMENT column value to 0.")
706         return geniusql.Table.insert(self, **kwargs)
707
708
709 connargs = ["host", "user", "passwd", "db", "port", "unix_socket",
710             "conv", "connect_time", "compress", "named_pipe",
711             "init_command", "read_default_file", "read_default_group",
712             "cursorclass", "client_flag",
713             ]
714
715 class MySQLConnectionManager(conns.ConnectionManager):
716    
717     # InnoDB default
718     default_isolation = "REPEATABLE READ"
719    
720     def _get_conn(self, master=False):
721         if master:
722             args = self.connargs.copy()
723             args['db'] = 'mysql'
724         else:
725             args = self.connargs
726         try:
727             conn = _mysql.connect(**args)
728             #conn.set_character_set("utf8")
729             if self.initial_sql:
730                 conn.query(self.initial_sql)
731         except _mysql.OperationalError, x:
732             if x.args[0] == 1040:   # Too many connections
733                 raise errors.OutOfConnectionsError
734             raise
735         return conn
736    
737     def _del_conn(self, conn):
738         """Close a connection object."""
739         try:
740             conn.close()
741         except _mysql.ProgrammingError, exc:
742             # ProgrammingError: closing a closed connection
743             if exc.args == ('closing a closed connection',):
744                 pass
745             else:
746                 raise
747    
748     def _start_transaction(self, conn, isolation=None):
749         """Start a transaction."""
750         # http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html
751         # "The default behavior of SET TRANSACTION is to set the
752         # isolation level for the next (not yet started) transaction."
753         # So swap the usual order of statements to execute SET before START.
754         self.isolate(conn, isolation)
755         self.db.execute("START TRANSACTION;", conn)
756
757
758 class MySQLSchema(geniusql.Schema):
759    
760     tableclass = MySQLTable
761     indexsetclass = MySQLIndexSet
762    
763     def columnclause(self, column):
764         """Return a clause for the given column for CREATE or ALTER TABLE.
765         
766         This will be of the form "name type [DEFAULT x] [AUTO_INCREMENT]"
767         """
768         autoincr = ""
769         if column.autoincrement:
770             autoincr = " AUTO_INCREMENT"
771        
772         default = column.default or ""
773         if default:
774             default = column.adapter.pull(default, column.dbtype)
775             default = " DEFAULT %s" % default
776        
777         return "%s %s%s%s" % (column.qname, column.dbtype.ddl(),
778                               default, autoincr)
779    
780     def _get_tables(self, conn=None):
781         data, _ = self.db.fetch("SHOW TABLES FROM %s" % self.db.qname, conn=conn)
782         return [self.tableclass(row[0], self.db.quote(row[0]),
783                                 self, created=True)
784                 for row in data]
785    
786     def _get_table(self, tablename, conn=None):
787         data, _ = self.db.fetch("SHOW TABLES FROM %s LIKE '%s'"
788                              % (self.db.qname, tablename), conn=conn)
789         for row in data:
790             name = row[0]
791             if name == tablename:
792                 return self.tableclass(name, self.db.quote(name),
793                                        self, created=True)
794         raise errors.MappingError("Table %r not found." % tablename)
795    
796     def _get_columns(self, table, conn=None):
797         # cols are: Field, Type, Null, Key, Default, Extra.
798         # See http://dev.mysql.com/doc/refman/4.1/en/describe.html
799         data, _ = self.db.fetch("SHOW COLUMNS FROM %s.%s" %
800                                 (self.db.qname, table.qname), conn=conn)
801         cols = []
802         for row in data:
803             hints = {}
804             dbtypename = row[1].upper()
805             atoms = dbtypename.split("(", 1)
806            
807             dbtype = self.db.typeset.canonicalize(atoms.pop(0))()
808            
809             if atoms:
810                 args = atoms[0][:-1]
811                 if isinstance(dbtype, DECIMAL):
812                     args = [x.strip() for x in args.split(",")]
813                     dbtype.precision, dbtype.scale = map(int, args)
814                 elif isinstance(dbtype, VARCHAR):
815                     # This is an ENUM. Strip quotes.
816                     args = [x.strip().strip("'\"") for x in args.split(",")]
817                     dbtype.enum_values = args
818                     dbtype.bytes = 1
819                     if len(args) > 255:
820                         dbtype.bytes = 2
821                 else:
822                     dbtype.bytes = int(args)
823            
824             key = (row[3] == "PRI")
825             pytype = dbtype.default_pytype
826             col = geniusql.Column(pytype, dbtype, None, key,
827                                   name=row[0], qname=self.db.quote(row[0]))
828             col.adapter = dbtype.default_adapter(col.pytype)
829            
830             if row[4]:
831                 col.default = col.adapter.pull(row[4], col.dbtype)
832             if "auto_increment" in row[5].lower():
833                 col.autoincrement = True
834            
835             cols.append(col)
836         return cols
837    
838     def _get_indices(self, table, conn=None):
839         indices = []
840         try:
841             # cols are: Table, Non_unique, Key_name, Seq_in_index, Column_name,
842             # Collation, Cardinality, Sub_part, Packed, Null, Index_type, Comment
843             data, _ = self.db.fetch("SHOW INDEX FROM %s.%s"
844                                     % (self.db.qname, table.qname), conn=conn)
845         except _mysql.ProgrammingError, x:
846             if x.args[0] != 1146:
847                 raise
848         else:
849             for row in data:
850                 i = geniusql.Index(row[2], self.db.quote(row[2]),
851                                    row[0], row[4], not row[1])
852                 indices.append(i)
853         return indices
854
855
856 class MySQLDatabase(geniusql.Database):
857    
858     sql_name_max_length = 64
859     # MySQL uses case-sensitive database and table names on Unix, but
860     # not on Windows. Use all-lowercase identifiers to work around the
861     # problem. "Column names, index names, and column aliases are not
862     # case sensitive on any platform."
863     # If deployers set lower_case_table_names to 1, it would help.
864     sql_name_caseless = True
865     encoding = "utf8"
866
867     # The default engine for dejavu with mysql is InnoDB.
868     engine = "InnoDB"
869    
870     connectionmanager = MySQLConnectionManager
871     schemaclass = MySQLSchema
872
873     insertwriter = MySQLInsertWriter
874    
875     def __init__(self, **kwargs):
876         kwargs['name'] = kwargs['db']
877         geniusql.Database.__init__(self, **kwargs)
878        
879         self.connections.connargs = dict([(k, v) for k, v in kwargs.iteritems()
880                                           if k in connargs])
881        
882         self.deparser = MySQLDeparser
883        
884         # Get the version string from MySQL, to see if we need
885         # a different deparser.
886         conn = self.connections._get_conn(master=True)
887         rowdata, _ = self.fetch("SELECT version();", conn)
888         conn.close()
889         v = rowdata[0][0]
890         self._version = providers.Version(v)
891        
892         # deparser
893         if self._version > providers.Version("4.1.1"):
894             self.deparser = MySQLDeparser411
895        
896         self.typeset = MySQLTypeSet(self._version)
897    
898     def version(self):
899         return "MySQL Version: %s\nMySQLdb Version: %s" % (self._version, _mysql.version_info)
900    
901     def quote(self, name):
902         """Return name, quoted for use in an SQL statement."""
903         return '`' + name.replace('`', '``') + '`'
904    
905     def is_connection_error(self, exc):
906         """If the given exception instance is a connection error, return True.
907         
908         This should return True for errors which arise from broken connections;
909         for example, if the database server has dropped the connection socket,
910         or is unreachable.
911         """
912         if isinstance(exc, _mysql.OperationalError):
913             # OperationalError: (2006, 'MySQL server has gone away')
914             return exc.args[0] == 2006
915         return False
916    
917     def execute(self, sql, conn=None):
918         """Return a native response for the given SQL."""
919         try:
920             return geniusql.Database.execute(self, sql, conn=conn)
921         except _mysql.OperationalError, x:
922             if x.args[0] == 1030 and x.args[1] == 'Got error 139 from storage engine':
923                 raise ValueError("row length exceeds 8000 byte limit")
924             raise
925    
926     def fetch(self, sql, conn=None):
927         """Return rowdata, columns(name, type) for the given sql.
928         
929         sql should be a SQL string.
930         
931         rowdata will be an iterable of iterables containing the result values.
932         columns will be an iterable of (column name, data type) pairs.
933         """
934         if conn is None:
935             conn = self.connections.get()
936         self.execute(sql, conn)
937        
938         # store_result uses a client-side cursor
939         res = conn.store_result()
940        
941         # The Python MySQLdb library swallows lock timeouts and returns []
942         # (for example, when deadlocked during a SERIALIZABLE transaction).
943         # Raise an error instead.
944         # Oddly, although the deadlock will stall the conn.query() call,
945         # the error message is only available after store_result().
946         err = conn.error()
947         if err == "Lock wait timeout exceeded; try restarting transaction":
948             raise _mysql.OperationalError(1205, err)
949        
950         if res is None:
951             return [], []
952         return res.fetch_row(0, 0), res.describe()
953    
954     def is_timeout_error(self, exc):
955         # OperationalError: (1205, 'Lock wait timeout exceeded; try restarting transaction')
956         if not isinstance(exc, _mysql.OperationalError):
957             return False
958         return exc.args[0] == 1205
959    
960     def create(self):
961         # _mysql has create_db and drop_db commands, but they're deprecated.
962         encoding = self.encoding
963         if encoding:
964             encoding = " CHARACTER SET %s" % encoding
965         sql = 'CREATE DATABASE %s%s;' % (self.qname, encoding)
966         conn = self.connections._get_conn(master=True)
967         self.execute_ddl(sql, conn)
968         conn.close()
969    
970     def exists(self):
971         """Return True if this database has been created, False otherwise."""
972         sql = ("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA "
973                "WHERE SCHEMA_NAME = '%s';" % self.sql_name(self.name))
974         conn = self.connections._get_conn(master=True)
975         data, _ = self.fetch(sql, conn)
976         conn.close()
977         return bool(data)
978    
979     def drop(self):
980         conn = self.connections._get_conn(master=True)
981         try:
982             try:
983                 self.execute_ddl('DROP DATABASE %s;' % self.qname, conn)
984             except _mysql.OperationalError, x:
985                 # OperationalError: (1008, "Can't drop database
986                 # 'dejavu_test'; database doesn't exist")
987                 if x.args[0] == 1008:
988                     raise errors.MappingError(x.args[1])
989         finally:
990             conn.close()
991
Note: See TracBrowser for help on using the browser.