diff --git a/includes/database/backup.inc b/includes/database/backup.inc
new file mode 100644
index 0000000..0d65cc7
--- /dev/null
+++ b/includes/database/backup.inc
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Backup class for the database layer.
+ */
+
+/**
+ * Database backup.
+ *
+ * Allows databases to be backed up and then restore from backup.
+ */
+abstract class DatabaseBackup {
+  /**
+   * The connection object.
+   *
+   * @var object
+   */
+  protected $connection;
+
+  function __construct($connection) {
+    $this->connection = $connection;
+  }
+
+  /**
+   * Dump active database.
+   *
+   * @param $filename
+   *   The filename including path to write the dump to.
+   * @param $options
+   *   An associative array of snapshot options, as described in demo_dump().
+   */
+  function dump($filename) {
+    // Make sure we have permission to save our backup file.
+    $directory = dirname($filename);
+    if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) {
+      return FALSE;
+    }
+
+    if ($this->fp = fopen($filename, 'wb')) {
+      fwrite($this->fp, $this->header());
+
+      $tables = drupal_get_schema();
+
+      foreach ($tables as $table => $dump_options) {
+        $this->dumpSchema($table);
+        if (!isset($dump_options['backup data']) || $dump_options['backup data']) {
+          $this->dumpData($table);
+        }
+      }
+
+      fwrite($this->fp, $this->footer());
+
+      fclose($this->fp);
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
+  protected function header() {
+    return '';
+  }
+
+  protected function footer() {
+    return '';
+  }
+
+  /**
+   * Dump table schema.
+   *
+   * @param $table
+   *   A table name to export the schema for.
+   */
+  abstract protected function dumpSchema($table);
+
+  /**
+   * Dump table data.
+   *
+   * @param $table
+   *   A table name to export the data for.
+   */
+  abstract protected function dumpData($table);
+}
diff --git a/includes/database/database.inc b/includes/database/database.inc
index 4cc1a33..1429eb8 100644
--- a/includes/database/database.inc
+++ b/includes/database/database.inc
@@ -1250,6 +1250,16 @@ abstract class DatabaseConnection extends PDO {
    *   also larger than the $existing_id if one was passed in.
    */
   abstract public function nextId($existing_id = 0);
+
+  /**
+   * Returns a backup object.
+   *
+   * @see DatabaseBackup
+   */
+  public function backup() {
+    $class = $this->getDriverClass('DatabaseBackup', array('backup.inc'));
+    return new $class($this);
+  }
 }
 
 /**
diff --git a/includes/database/mysql/backup.inc b/includes/database/mysql/backup.inc
new file mode 100644
index 0000000..2ed2f62
--- /dev/null
+++ b/includes/database/mysql/backup.inc
@@ -0,0 +1,264 @@
+<?php
+
+/**
+ * @file
+ * Backup class for the database layer.
+ */
+
+/**
+ * Database backup.
+ *
+ * Allows databases to be backed up and then restore from backup.
+ */
+class DatabaseBackup_mysql extends DatabaseBackup {
+  function header() {
+    $header = array();
+    // Avoid auto value for zero values (required for user id 0).
+    $header[] = 'SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";';
+    // Temporarily disable foreign key checks for the time of import.
+    $header[] = 'SET FOREIGN_KEY_CHECKS = 0;';
+    $header[] = '';
+
+    // Set collations for the import. PMA and mysqldump use conditional comments
+    // to exclude MySQL <4.1, but D6 requires >=4.1.
+    $header[] = 'SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT;';
+    $header[] = 'SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS;';
+    $header[] = 'SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION;';
+    $header[] = 'SET NAMES utf8;';
+    $header[] = '';
+    return implode("\n", $header);
+  }
+
+  function footer() {
+    // Re-enable foreign key checks.
+    $footer[] = 'SET FOREIGN_KEY_CHECKS = 1;';
+
+    // Revert collations for potential subsequent database queries not belonging
+    // to this module.
+    // @todo Double-check this behavior according to the results of
+    //   http://drupal.org/node/772678
+    $footer[] = 'SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT;';
+    $footer[] = 'SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS;';
+    $footer[] = 'SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION;';
+    $footer[] = '';
+
+    $footer[] = '';
+    return implode("\n", $footer);
+  }
+
+  /**
+   * Dump table schema.
+   *
+   * @param $table
+   *   A table name to export the schema for.
+   */
+  protected function dumpSchema($table) {
+    $output = "\n";
+    $output .= "--\n";
+    $output .= "-- Table structure for table '$table'\n";
+    $output .= "--\n\n";
+
+    $data = $this->connection->query("SHOW CREATE TABLE `$table`")->fetchAssoc();
+    $status = $this->connection->query('SHOW TABLE STATUS LIKE :table', array(':table' => $table))->fetchAssoc();
+
+    // Column keys in $status start with a lower-case letter in PDO and with a
+    // upper-case letter otherwise. We convert all to lower-case.
+    foreach ($status as $key => $value) {
+      $key_lower = strtolower($key);
+      if ($key[0] != $key_lower[0]) {
+        $status[$key_lower] = $value;
+        unset($status[$key]);
+      }
+    }
+
+    // Add IF NOT EXISTS to CREATE TABLE, replace double quotes with MySQL quotes.
+    $output .= preg_replace(
+      array('/^CREATE TABLE/', '/"/'),
+      array('CREATE TABLE IF NOT EXISTS', '`'),
+      $data['create table']
+    );
+
+    // @TODO Rethink the following code. Perhaps try to strip + parse the existing
+    //   table definition (after leading ")" on last line) and merge anything
+    //   missing into it, and re-append it again. There are too many differences
+    //   between MySQL 5.0 and 5.1+, and PHP mysql(i) and pdo_mysql extensions.
+
+    // PDO is missing the table engine.
+    if (!strpos($output, ' ENGINE=')) {
+      $output .= ' ENGINE=' . $status['engine'];
+    }
+
+    // Always add charset and collation info to table definitions.
+    // SHOW CREATE TABLE does not contain collation information, if the collation
+    // is equal to the default collation of the connection. Since dumps can be
+    // moved across servers, we need to ensure correct collations.
+    // Note that [DEFAULT] CHARSET or [DEFAULT] CHARACTER SET is always contained
+    // on MySQL 5.1, even if it is equal to the default.
+    // This addition assumes that a collation specified for a table is taken over
+    // for the table's columns. The MySQL manual does not state whether this is
+    // the case, but manual tests confirmed that it works that way.
+    // Like Drupal core, we need to enforce UTF8 as character set and
+    // utf8_general_ci as default database collation, if not overridden via
+    // settings.php.
+    if (!strpos($output, 'COLLATE=')) {
+      // Only if the collation contains a underscore, the first string up to the
+      // first underscore is the character set.
+      // @see PMA_exportDBCreate()
+      if (strpos($status['collation'], '_')) {
+        $collate = 'COLLATE=' . $status['collation'];
+      }
+      // If there is a character set defined already, just append the collation.
+      if (strpos($output, 'CHARSET') || strpos($output, 'CHARACTER SET')) {
+        // @TODO This may also hit column definitions instead of the table
+        //   definition only. Should technically also be case-insensitive.
+        $output = preg_replace('@((?:DEFAULT )?(?:CHARSET|CHARACTER SET) \w+)@', '$1 ' . $collate, $output);
+      }
+      else {
+        $output .= ' DEFAULT CHARSET=utf8 ' . $collate;
+      }
+    }
+
+    // Add the table comment, if any.
+    if (!preg_match('@^\) .*COMMENT.+$@', $output) && !empty($status['comment'])) {
+      // On PHP 5.2.6/Win32 with PDO MySQL 5.0 with InnoDB, the table comment has
+      // a trailing "; InnoDB free: 84992 kB".
+      $status['comment'] = preg_replace('@; InnoDB free: .+$@', '', $status['comment']);
+      $output .= " COMMENT='" . $status['comment'] . "'";
+    }
+
+    // @TODO Depends on whether we dump data and table existence on import.
+    // if (!empty($status['auto_increment'])) {
+    //   $output .= ' AUTO_INCREMENT=' . $status['auto_increment'];
+    // }
+
+    $output .= ";\n";
+    fwrite($this->fp, $output);
+  }
+
+  /**
+   * Dump table data.
+   *
+   * This code has largely been stolen from the phpMyAdmin project.
+   *
+   * @param $table
+   *   A table name to export the data for.
+   */
+  protected function dumpData($table) {
+    $output = "\n";
+    $output .= "--\n";
+    $output .= "-- Dumping data for table '$table'\n";
+    $output .= "--\n\n";
+
+    // Dump table data.
+    $result = $this->connection->query("SELECT * FROM `$table`", array(), array('fetch' => PDO::FETCH_ASSOC));
+
+    // Get table fields.
+    if ($fields = $this->getFields($result)) {
+      // Disable indices to speed up import.
+      $output .= "/*!40000 ALTER TABLE $table DISABLE KEYS */;\n";
+
+      // Escape backslashes, PHP code, special chars
+      $search = array('\\', "'", "\x00", "\x0a", "\x0d", "\x1a");
+      $replace = array('\\\\', "''", '\0', '\n', '\r', '\Z');
+
+      $insert_cmd = "INSERT INTO `$table` VALUES\n";
+      $insert_buffer = '';
+      $current_row = 0;
+      $query_size = 0;
+
+      foreach ($result as $row) {
+        $current_row++;
+        $values = array();
+        $field = 0;
+        foreach ($row as $value) {
+          // NULL
+          if (!isset($value) || is_null($value)) {
+            $values[] = 'NULL';
+          }
+          // A number
+          // timestamp is numeric on some MySQL 4.1, BLOBs are sometimes numeric
+          else if ($fields[$field]->numeric && !$fields[$field]->timestamp && !$fields[$field]->blob) {
+            $values[] = $value;
+          }
+          // A true BLOB
+          // - mysqldump only generates hex data when the --hex-blob
+          //   option is used, for fields having the binary attribute
+          //   no hex is generated
+          // - a TEXT field returns type blob but a real blob
+          //   returns also the 'binary' flag
+          else if ($fields[$field]->binary && $fields[$field]->blob) {
+            // Empty blobs need to be different, but '0' is also empty :-(
+            if (empty($value) && $value != '0') {
+              $values[] = "''";
+            }
+            else {
+              $values[] = '0x' . bin2hex($value);
+            }
+          }
+          // Something else -> treat as a string
+          else {
+            $values[] = "'" . str_replace($search, $replace, $value) . "'";
+          }
+          $field++;
+        }
+
+        if ($current_row == 1) {
+          $insert_buffer = $insert_cmd . '(' . implode(', ', $values) . ')';
+        }
+        else {
+          $insert_buffer = '(' . implode(', ', $values) . ')';
+
+          // Check if buffer size exceeds 50KB.
+          if ($query_size + strlen($insert_buffer) > 50000) {
+            // Flush to disc and start new buffer.
+            fwrite($this->fp, $output . ";\n");
+            $output = '';
+            $current_row = 1;
+            $query_size = 0;
+            $insert_buffer = $insert_cmd . $insert_buffer;
+          }
+        }
+        $query_size += strlen($insert_buffer);
+
+        $output .= ($current_row == 1 ? '' : ",\n") . $insert_buffer;
+      }
+
+      if ($current_row > 0) {
+        $output .= ";\n";
+      }
+
+      // Enable indices again.
+      $output .= "/*!40000 ALTER TABLE $table ENABLE KEYS */;\n";
+    }
+
+    fwrite($this->fp, $output);
+  }
+
+  /**
+   * Return table fields and their properties.
+   */
+  protected function getFields($result) {
+    $fields = array();
+
+    $i = 0;
+    while ($meta = $result->getColumnMeta($i)) {
+      settype($meta, 'object');
+      // pdo_mysql does not add a native type for INT fields.
+      if (isset($meta->native_type)) {
+        // Enhance the field definition for mysql-extension compatibilty.
+        $meta->numeric = (strtolower($meta->native_type) == 'short');
+        $meta->blob = (strtolower($meta->native_type) == 'blob');
+        // Add custom properties.
+        $meta->timestamp = (strtolower($meta->native_type) == 'long');
+      }
+      else {
+        $meta->numeric = $meta->blob = $meta->timestamp = FALSE;
+      }
+      $meta->binary = (array_search('not_null', $meta->flags));
+      $fields[] = $meta;
+      $i++;
+    }
+
+    return $fields;
+  }
+}
diff --git a/modules/dblog/dblog.install b/modules/dblog/dblog.install
index 23f85ba..97baa6a 100644
--- a/modules/dblog/dblog.install
+++ b/modules/dblog/dblog.install
@@ -86,6 +86,7 @@ function dblog_schema() {
       'type' => array('type'),
       'uid' => array('uid'),
     ),
+    'backup data' => FALSE,
   );
 
   return $schema;
diff --git a/modules/system/system.install b/modules/system/system.install
index 6d2fc80..74eb00e 100644
--- a/modules/system/system.install
+++ b/modules/system/system.install
@@ -687,6 +687,7 @@ function system_schema() {
       'expire' => array('expire'),
     ),
     'primary key' => array('cid'),
+    'backup data' => FALSE,
   );
   $schema['cache_bootstrap'] = $schema['cache'];
   $schema['cache_bootstrap']['description'] = 'Cache table for data required to bootstrap Drupal, may be routed to a shared memory cache.';
diff --git a/update.php b/update.php
index a777920..074372a 100644
--- a/update.php
+++ b/update.php
@@ -245,6 +245,19 @@ function update_info_page() {
   $output .= "<li>Install your new files in the appropriate location, as described in the handbook.</li>\n";
   $output .= "</ol>\n";
   $output .= "<p>When you have performed the steps above, you may proceed.</p>\n";
+  $output .= '<form method="post" action="update.php?op=backup&amp;token=' . $token . '"><p><input type="submit" value="Continue" class="form-submit" /></p></form>';
+  $output .= "\n";
+  return $output;
+}
+
+function update_backup_page() {
+  update_task_list('backup');
+  drupal_set_title('Drupal database update');
+  $token = drupal_get_token('update');
+
+  Database::getConnection()->backup()->dump(file_default_scheme() . '://dump-' . time() . '.mysql');
+
+  $output = "<p>All backed up!</p>\n";
   $output .= '<form method="post" action="update.php?op=selection&amp;token=' . $token . '"><p><input type="submit" value="Continue" class="form-submit" /></p></form>';
   $output .= "\n";
   return $output;
@@ -295,6 +308,7 @@ function update_task_list($active = NULL) {
   $tasks = array(
     'requirements' => 'Verify requirements',
     'info' => 'Overview',
+    'backup' => 'Back up database',
     'select' => 'Review updates',
     'run' => 'Run updates',
     'finished' => 'Review log',
@@ -436,6 +450,10 @@ if (update_access_allowed()) {
       $output = update_results_page();
       break;
 
+    case 'backup':
+      $output = update_backup_page();
+      break;
+
     // Regular batch ops : defer to batch processing API.
     default:
       update_task_list('run');
