be07b8706158fc4c6f854f90ea158c1fb469682f
1 #
2 # Copyright (c) 2003 Martynas Sklyzmantas, Andrey Lebedev <andrey@micro.lt>
3 #
4 # This module is free software, and you may redistribute it and/or modify
5 # under the same terms as Python, so long as this copyright message and
6 # disclaimer are retained in their original form.
7 #
9 '''This module defines a backend implementation for MySQL.
12 How to implement AUTO_INCREMENT:
14 mysql> create table foo (num integer auto_increment primary key, name
15 varchar(255)) AUTO_INCREMENT=1 type=InnoDB;
17 ql> insert into foo (name) values ('foo5');
18 Query OK, 1 row affected (0.00 sec)
20 mysql> SELECT num FROM foo WHERE num IS NULL;
21 +-----+
22 | num |
23 +-----+
24 | 4 |
25 +-----+
26 1 row in set (0.00 sec)
28 mysql> SELECT num FROM foo WHERE num IS NULL;
29 Empty set (0.00 sec)
31 NOTE: we don't need an index on the id column if it's PRIMARY KEY
33 '''
34 __docformat__ = 'restructuredtext'
36 from roundup.backends.rdbms_common import *
37 from roundup.backends import rdbms_common
38 import MySQLdb
39 import os, shutil
40 from MySQLdb.constants import ER
43 def db_nuke(config):
44 """Clear all database contents and drop database itself"""
45 if db_exists(config):
46 conn = MySQLdb.connect(config.MYSQL_DBHOST, config.MYSQL_DBUSER,
47 config.MYSQL_DBPASSWORD)
48 try:
49 conn.select_db(config.MYSQL_DBNAME)
50 except:
51 # no, it doesn't exist
52 pass
53 else:
54 cursor = conn.cursor()
55 cursor.execute("SHOW TABLES")
56 tables = cursor.fetchall()
57 for table in tables:
58 if __debug__:
59 print >>hyperdb.DEBUG, 'DROP TABLE %s'%table[0]
60 cursor.execute("DROP TABLE %s"%table[0])
61 if __debug__:
62 print >>hyperdb.DEBUG, "DROP DATABASE %s"%config.MYSQL_DBNAME
63 cursor.execute("DROP DATABASE %s"%config.MYSQL_DBNAME)
64 conn.commit()
65 conn.close()
67 if os.path.exists(config.DATABASE):
68 shutil.rmtree(config.DATABASE)
70 def db_create(config):
71 """Create the database."""
72 conn = MySQLdb.connect(config.MYSQL_DBHOST, config.MYSQL_DBUSER,
73 config.MYSQL_DBPASSWORD)
74 cursor = conn.cursor()
75 if __debug__:
76 print >>hyperdb.DEBUG, "CREATE DATABASE %s"%config.MYSQL_DBNAME
77 cursor.execute("CREATE DATABASE %s"%config.MYSQL_DBNAME)
78 conn.commit()
79 conn.close()
81 def db_exists(config):
82 """Check if database already exists."""
83 conn = MySQLdb.connect(config.MYSQL_DBHOST, config.MYSQL_DBUSER,
84 config.MYSQL_DBPASSWORD)
85 # tables = None
86 try:
87 try:
88 conn.select_db(config.MYSQL_DBNAME)
89 # cursor = conn.cursor()
90 # cursor.execute("SHOW TABLES")
91 # tables = cursor.fetchall()
92 # if __debug__:
93 # print >>hyperdb.DEBUG, "tables %s"%(tables,)
94 except MySQLdb.OperationalError:
95 if __debug__:
96 print >>hyperdb.DEBUG, "no database '%s'"%config.MYSQL_DBNAME
97 return 0
98 finally:
99 conn.close()
100 if __debug__:
101 print >>hyperdb.DEBUG, "database '%s' exists"%config.MYSQL_DBNAME
102 return 1
105 class Database(Database):
106 arg = '%s'
108 # Backend for MySQL to use.
109 # InnoDB is faster, but if you're running <4.0.16 then you'll need to
110 # use BDB to pass all unit tests.
111 mysql_backend = 'InnoDB'
112 #mysql_backend = 'BDB'
114 hyperdb_to_sql_datatypes = {
115 hyperdb.String : 'VARCHAR(255)',
116 hyperdb.Date : 'DATETIME',
117 hyperdb.Link : 'INTEGER',
118 hyperdb.Interval : 'VARCHAR(255)',
119 hyperdb.Password : 'VARCHAR(255)',
120 hyperdb.Boolean : 'BOOL',
121 hyperdb.Number : 'REAL',
122 }
124 hyperdb_to_sql_value = {
125 hyperdb.String : str,
126 # no fractional seconds for MySQL
127 hyperdb.Date : lambda x: x.formal(sep=' '),
128 hyperdb.Link : int,
129 hyperdb.Interval : lambda x: x.serialise(),
130 hyperdb.Password : str,
131 hyperdb.Boolean : int,
132 hyperdb.Number : lambda x: x,
133 }
135 def sql_open_connection(self):
136 db = getattr(self.config, 'MYSQL_DATABASE')
137 try:
138 conn = MySQLdb.connect(*db)
139 except MySQLdb.OperationalError, message:
140 raise DatabaseError, message
141 cursor = conn.cursor()
142 cursor.execute("SET AUTOCOMMIT=0")
143 cursor.execute("BEGIN")
144 return (conn, cursor)
146 def open_connection(self):
147 # make sure the database actually exists
148 if not db_exists(self.config):
149 db_create(self.config)
151 self.conn, self.cursor = self.sql_open_connection()
153 try:
154 self.load_dbschema()
155 except MySQLdb.OperationalError, message:
156 if message[0] != ER.NO_DB_ERROR:
157 raise
158 except MySQLdb.ProgrammingError, message:
159 if message[0] != ER.NO_SUCH_TABLE:
160 raise DatabaseError, message
161 self.init_dbschema()
162 self.sql("CREATE TABLE schema (schema TEXT) TYPE=%s"%
163 self.mysql_backend)
164 self.cursor.execute('''CREATE TABLE ids (name VARCHAR(255),
165 num INTEGER) TYPE=%s'''%self.mysql_backend)
166 self.cursor.execute('create index ids_name_idx on ids(name)')
167 self.create_version_2_tables()
169 def create_version_2_tables(self):
170 # OTK store
171 self.cursor.execute('''CREATE TABLE otks (otk_key VARCHAR(255),
172 otk_value VARCHAR(255), otk_time FLOAT(20))
173 TYPE=%s'''%self.mysql_backend)
174 self.cursor.execute('CREATE INDEX otks_key_idx ON otks(otk_key)')
176 # Sessions store
177 self.cursor.execute('''CREATE TABLE sessions (
178 session_key VARCHAR(255), session_time FLOAT(20),
179 session_value VARCHAR(255)) TYPE=%s'''%self.mysql_backend)
180 self.cursor.execute('''CREATE INDEX sessions_key_idx ON
181 sessions(session_key)''')
183 # full-text indexing store
184 self.cursor.execute('''CREATE TABLE __textids (_class VARCHAR(255),
185 _itemid VARCHAR(255), _prop VARCHAR(255), _textid INT)
186 TYPE=%s'''%self.mysql_backend)
187 self.cursor.execute('''CREATE TABLE __words (_word VARCHAR(30),
188 _textid INT) TYPE=%s'''%self.mysql_backend)
189 self.cursor.execute('CREATE INDEX words_word_ids ON __words(_word)')
190 sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
191 self.cursor.execute(sql, ('__textids', 1))
193 def add_actor_column(self):
194 '''While we're adding the actor column, we need to update the
195 tables to have the correct datatypes.'''
196 for klass in self.classes.values():
197 cn = klass.classname
198 properties = klass.getprops()
199 old_spec = self.database_schema['tables'][cn]
201 execute = self.cursor.execute
203 # figure the non-Multilink properties to copy over
204 propnames = ['activity', 'creation', 'creator']
206 # figure actions based on data type
207 for name, s_prop in old_spec[1]:
208 # s_prop is a repr() string of a hyperdb type object
209 if s_prop.find('Multilink') == -1:
210 if properties.has_key(name):
211 propnames.append(name)
212 continue
213 tn = '%s_%s'%(cn, name)
215 if properties.has_key(name):
216 # grabe the current values
217 sql = 'select linkid, nodeid from %s'%tn
218 if __debug__:
219 print >>hyperdb.DEBUG, 'migration', (self, sql)
220 execute(sql)
221 rows = self.cursor.fetchall()
223 # drop the old table
224 self.drop_multilink_table_indexes(cn, name)
225 sql = 'drop table %s'%tn
226 if __debug__:
227 print >>hyperdb.DEBUG, 'migration', (self, sql)
228 execute(sql)
230 if properties.has_key(name):
231 # re-create and populate the new table
232 self.create_multilink_table(klass, name)
233 sql = '''insert into %s (linkid, nodeid) values
234 (%s, %s)'''%(tn, self.arg, self.arg)
235 for linkid, nodeid in rows:
236 execute(sql, (int(linkid), int(nodeid)))
238 # figure the column names to fetch
239 fetch = ['_%s'%name for name in propnames]
241 # select the data out of the old table
242 fetch.append('id')
243 fetch.append('__retired__')
244 fetchcols = ','.join(fetch)
245 sql = 'select %s from _%s'%(fetchcols, cn)
246 if __debug__:
247 print >>hyperdb.DEBUG, 'migration', (self, sql)
248 self.cursor.execute(sql)
250 # unserialise the old data
251 olddata = []
252 propnames = propnames + ['id', '__retired__']
253 for entry in self.cursor.fetchall():
254 l = []
255 olddata.append(l)
256 for i in range(len(propnames)):
257 name = propnames[i]
258 v = entry[i]
260 if name in ('id', '__retired__'):
261 l.append(int(v))
262 continue
263 prop = properties[name]
264 if isinstance(prop, Date) and v is not None:
265 v = date.Date(v)
266 elif isinstance(prop, Interval) and v is not None:
267 v = date.Interval(v)
268 elif isinstance(prop, Password) and v is not None:
269 v = password.Password(encrypted=v)
270 elif (isinstance(prop, Boolean) or
271 isinstance(prop, Number)) and v is not None:
272 v = float(v)
274 # convert to new MySQL data type
275 prop = properties[name]
276 if v is not None:
277 v = self.hyperdb_to_sql_value[prop.__class__](v)
278 l.append(v)
280 self.drop_class_table_indexes(cn, old_spec[0])
282 # drop the old table
283 execute('drop table _%s'%cn)
285 # create the new table
286 self.create_class_table(klass)
288 # do the insert of the old data
289 args = ','.join([self.arg for x in fetch])
290 sql = 'insert into _%s (%s) values (%s)'%(cn, fetchcols, args)
291 if __debug__:
292 print >>hyperdb.DEBUG, 'migration', (self, sql)
293 for entry in olddata:
294 if __debug__:
295 print >>hyperdb.DEBUG, '... data', entry
296 execute(sql, tuple(entry))
298 # now load up the old journal data
299 cols = ','.join('nodeid date tag action params'.split())
300 sql = 'select %s from %s__journal'%(cols, cn)
301 if __debug__:
302 print >>hyperdb.DEBUG, 'migration', (self, sql)
303 execute(sql)
305 olddata = []
306 for nodeid, journaldate, journaltag, action, params in \
307 self.cursor.fetchall():
308 nodeid = int(nodeid)
309 journaldate = date.Date(journaldate)
310 params = eval(params)
311 olddata.append((nodeid, journaldate, journaltag, action,
312 params))
314 # drop journal table and indexes
315 self.drop_journal_table_indexes(cn)
316 sql = 'drop table %s__journal'%cn
317 if __debug__:
318 print >>hyperdb.DEBUG, 'migration', (self, sql)
319 execute(sql)
321 # re-create journal table
322 self.create_journal_table(klass)
323 for nodeid, journaldate, journaltag, action, params in olddata:
324 self.save_journal(cn, cols, nodeid, journaldate,
325 journaltag, action, params)
327 # make sure the normal schema update code doesn't try to
328 # change things
329 self.database_schema['tables'][cn] = klass.schema()
331 def __repr__(self):
332 return '<myroundsql 0x%x>'%id(self)
334 def sql_fetchone(self):
335 return self.cursor.fetchone()
337 def sql_fetchall(self):
338 return self.cursor.fetchall()
340 def sql_index_exists(self, table_name, index_name):
341 self.cursor.execute('show index from %s'%table_name)
342 for index in self.cursor.fetchall():
343 if index[2] == index_name:
344 return 1
345 return 0
347 def save_dbschema(self, schema):
348 s = repr(self.database_schema)
349 self.sql('INSERT INTO schema VALUES (%s)', (s,))
351 def create_class_table(self, spec):
352 cols, mls = self.determine_columns(spec.properties.items())
354 # add on our special columns
355 cols.append(('id', 'INTEGER PRIMARY KEY'))
356 cols.append(('__retired__', 'INTEGER DEFAULT 0'))
358 # create the base table
359 scols = ','.join(['%s %s'%x for x in cols])
360 sql = 'create table _%s (%s) type=%s'%(spec.classname, scols,
361 self.mysql_backend)
362 if __debug__:
363 print >>hyperdb.DEBUG, 'create_class', (self, sql)
364 self.cursor.execute(sql)
366 self.create_class_table_indexes(spec)
367 return cols, mls
369 def drop_class_table_indexes(self, cn, key):
370 # drop the old table indexes first
371 l = ['_%s_id_idx'%cn, '_%s_retired_idx'%cn]
372 if key:
373 l.append('_%s_%s_idx'%(cn, key))
375 table_name = '_%s'%cn
376 for index_name in l:
377 if not self.sql_index_exists(table_name, index_name):
378 continue
379 index_sql = 'drop index %s on %s'%(index_name, table_name)
380 if __debug__:
381 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
382 self.cursor.execute(index_sql)
384 def create_journal_table(self, spec):
385 # journal table
386 cols = ','.join(['%s varchar'%x
387 for x in 'nodeid date tag action params'.split()])
388 sql = '''create table %s__journal (
389 nodeid integer, date timestamp, tag varchar(255),
390 action varchar(255), params varchar(255)) type=%s'''%(
391 spec.classname, self.mysql_backend)
392 if __debug__:
393 print >>hyperdb.DEBUG, 'create_journal_table', (self, sql)
394 self.cursor.execute(sql)
395 self.create_journal_table_indexes(spec)
397 def drop_journal_table_indexes(self, classname):
398 index_name = '%s_journ_idx'%classname
399 if not self.sql_index_exists('%s__journal'%classname, index_name):
400 return
401 index_sql = 'drop index %s on %s__journal'%(index_name, classname)
402 if __debug__:
403 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
404 self.cursor.execute(index_sql)
406 def create_multilink_table(self, spec, ml):
407 sql = '''CREATE TABLE `%s_%s` (linkid VARCHAR(255),
408 nodeid VARCHAR(255)) TYPE=%s'''%(spec.classname, ml,
409 self.mysql_backend)
410 if __debug__:
411 print >>hyperdb.DEBUG, 'create_class', (self, sql)
412 self.cursor.execute(sql)
413 self.create_multilink_table_indexes(spec, ml)
415 def drop_multilink_table_indexes(self, classname, ml):
416 l = [
417 '%s_%s_l_idx'%(classname, ml),
418 '%s_%s_n_idx'%(classname, ml)
419 ]
420 table_name = '%s_%s'%(classname, ml)
421 for index_name in l:
422 if not self.sql_index_exists(table_name, index_name):
423 continue
424 index_sql = 'drop index %s on %s'%(index_name, table_name)
425 if __debug__:
426 print >>hyperdb.DEBUG, 'drop_index', (self, index_sql)
427 self.cursor.execute(index_sql)
429 def drop_class_table_key_index(self, cn, key):
430 table_name = '_%s'%cn
431 index_name = '_%s_%s_idx'%(cn, key)
432 if not self.sql_index_exists(table_name, index_name):
433 return
434 sql = 'drop index %s on %s'%(index_name, table_name)
435 if __debug__:
436 print >>hyperdb.DEBUG, 'drop_index', (self, sql)
437 self.cursor.execute(sql)
439 # old-skool id generation
440 def newid(self, classname):
441 ''' Generate a new id for the given class
442 '''
443 # get the next ID
444 sql = 'select num from ids where name=%s'%self.arg
445 if __debug__:
446 print >>hyperdb.DEBUG, 'newid', (self, sql, classname)
447 self.cursor.execute(sql, (classname, ))
448 newid = int(self.cursor.fetchone()[0])
450 # update the counter
451 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
452 vals = (int(newid)+1, classname)
453 if __debug__:
454 print >>hyperdb.DEBUG, 'newid', (self, sql, vals)
455 self.cursor.execute(sql, vals)
457 # return as string
458 return str(newid)
460 def setid(self, classname, setid):
461 ''' Set the id counter: used during import of database
463 We add one to make it behave like the seqeunces in postgres.
464 '''
465 sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
466 vals = (int(setid)+1, classname)
467 if __debug__:
468 print >>hyperdb.DEBUG, 'setid', (self, sql, vals)
469 self.cursor.execute(sql, vals)
471 def create_class(self, spec):
472 rdbms_common.Database.create_class(self, spec)
473 sql = 'insert into ids (name, num) values (%s, %s)'
474 vals = (spec.classname, 1)
475 if __debug__:
476 print >>hyperdb.DEBUG, 'create_class', (self, sql, vals)
477 self.cursor.execute(sql, vals)
479 class MysqlClass:
480 # we're overriding this method for ONE missing bit of functionality.
481 # look for "I can't believe it's not a toy RDBMS" below
482 def filter(self, search_matches, filterspec, sort=(None,None),
483 group=(None,None)):
484 '''Return a list of the ids of the active nodes in this class that
485 match the 'filter' spec, sorted by the group spec and then the
486 sort spec
488 "filterspec" is {propname: value(s)}
490 "sort" and "group" are (dir, prop) where dir is '+', '-' or None
491 and prop is a prop name or None
493 "search_matches" is {nodeid: marker}
495 The filter must match all properties specificed - but if the
496 property value to match is a list, any one of the values in the
497 list may match for that property to match.
498 '''
499 # just don't bother if the full-text search matched diddly
500 if search_matches == {}:
501 return []
503 cn = self.classname
505 timezone = self.db.getUserTimezone()
507 # figure the WHERE clause from the filterspec
508 props = self.getprops()
509 frum = ['_'+cn]
510 where = []
511 args = []
512 a = self.db.arg
513 for k, v in filterspec.items():
514 propclass = props[k]
515 # now do other where clause stuff
516 if isinstance(propclass, Multilink):
517 tn = '%s_%s'%(cn, k)
518 if v in ('-1', ['-1']):
519 # only match rows that have count(linkid)=0 in the
520 # corresponding multilink table)
522 # "I can't believe it's not a toy RDBMS"
523 # see, even toy RDBMSes like gadfly and sqlite can do
524 # sub-selects...
525 self.db.sql('select nodeid from %s'%tn)
526 s = ','.join([x[0] for x in self.db.sql_fetchall()])
528 where.append('id not in (%s)'%s)
529 elif isinstance(v, type([])):
530 frum.append(tn)
531 s = ','.join([a for x in v])
532 where.append('id=%s.nodeid and %s.linkid in (%s)'%(tn,tn,s))
533 args = args + v
534 else:
535 frum.append(tn)
536 where.append('id=%s.nodeid and %s.linkid=%s'%(tn, tn, a))
537 args.append(v)
538 elif k == 'id':
539 if isinstance(v, type([])):
540 s = ','.join([a for x in v])
541 where.append('%s in (%s)'%(k, s))
542 args = args + v
543 else:
544 where.append('%s=%s'%(k, a))
545 args.append(v)
546 elif isinstance(propclass, String):
547 if not isinstance(v, type([])):
548 v = [v]
550 # Quote the bits in the string that need it and then embed
551 # in a "substring" search. Note - need to quote the '%' so
552 # they make it through the python layer happily
553 v = ['%%'+self.db.sql_stringquote(s)+'%%' for s in v]
555 # now add to the where clause
556 where.append(' or '.join(["_%s LIKE '%s'"%(k, s) for s in v]))
557 # note: args are embedded in the query string now
558 elif isinstance(propclass, Link):
559 if isinstance(v, type([])):
560 if '-1' in v:
561 v = v[:]
562 v.remove('-1')
563 xtra = ' or _%s is NULL'%k
564 else:
565 xtra = ''
566 if v:
567 s = ','.join([a for x in v])
568 where.append('(_%s in (%s)%s)'%(k, s, xtra))
569 args = args + v
570 else:
571 where.append('_%s is NULL'%k)
572 else:
573 if v == '-1':
574 v = None
575 where.append('_%s is NULL'%k)
576 else:
577 where.append('_%s=%s'%(k, a))
578 args.append(v)
579 elif isinstance(propclass, Date):
580 if isinstance(v, type([])):
581 s = ','.join([a for x in v])
582 where.append('_%s in (%s)'%(k, s))
583 args = args + [date.Date(x).serialise() for x in v]
584 else:
585 try:
586 # Try to filter on range of dates
587 date_rng = Range(v, date.Date, offset=timezone)
588 if (date_rng.from_value):
589 where.append('_%s >= %s'%(k, a))
590 args.append(date_rng.from_value.serialise())
591 if (date_rng.to_value):
592 where.append('_%s <= %s'%(k, a))
593 args.append(date_rng.to_value.serialise())
594 except ValueError:
595 # If range creation fails - ignore that search parameter
596 pass
597 elif isinstance(propclass, Interval):
598 if isinstance(v, type([])):
599 s = ','.join([a for x in v])
600 where.append('_%s in (%s)'%(k, s))
601 args = args + [date.Interval(x).serialise() for x in v]
602 else:
603 try:
604 # Try to filter on range of intervals
605 date_rng = Range(v, date.Interval)
606 if (date_rng.from_value):
607 where.append('_%s >= %s'%(k, a))
608 args.append(date_rng.from_value.serialise())
609 if (date_rng.to_value):
610 where.append('_%s <= %s'%(k, a))
611 args.append(date_rng.to_value.serialise())
612 except ValueError:
613 # If range creation fails - ignore that search parameter
614 pass
615 #where.append('_%s=%s'%(k, a))
616 #args.append(date.Interval(v).serialise())
617 else:
618 if isinstance(v, type([])):
619 s = ','.join([a for x in v])
620 where.append('_%s in (%s)'%(k, s))
621 args = args + v
622 else:
623 where.append('_%s=%s'%(k, a))
624 args.append(v)
626 # don't match retired nodes
627 where.append('__retired__ <> 1')
629 # add results of full text search
630 if search_matches is not None:
631 v = search_matches.keys()
632 s = ','.join([a for x in v])
633 where.append('id in (%s)'%s)
634 args = args + v
636 # "grouping" is just the first-order sorting in the SQL fetch
637 # can modify it...)
638 orderby = []
639 ordercols = []
640 if group[0] is not None and group[1] is not None:
641 if group[0] != '-':
642 orderby.append('_'+group[1])
643 ordercols.append('_'+group[1])
644 else:
645 orderby.append('_'+group[1]+' desc')
646 ordercols.append('_'+group[1])
648 # now add in the sorting
649 group = ''
650 if sort[0] is not None and sort[1] is not None:
651 direction, colname = sort
652 if direction != '-':
653 if colname == 'id':
654 orderby.append(colname)
655 else:
656 orderby.append('_'+colname)
657 ordercols.append('_'+colname)
658 else:
659 if colname == 'id':
660 orderby.append(colname+' desc')
661 ordercols.append(colname)
662 else:
663 orderby.append('_'+colname+' desc')
664 ordercols.append('_'+colname)
666 # construct the SQL
667 frum = ','.join(frum)
668 if where:
669 where = ' where ' + (' and '.join(where))
670 else:
671 where = ''
672 cols = ['id']
673 if orderby:
674 cols = cols + ordercols
675 order = ' order by %s'%(','.join(orderby))
676 else:
677 order = ''
678 cols = ','.join(cols)
679 sql = 'select %s from %s %s%s%s'%(cols, frum, where, group, order)
680 args = tuple(args)
681 if __debug__:
682 print >>hyperdb.DEBUG, 'filter', (self, sql, args)
683 self.db.cursor.execute(sql, args)
684 l = self.db.cursor.fetchall()
686 # return the IDs (the first column)
687 # XXX numeric ids
688 return [str(row[0]) for row in l]
690 class Class(MysqlClass, rdbms_common.Class):
691 pass
692 class IssueClass(MysqlClass, rdbms_common.IssueClass):
693 pass
694 class FileClass(MysqlClass, rdbms_common.FileClass):
695 pass
697 #vim: set et