Index: trac/db/util.py
===================================================================
--- trac/db/util.py	(revision 8643)
+++ trac/db/util.py	(working copy)
@@ -15,12 +15,72 @@
 #
 # Author: Christopher Lenz <cmlenz@gmx.de>
 
+from __future__ import with_statement # to be used once python 2.4 support is dropped
+
 def sql_escape_percent(sql):
     import re
     return re.sub("'((?:[^']|(?:''))*)'",
                   lambda m: m.group(0).replace('%', '%%'), sql)
 
+def with_transaction(env, db, raise_exceptions = False):
+    """
+    Decorator for simple use-once transactions.
+    Usage:
+     -- def api_method(p1, p2):
+     --     @with_transaction(env, db)
+     --     def implementation_method(db):
+     --         # implementation
+     --         return result
+     --
+     --     result = implementation
+    """
+    def transaction_wrapper(fn):
+        if db:
+            try: return fn(db)
+            except Exception, e:
+                if raise_exception: raise
+        else:
+            tmpdb = env.get_db_cnx()
+            try:
+                r = fn(tmpdb)
+                tmpdb.commit()
+                return r
+            except Exception, e:
+                tmpdb.rollback()
+                if raise_exception: raise
+    return transaction_wrapper
 
+
+
+class transaction(object):
+    """
+    Transaction context manager.
+    Usage:
+     -- def api_method(p1, p2):
+     --     with transaction(env, db) as db:
+     --         # implementation
+     --         result = data
+     --
+    """
+    def __init__(env, db, raise_exceptions = False):
+        self._env = env
+        self._db = db
+        self._raise_exceptions = raise_exceptions
+
+    def __enter__(self):
+        if not self._db:
+            self._db = self._env.get_db_cnx()
+            self._env = None
+        return self._db
+
+    def __exit__(self, et, ev, tb):
+        if self._env is None:
+            if et is None:
+                self._db.commit()
+            else:
+                self._db.rollback()
+        return not self._raise_exception
+
 class IterableCursor(object):
     """Wrapper for DB-API cursor objects that makes the cursor iterable
     and escapes all "%"s used inside literal strings with parameterized
Index: trac/wiki/api.py
===================================================================
--- trac/wiki/api.py	(revision 8643)
+++ trac/wiki/api.py	(working copy)
@@ -42,6 +42,9 @@
     def wiki_page_changed(page, version, t, comment, author, ipnr):
         """Called when a page has been modified."""
 
+    def wiki_page_renamed(page, old_page_name): 
+        """Called when a page has been renamed in-place.""" 
+
     def wiki_page_deleted(page):
         """Called when a page has been deleted."""
 
Index: trac/wiki/tests/model.py
===================================================================
--- trac/wiki/tests/model.py	(revision 8643)
+++ trac/wiki/tests/model.py	(working copy)
@@ -14,6 +14,7 @@
         self.changed = []
         self.deleted = []
         self.deleted_version = []
+        self.renamed = {}
 
     def wiki_page_added(self, page):
         self.added.append(page)
@@ -27,6 +28,8 @@
     def wiki_page_version_deleted(self, page):
         self.deleted_version.append(page)
 
+    def wiki_page_renamed(self, page, old_page_name):
+        self.renamed[old_page_name] = page
 
 class WikiPageTestCase(unittest.TestCase):
 
@@ -166,7 +169,31 @@
         listener = TestWikiChangeListener(self.env)
         self.assertEqual(page, listener.deleted[0])
 
+    def test_rename_page(self):
+        cursor = self.db.cursor()
+        data = (1, 42, 'joe', '::1', 'Bla bla', 'Testing', 0)
+        cursor.execute("INSERT INTO wiki VALUES(%s,%s,%s,%s,%s,%s,%s,%s)",
+                       ('TestPage',) + data)
+        
+        page = WikiPage(self.env, 'TestPage')
 
+        page.rename('PageRenamed', self.db)
+        
+        cursor.execute("SELECT version,time,author,ipnr,text,comment,"
+                       "readonly FROM wiki WHERE name=%s", ('PageRenamed',))
+        self.assertEqual(data, cursor.fetchone())
+        self.assertEqual(None, cursor.fetchone())
+        
+        old_page = WikiPage(self.env, 'TestPage')
+        
+        cursor.execute("SELECT version,time,author,ipnr,text,comment,"
+                       "readonly FROM wiki WHERE name=%s", ('TestPage',))
+        self.assertEqual(None, cursor.fetchone())
+        
+        listener = TestWikiChangeListener(self.env)
+        self.assertEqual({'TestPage': page}, listener.renamed)
+
+
 def suite():
     return unittest.makeSuite(WikiPageTestCase, 'test')
 
Index: trac/wiki/model.py
===================================================================
--- trac/wiki/model.py	(revision 8643)
+++ trac/wiki/model.py	(working copy)
@@ -177,3 +177,32 @@
         for version,ts,author,comment,ipnr in cursor:
             time = datetime.fromtimestamp(ts, utc)
             yield version,time,author,comment,ipnr
+
+    def rename(self, new_name, db=None):
+        """Rename wiki page in-place, keeping the history intact.
+        Renaming a page this way will eventually leave dangling references
+        to the old page - which litterally doesn't exist anymore.
+        """
+        assert self.exists, 'Cannot rename non-existent page'
+
+        from trac.db.util import with_transaction
+        
+        @with_transaction(self.env, db)
+        def do_rename(db):
+            new_page = WikiPage(self.env, new_name, version=None, db=db)
+            old_name = self.name
+            cursor = db.cursor()
+            cursor.execute("UPDATE wiki SET name=%s WHERE name=%s",
+                           (new_name, old_name))
+            self.env.log.info('Renamed page %s in-place to %s' %
+                              (old_name, new_name))
+            return old_name
+        old_name = do_rename
+        
+        self.name = new_name
+        self._fetch(self.name, None, db)
+
+        for listener in WikiSystem(self.env).change_listeners:
+            if hasattr(listener, 'wiki_page_renamed'):
+                listener.wiki_page_renamed(self, old_name)
+
