Code

be07b8706158fc4c6f854f90ea158c1fb469682f
[roundup.git] / roundup / backends / back_mysql.py
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)
145     
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,))
350     
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()
506         
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