=== added file 'trac/db/mysql_backend.py'
--- /dev/null	
+++ trac/db/mysql_backend.py	
@@ -0,0 +1,119 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005 Jeff Weiss <trac@jeffweiss.org>
+# Copyright (C) 2006 Andres Salomon <dilinger@athenacr.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Derived from postgres_backend.py
+# Author: Christopher Lenz <cmlenz@gmx.de> 
+# Author: Jeff Weiss <trac@jeffweiss.org>
+
+from trac.core import *
+from trac.db.api import IDatabaseConnector
+from trac.db.util import ConnectionWrapper
+
+class MySQLConnector(Component):
+    """MySQL database support.  Still extremely experimental!
+Currently only supports database urls of the following formats:
+mysql://user:password@host/database
+mysql://user:password@host:port/database
+"""
+
+    implements(IDatabaseConnector)
+
+    def get_supported_schemes(self):
+        return [('mysql', 1)]
+
+    def get_connection(self, path, user=None, password=None, host=None,
+                       port=None, params={}):
+        return MySQLConnection(path, user, password, host, port, params)
+
+    def init_db(self, path, user=None, password=None, host=None, port=None,
+                params={}):
+        cnx = self.get_connection(path, user, password, host, port, params)
+        cursor = cnx.cursor()
+        from trac.db_default import schema
+        for table in schema:
+            for stmt in self.to_sql(table):
+                self.env.log.debug(stmt)
+                cursor.execute(stmt)
+        cnx.commit()
+
+    def _collist(self, table, columns):
+        """
+        Take a list of columns and impose limits on each so that indexing
+        works properly.  Some Versions of MySQL limit each index prefix to
+        500 bytes total, with a max of 255 bytes per column.
+        """
+        cols = []
+        limit = 500 / len(columns)
+        if limit > 255:
+            limit = 255
+        for c in columns:
+            name = '`%s`' % c
+            table_col = filter((lambda x: x.name == c), table.columns)
+            if len(table_col) == 1 and table_col[0].type.lower() == 'text':
+                name += '(%s)' % limit
+            # For non-text columns, we simply throw away the extra bytes.
+            # That could certainly be optimized better, but for now let's KISS.
+            cols.append(name)
+        return ','.join(cols)
+
+    def to_sql(self, table):
+        sql = ['CREATE TABLE %s (' % table.name]
+        coldefs = []
+        for column in table.columns:
+            ctype = column.type
+            if column.auto_increment:
+                ctype = 'INT UNSIGNED NOT NULL AUTO_INCREMENT'
+                # Override the column type, as a text field cannot
+                # use auto_increment.
+                column.type = 'int'
+            coldefs.append('    `%s` %s' % (column.name, ctype))
+        if len(table.key) > 0:
+            coldefs.append('    PRIMARY KEY (%s)' %
+                           self._collist(table, table.key))
+        sql.append(',\n'.join(coldefs) + '\n)')
+        yield '\n'.join(sql)
+        for index in table.indices:
+            yield 'CREATE INDEX %s_%s_idx ON %s (%s);' % (table.name,
+                  '_'.join(index.columns), table.name,
+                  self._collist(table, index.columns))
+
+class MySQLConnection(ConnectionWrapper):
+    """Connection wrapper for MySQL."""
+
+    poolable = True
+
+    def __init__(self, path, user=None, password=None, host=None,
+                 port=None, params={}):
+        import MySQLdb
+
+        if path.startswith('/'):
+            path = path[1:]
+        if port == None:
+            port = 3306
+        cnx = MySQLdb.connect(db=path, user=user, passwd=password, host=host, port=port)
+        ConnectionWrapper.__init__(self, cnx)
+
+    def cast(self, column, type):
+        # Temporary hack needed for the union of selects in the search module
+        return 'CAST(%s AS %s)' % (column, type)
+
+    def like(self):
+        # Temporary hack needed for the case-insensitive string matching in the
+        # search module
+        return 'LIKE'
+
+    def get_last_id(self, cursor, table, column='id'):
+        return self.cnx.insert_id()

=== modified file 'trac/db/tests/api.py'
--- trac/db/tests/api.py	
+++ trac/db/tests/api.py	
@@ -40,6 +40,15 @@
                                        'path': '/trac'}),
                          _parse_db_str('postgres://john:letmein@localhost:9431/trac'))
 
+    def test_mysql_simple(self):
+        self.assertEqual(('mysql', {'host': 'localhost', 'path': '/trac'}),
+                         _parse_db_str('mysql://localhost/trac'))
+
+    def test_mysql_with_creds(self):
+        self.assertEqual(('mysql', {'user': 'john', 'password': 'letmein',
+                                    'host': 'localhost', 'port': 3306,
+                                    'path': '/trac'}),
+                         _parse_db_str('mysql://john:letmein@localhost:3306/trac'))
 
 def suite():
     return unittest.makeSuite(ParseConnectionStringTestCase,'test')

=== modified file 'trac/db_default.py'
--- trac/db_default.py	
+++ trac/db_default.py	
@@ -148,7 +148,7 @@
         Column('id', auto_increment=True),
         Column('author'),
         Column('title'),
-        Column('sql'),
+        Column('query'),
         Column('description')],
 ]
 
@@ -379,7 +379,7 @@
              ('name', 'value'),
                (('database_version', str(db_version)),)),
            ('report',
-             ('author', 'title', 'sql', 'description'),
+             ('author', 'title', 'query', 'description'),
                __mkreports(reports)))
 
 default_config = \
@@ -442,6 +442,7 @@
 
 default_components = ('trac.About', 'trac.attachment',
                       'trac.db.postgres_backend', 'trac.db.sqlite_backend',
+                      'trac.db.mysql_backend',
                       'trac.mimeview.enscript', 'trac.mimeview.patch',
                       'trac.mimeview.php', 'trac.mimeview.rst',
                       'trac.mimeview.silvercity', 'trac.mimeview.txtl',

=== modified file 'trac/ticket/report.py'
--- trac/ticket/report.py	
+++ trac/ticket/report.py	
@@ -148,11 +148,11 @@
             req.redirect(self.env.href.report())
 
         title = req.args.get('title', '')
-        sql = req.args.get('sql', '')
+        query = req.args.get('query', '')
         description = req.args.get('description', '')
         cursor = db.cursor()
-        cursor.execute("INSERT INTO report (title,sql,description) "
-                       "VALUES (%s,%s,%s)", (title, sql, description))
+        cursor.execute("INSERT INTO report (title,query,description) "
+                       "VALUES (%s,%s,%s)", (title, query, description))
         id = db.get_last_id(cursor, 'report')
         db.commit()
         req.redirect(self.env.href.report(id))
@@ -176,11 +176,11 @@
 
         if not req.args.has_key('cancel'):
             title = req.args.get('title', '')
-            sql = req.args.get('sql', '')
+            query = req.args.get('query', '')
             description = req.args.get('description', '')
             cursor = db.cursor()
-            cursor.execute("UPDATE report SET title=%s,sql=%s,description=%s "
-                           "WHERE id=%s", (title, sql, description, id))
+            cursor.execute("UPDATE report SET title=%s,query=%s,description=%s "
+                           "WHERE id=%s", (title, query, description, id))
             db.commit()
         req.redirect(self.env.href.report(id))
 
@@ -204,11 +204,11 @@
     def _render_editor(self, req, db, id, copy=False):
         if id == -1:
             req.perm.assert_permission('REPORT_CREATE')
-            title = sql = description = ''
+            title = query = description = ''
         else:
             req.perm.assert_permission('REPORT_MODIFY')
             cursor = db.cursor()
-            cursor.execute("SELECT title,description,sql FROM report "
+            cursor.execute("SELECT title,description,query FROM report "
                            "WHERE id=%s", (id,))
             row = cursor.fetchone()
             if not row:
@@ -216,7 +216,7 @@
                                      'Invalid Report Number')
             title = row[0] or ''
             description = row[1] or ''
-            sql = row[2] or ''
+            query = row[2] or ''
 
         if copy:
             title += ' (copy)'
@@ -233,7 +233,7 @@
         req.hdf['report.id'] = id
         req.hdf['report.mode'] = 'edit'
         req.hdf['report.title'] = title
-        req.hdf['report.sql'] = sql
+        req.hdf['report.sql'] = query
         req.hdf['report.description'] = description
 
     def _render_view(self, req, db, id):
@@ -426,7 +426,7 @@
             description = 'This is a list of reports available.'
         else:
             cursor = db.cursor()
-            cursor.execute("SELECT title,sql,description from report "
+            cursor.execute("SELECT title,query,description from report "
                            "WHERE id=%s", (id,))
             row = cursor.fetchone()
             if not row:

=== modified file 'trac/upgrades/db17.py'
--- trac/upgrades/db17.py	
+++ trac/upgrades/db17.py	
@@ -2,8 +2,11 @@
 
 def do_upgrade(env, ver, cursor):
     """Rename the columns `kind` and `change` in the `node_change` table for
-    compatibity with MySQL.
+    compatibity with MySQL, as well as the `sql` column in the `reports` table.
     """
+    db_connector, _ = DatabaseManager(env)._get_connector()
+
+    # alter node_change table
     cursor.execute("CREATE TEMP TABLE nc_old AS SELECT * FROM node_change")
     cursor.execute("DROP TABLE node_change")
 
@@ -16,10 +19,26 @@
         Column('base_rev'),
         Index(['rev'])
     ]
-    db_connector, _ = DatabaseManager(env)._get_connector()
     for stmt in db_connector.to_sql(table):
         cursor.execute(stmt)
 
     cursor.execute("INSERT INTO node_change (rev,path,node_type,change_type,"
                    "base_path,base_rev) SELECT rev,path,kind,change,"
                    "base_path,base_rev FROM nc_old")
+
+    # alter report table
+    cursor.execute("CREATE TEMP TABLE report_old AS SELECT * FROM report")
+    cursor.execute("DROP TABLE report")
+
+    table = Table('report', key='id')[
+        Column('id', auto_increment=True),
+        Column('author'),
+        Column('title'),
+        Column('query'),
+        Column('description')
+    ]
+    for stmt in db_connector.to_sql(table):
+        cursor.execute(stmt)
+
+    cursor.execute("INSERT INTO report (id,author,title,query,description) "
+                   "SELECT id,author,title,sql,description FROM report_old")

=== modified file 'trac/web/session.py'
--- trac/web/session.py	
+++ trac/web/session.py	
@@ -181,9 +181,12 @@
             # changed as to minimize the purging.
             mintime = now - PURGE_AGE
             self.env.log.debug('Purging old, expired, sessions.')
-            cursor.execute("DELETE FROM session WHERE authenticated=0 AND "
-                           "sid IN (SELECT sid FROM session WHERE "
-                           "var_name='last_visit' AND var_value < %s)",
+            cursor.execute("SELECT DISTINCT sid FROM session WHERE "
+                           "var_name='last_visit' AND var_value < %s",
                            (mintime,))
-
+            args = cursor.fetchall()
+            if args:
+                sql = 'DELETE FROM session WHERE authenticated = 0 AND ('
+                sql += ' OR '.join(['sid = %s'] * len(args)) + ')'
+                cursor.execute(sql, args)
             db.commit()


