diff --git a/includes/tablesort.inc b/includes/tablesort.inc
index 55a3f4c..121a1b9 100644
--- a/includes/tablesort.inc
+++ b/includes/tablesort.inc
@@ -73,18 +73,7 @@ class TableSort extends SelectQueryExtender {
    *   The current sort direction ("asc" or "desc").
    */
   protected function getSort() {
-    if (isset($_GET['sort'])) {
-      return ($_GET['sort'] == 'desc') ? 'desc' : 'asc';
-    }
-    // User has not specified a sort. Use default if specified; otherwise use "asc".
-    else {
-      foreach ($this->header as $header) {
-        if (is_array($header) && isset($header['sort'])) {
-          return $header['sort'];
-        }
-      }
-    }
-    return 'asc';
+    return tablesort_get_sort($this->header);
   }
 
   /**
@@ -111,32 +100,7 @@ class TableSort extends SelectQueryExtender {
    *   - "sql": The name of the database field to sort on.
    */
   protected function order() {
-    $order = isset($_GET['order']) ? $_GET['order'] : '';
-    foreach ($this->header as $header) {
-      if (isset($header['data']) && $order == $header['data']) {
-        return array('name' => $header['data'], 'sql' => isset($header['field']) ? $header['field'] : '');
-      }
-
-      if (isset($header['sort']) && ($header['sort'] == 'asc' || $header['sort'] == 'desc')) {
-        $default = array('name' => $header['data'], 'sql' => isset($header['field']) ? $header['field'] : '');
-      }
-    }
-
-    if (isset($default)) {
-      return $default;
-    }
-    else {
-      // The first column specified is initial 'order by' field unless otherwise specified
-      $headers = array_values($this->header);
-      $header = $headers[0];
-      if (is_array($header)) {
-        $header += array('data' => NULL, 'field' => NULL);
-        return array('name' => $header['data'], 'sql' => $header['field']);
-      }
-      else {
-        return array('name' => $header);
-      }
-    }
+    return tablesort_get_order($this->header);
   }
 }
 
@@ -238,29 +202,27 @@ function tablesort_get_query_parameters() {
 function tablesort_get_order($headers) {
   $order = isset($_GET['order']) ? $_GET['order'] : '';
   foreach ($headers as $header) {
-    if (isset($header['data']) && $order == $header['data']) {
-      return array('name' => $header['data'], 'sql' => isset($header['field']) ? $header['field'] : '');
-    }
+    if (is_array($header)) {
+      if (isset($header['data']) && $order == $header['data']) {
+        $default = $header;
+        break;
+      }
 
-    if (isset($header['sort']) && ($header['sort'] == 'asc' || $header['sort'] == 'desc')) {
-      $default = array('name' => $header['data'], 'sql' => isset($header['field']) ? $header['field'] : '');
+      if (empty($default) && isset($header['sort']) && ($header['sort'] == 'asc' || $header['sort'] == 'desc')) {
+        $default = $header;
+      }
     }
   }
 
-  if (isset($default)) {
-    return $default;
-  }
-  else {
-    // The first column specified is the initial 'order by' field unless otherwise specified.
-    $first = current($headers);
-    if (is_array($first)) {
-      $first += array('data' => NULL, 'field' => NULL);
-      return array('name' => $first['data'], 'sql' => $first['field']);
-    }
-    else {
-      return array('name' => $first, 'sql' => '');
+  if (!isset($default)) {
+    $default = reset($headers);
+    if (!is_array($default)) {
+      $default = array('data' => $default);
     }
   }
+
+  $default += array('data' => NULL, 'field' => NULL);
+  return array('name' => $default['data'], 'sql' => $default['field']);
 }
 
 /**
@@ -273,12 +235,15 @@ function tablesort_get_order($headers) {
  */
 function tablesort_get_sort($headers) {
   if (isset($_GET['sort'])) {
-    return ($_GET['sort'] == 'desc') ? 'desc' : 'asc';
+    return (strtolower($_GET['sort']) == 'desc') ? 'desc' : 'asc';
   }
-  // User has not specified a sort. Use default if specified; otherwise use "asc".
+  // The user has not specified a sort. Use the default for the currently sorted
+  // header if specified; otherwise use "asc".
   else {
+    // Find out which header is currently being sorted.
+    $ts = tablesort_get_order($headers);
     foreach ($headers as $header) {
-      if (isset($header['sort'])) {
+      if (is_array($header) && isset($header['data']) && $header['data'] == $ts['name'] && isset($header['sort'])) {
         return $header['sort'];
       }
     }
diff --git a/includes/theme.inc b/includes/theme.inc
index 806e5ee..81165b3 100644
--- a/includes/theme.inc
+++ b/includes/theme.inc
@@ -362,7 +362,6 @@ function drupal_theme_rebuild() {
  */
 function _theme_process_registry(&$cache, $name, $type, $theme, $path) {
   $result = array();
-  $function = $name . '_theme';
 
   // Processor functions work in two distinct phases with the process
   // functions always being executed after the preprocess functions.
@@ -371,24 +370,43 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) {
     'process functions'    => 'process',
   );
 
+  $hook_defaults = array(
+    'variables' => TRUE,
+    'render element' => TRUE,
+    'pattern' => TRUE,
+    'base hook' => TRUE,
+  );
+
+  // Invoke the hook_theme() implementation, process what is returned, and
+  // merge it into $cache.
+  $function = $name . '_theme';
   if (function_exists($function)) {
     $result = $function($cache, $type, $theme, $path);
     foreach ($result as $hook => $info) {
+      // When a theme or engine overrides a module's theme function
+      // $result[$hook] will only contain key/value pairs for information being
+      // overridden.  Pull the rest of the information from what was defined by
+      // an earlier hook.
+
+      // Fill in the type and path of the module, theme, or engine that
+      // implements this theme function.
       $result[$hook]['type'] = $type;
       $result[$hook]['theme path'] = $path;
-      // if function and file are left out, default to standard naming
+
+      // If function and file are omitted, default to standard naming
       // conventions.
       if (!isset($info['template']) && !isset($info['function'])) {
         $result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name . '_') . $hook;
       }
-      // If a path is set in the info, use what was set. Otherwise use the
-      // default path. This is mostly so system.module can declare theme
-      // functions on behalf of core .include files.
-      // All files are included to be safe. Conditionally included
-      // files can prevent them from getting registered.
+
       if (isset($cache[$hook]['includes'])) {
         $result[$hook]['includes'] = $cache[$hook]['includes'];
       }
+
+      // If the theme implementation defines a file, then also use the path
+      // that it defined. Otherwise use the default path. This allows
+      // system.module to declare theme functions on behalf of core .include
+      // files.
       if (isset($info['file'])) {
         $include_file = isset($info['path']) ? $info['path'] : $path;
         $include_file .= '/' . $info['file'];
@@ -396,14 +414,10 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) {
         $result[$hook]['includes'][] = $include_file;
       }
 
-      // If these keys are left unspecified within overridden entries returned
-      // by hook_theme(), carry them forward from the prior entry. This is so
-      // that themes don't need to specify this information, since the module
-      // that registered the theme hook already has.
-      foreach (array('variables', 'render element', 'pattern', 'base hook') as $key) {
-        if (!isset($info[$key]) && isset($cache[$hook][$key])) {
-          $result[$hook][$key] = $cache[$hook][$key];
-        }
+      // If the default keys are not set, use the default values registered
+      // by the module.
+      if (isset($cache[$hook])) {
+        $result[$hook] += array_intersect_key($cache[$hook], $hook_defaults);
       }
 
       // The following apply only to theming hooks implemented as templates.
@@ -468,7 +482,7 @@ function _theme_process_registry(&$cache, $name, $type, $theme, $path) {
     }
 
     // Merge the newly created theme hooks into the existing cache.
-    $cache = array_merge($cache, $result);
+    $cache = $result + $cache;
   }
 
   // Let themes have variable processors even if they didn't register a template.
diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info
index 0e3c630..15d2bfd 100644
--- a/modules/simpletest/simpletest.info
+++ b/modules/simpletest/simpletest.info
@@ -32,6 +32,7 @@ files[] = tests/path.test
 files[] = tests/registry.test
 files[] = tests/schema.test
 files[] = tests/session.test
+files[] = tests/tablesort.test
 files[] = tests/theme.test
 files[] = tests/unicode.test
 files[] = tests/update.test
diff --git a/modules/simpletest/tests/tablesort.test b/modules/simpletest/tests/tablesort.test
index e69de29..3462ce3 100644
--- a/modules/simpletest/tests/tablesort.test
+++ b/modules/simpletest/tests/tablesort.test
@@ -0,0 +1,167 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Various tablesort tests.
+ */
+
+/**
+ * Test unicode handling features implemented in unicode.inc.
+ */
+class TableSortTest extends DrupalUnitTestCase {
+
+  /**
+   * Storage for initial value of $_GET.
+   *
+   * @var array
+   */
+  protected $GET = array();
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Tablesort',
+      'description' => 'Tests table sorting.',
+      'group' => 'System',
+    );
+  }
+
+  function setUp() {
+    // Save the original $_GET to be restored later.
+    $this->GET = $_GET;
+
+    parent::setUp();
+  }
+
+  function tearDown() {
+    // Revert $_GET.
+    $_GET = $this->GET;
+
+    parent::tearDown();
+  }
+
+  /**
+   * Test tablesort_init().
+   */
+  function testTableSortInit() {
+
+    // Test simple table headers.
+
+    $headers = array('foo', 'bar', 'baz');
+    // Reset $_GET to prevent parameters from Simpletest and Batch API ending
+    // up in $ts['query'].
+    $_GET = array('q' => 'jahwohl');
+    $expected_ts = array(
+      'name' => 'foo',
+      'sql' => '',
+      'sort' => 'asc',
+      'query' => array(),
+    );
+    $ts = tablesort_init($headers);
+    $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE)))));
+    $this->assertEqual($ts, $expected_ts, t('Simple table headers sorted correctly.'));
+
+    // Test with simple table headers plus $_GET parameters that should _not_
+    // override the default.
+
+    $_GET = array(
+      'q' => 'jahwohl',
+      // This should not override the table order because only complex
+      // headers are overridable.
+      'order' => 'bar',
+    );
+    $ts = tablesort_init($headers);
+    $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE)))));
+    $this->assertEqual($ts, $expected_ts, t('Simple table headers plus non-overriding $_GET parameters sorted correctly.'));
+
+    // Test with simple table headers plus $_GET parameters that _should_
+    // override the default.
+
+    $_GET = array(
+      'q' => 'jahwohl',
+      'sort' => 'DESC',
+      // Add an unrelated parameter to ensure that tablesort will include
+      // it in the links that it creates.
+      'alpha' => 'beta',
+    );
+    $expected_ts['sort'] = 'desc';
+    $expected_ts['query'] = array('alpha' => 'beta');
+    $ts = tablesort_init($headers);
+    $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE)))));
+    $this->assertEqual($ts, $expected_ts, t('Simple table headers plus $_GET parameters sorted correctly.'));
+
+    // Test complex table headers.
+
+    $headers = array(
+      'foo',
+      array(
+        'data' => '1',
+        'field' => 'one',
+        'sort' => 'asc',
+        'colspan' => 1,
+      ),
+      array(
+        'data' => '2',
+        'field' => 'two',
+        'sort' => 'desc',
+      ),
+    );
+    // Reset $_GET from previous assertion.
+    $_GET = array(
+      'q' => 'jahwohl',
+      'order' => '2',
+    );
+    $ts = tablesort_init($headers);
+    $expected_ts = array(
+      'name' => '2',
+      'sql' => 'two',
+      'sort' => 'desc',
+      'query' => array(),
+    );
+    $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE)))));
+    $this->assertEqual($ts, $expected_ts, t('Complex table headers sorted correctly.'));
+
+    // Test complex table headers plus $_GET parameters that should _not_
+    // override the default.
+
+    $_GET = array(
+      'q' => 'jahwohl',
+      // This should not override the table order because this header does not
+      // exist.
+      'order' => 'bar',
+    );
+    $ts = tablesort_init($headers);
+    $expected_ts = array(
+      'name' => '1',
+      'sql' => 'one',
+      'sort' => 'asc',
+      'query' => array(),
+    );
+    $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE)))));
+    $this->assertEqual($ts, $expected_ts, t('Complex table headers plus non-overriding $_GET parameters sorted correctly.'));
+    unset($_GET['sort'], $_GET['order'], $_GET['alpha']);
+
+    // Test complex table headers plus $_GET parameters that _should_
+    // override the default.
+
+    $_GET = array(
+      'q' => 'jahwohl',
+      'order' => '1',
+      'sort' => 'ASC',
+      // Add an unrelated parameter to ensure that tablesort will include
+      // it in the links that it creates.
+      'alpha' => 'beta',
+    );
+    $expected_ts = array(
+      'name' => '1',
+      'sql' => 'one',
+      'sort' => 'asc',
+      'query' => array('alpha' => 'beta'),
+    );
+    $ts = tablesort_init($headers);
+    $this->verbose(strtr('$ts: <pre>!ts</pre>', array('!ts' => check_plain(var_export($ts, TRUE)))));
+    $this->assertEqual($ts, $expected_ts, t('Complex table headers plus $_GET parameters sorted correctly.'));
+    unset($_GET['sort'], $_GET['order'], $_GET['alpha']);
+
+  }
+}
