Index: includes/cache.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/cache.inc,v
retrieving revision 1.28
diff -u -p -r1.28 cache.inc
--- includes/cache.inc	3 Feb 2009 12:30:14 -0000	1.28
+++ includes/cache.inc	3 Mar 2009 02:28:24 -0000
@@ -10,14 +10,56 @@
  *   The cache ID of the data to retrieve.
  * @param $table
  *   The table $table to store the data in. Valid core values are
- *   'cache_filter', 'cache_menu', 'cache_page', or 'cache' for
- *   the default cache.
+ *   'cache_block', 'cache_filter', 'cache_menu', 'cache_node', 'cache_page',
+ *   'cache_registry', or 'cache' for the default cache.
  * @return The cache or FALSE on failure.
  */
 function cache_get($cid, $table = 'cache') {
+  // Garbage collection necessary when enforcing a minimum cache lifetime.
+  _cache_garbage_collection($table);
+  $cache = db_query("SELECT data, created, headers, expire, serialized FROM {" . $table . "} WHERE cid = :cid", array(':cid' => $cid))->fetchObject();
+
+  return _cache_prepare_item($cache);
+}
+
+/**
+ * Return data from the persistent cache when given an array of cache IDs.
+ *
+ * @param $cids
+ *   An array of cache IDs for the data to retrieve.
+ * @param $table
+ *   The table $table to store the data in. Valid core values are
+ *   'cache_block', 'cache_filter', 'cache_menu', 'cache_node', 'cache_page',
+ *   'cache_registry', or 'cache' for the default cache.
+ * @return
+ *   An array of the items successfully returned from cache indexed by cid.
+ */
+function cache_get_multiple(array $cids, $table = 'cache') {
+  // Garbage collection necessary when enforcing a minimum cache lifetime.
+  _cache_garbage_collection($table);
+  $query = db_select($table);
+  $query->fields($table, array('cid', 'data', 'created', 'headers', 'expire', 'serialized'));
+  $query->condition($table . '.cid', $cids, 'IN');
+  $result = $query->execute();
+  $cache = array();
+  foreach ($result as $item) {
+    $item = _cache_prepare_item($item);
+    if ($item) {
+      $cache[$item->cid] = $item;
+    }
+  }
+  return $cache;
+}
+
+/**
+ * Garbage collection for cache_get() and cache_get_multiple().
+ *
+ * @param $table
+ *  The table being requested.
+ */
+function _cache_garbage_collection($table) {
   global $user;
 
-  // Garbage collection necessary when enforcing a minimum cache lifetime
   $cache_flush = variable_get('cache_flush', 0);
   if ($cache_flush && ($cache_flush + variable_get('cache_lifetime', 0) <= REQUEST_TIME)) {
     // Reset the variable immediately to prevent a meltdown in heavy load situations.
@@ -28,8 +70,23 @@ function cache_get($cid, $table = 'cache
       ->condition('expire', $cache_flush, '<=')
       ->execute();
   }
+}
 
-  $cache = db_query("SELECT data, created, headers, expire, serialized FROM {" . $table . "} WHERE cid = :cid", array(':cid' => $cid))->fetchObject();
+
+/**
+ * Utility function for preparing a cached item.
+ *
+ * Checks that items are permanent or have not expired and unserializes
+ * data as appropriate.
+ *
+ * @param $cache
+ *   An item loaded from cache_get() or cache_get_multiple().
+ *
+ * @return
+ *   The item with data unserialized as appropriate or FALSE if there is no
+ *   valid item to load.
+ */
+function _cache_prepare_item($cache) {
   if (isset($cache->data)) {
     // If the data is permanent or we're not enforcing a minimum cache lifetime
     // always return the cached data.
@@ -132,8 +189,8 @@ function cache_set($cid, $data, $table =
  * entries will be cleared from the cache_page and cache_block tables.
  *
  * @param $cid
- *   If set, the cache ID to delete. Otherwise, all cache entries that can
- *   expire are deleted.
+ *   If set, the cache ID to delete, or an array of cache IDs to delete.
+ *   Otherwise, all cache entries that can expire are deleted.
  *
  * @param $table
  *   If set, the table $table to delete from. Mandatory
@@ -199,6 +256,17 @@ function cache_clear_all($cid = NULL, $t
           ->execute();
       }
     }
+    elseif (is_array($cid)) {
+      // This has the potential to cause max_allowed_packet errors with large
+      // large numbers of records, so chunk the deletes.
+      $cache_clear_threshold = variable_get('cache_clear_threshold', 1000);
+      do {
+        db_delete($table)
+          ->condition('cid', array_splice($cid, 0, $cache_clear_threshold), 'IN')
+          ->execute();
+      }
+      while (count($cid));
+    }
     else {
       db_delete($table)
         ->condition('cid', $cid)
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.870
diff -u -p -r1.870 common.inc
--- includes/common.inc	28 Feb 2009 07:36:06 -0000	1.870
+++ includes/common.inc	3 Mar 2009 02:28:33 -0000
@@ -4218,7 +4218,7 @@ function drupal_flush_all_caches() {
   node_types_rebuild();
   // Don't clear cache_form - in-progress form submissions may break.
   // Ordered so clearing the page cache will always be the last action.
-  $core = array('cache', 'cache_block', 'cache_filter', 'cache_registry', 'cache_page');
+  $core = array('cache', 'cache_block', 'cache_filter', 'cache_node', 'cache_registry', 'cache_page');
   $cache_tables = array_merge(module_invoke_all('flush_caches'), $core);
   foreach ($cache_tables as $table) {
     cache_clear_all('*', $table, TRUE);
Index: modules/book/book.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.module,v
retrieving revision 1.486
diff -u -p -r1.486 book.module
--- modules/book/book.module	18 Feb 2009 13:46:53 -0000	1.486
+++ modules/book/book.module	3 Mar 2009 02:28:35 -0000
@@ -720,9 +720,14 @@ function book_build_active_trail($book_l
 }
 
 /**
- * Implementation of hook_nodeapi_load().
+ * Implementation of hook_nodeapi_after_load().
+ *
+ * Add information about book hierarchy to the node object. This does not use
+ * hook_nodeapi_load() since any change in the book hierarchy can alter this
+ * information for a large of number nodes, and because the {menu_links} table
+ * is denormalized, making this query inexpensive.
  */
-function book_nodeapi_load($nodes, $types) {
+function book_nodeapi_after_load($nodes) {
   $result = db_query("SELECT * FROM {book} b INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE b.nid IN (:nids)", array(':nids' =>  array_keys($nodes)), array('fetch' => PDO::FETCH_ASSOC));
   foreach ($result as $record) {
     $nodes[$record['nid']]->book = $record;
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.694
diff -u -p -r1.694 comment.module
--- modules/comment/comment.module	26 Feb 2009 07:30:26 -0000	1.694
+++ modules/comment/comment.module	3 Mar 2009 02:28:43 -0000
@@ -875,6 +875,7 @@ function comment_save($edit) {
       _comment_update_node_statistics($edit['nid']);
       // Clear the cache so an anonymous user can see his comment being added.
       cache_clear_all();
+      cache_clear_all($edit['nid'], 'cache_node');
 
       // Explain the approval queue if necessary, and then
       // redirect the user to the node he's commenting on.
@@ -2004,6 +2005,8 @@ function _comment_update_node_statistics
       ->condition('nid', $nid)
       ->execute();
   }
+  // Clear the node cache.
+  cache_clear_all($nid, 'cache_node');
 }
 
 /**
Index: modules/node/node.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.api.php,v
retrieving revision 1.10
diff -u -p -r1.10 node.api.php
--- modules/node/node.api.php	28 Jan 2009 07:34:30 -0000	1.10
+++ modules/node/node.api.php	3 Mar 2009 02:28:44 -0000
@@ -228,7 +228,7 @@ function hook_nodeapi_insert($node) {
  * parameter. If your module keeps track of the node types it supports, this
  * allows for an early return if nothing needs to be done.
  *
- * Due to the internal cache in node_load_multiple(), you should not use this
+ * Due to the persistent cache in node_load_multiple(), you should not use this
  * hook to modify information returned from the {node} table itself, since
  * this may affect the way nodes are returned from the cache in subsequent
  * calls to the function.
@@ -250,6 +250,45 @@ function hook_nodeapi_load($nodes, $type
 }
 
 /**
+ * Act on nodes after they have been loaded from the database.
+ *
+ * This hook should be used for altering or adding information to the node
+ * object which can not be persistently cached. For example, depending on the
+ * current user or language, or in situations where the data cannot be
+ * refreshed easily without clearing the cache for large numbers of nodes.
+ * You should not alter fields returned from the {node} table here since it may
+ * interfere with the way nodes are retrieved from memory during the course of
+ * a page request.
+ *
+ * @see book_nodeapi_after_load()
+ */
+function hook_nodeapi_after_load($nodes) {
+  global $user;
+
+  foreach ($nodes as $node) {
+    if ($node->type == 'poll') {
+      // Determine whether or not this user is allowed to vote.
+      $node->allowvotes = FALSE;
+     if (user_access('vote on polls') && $node->active) {
+        if ($user->uid) {
+          $result = db_query('SELECT chid FROM {poll_vote} WHERE nid = :nid AND uid = :uid', array(':nid' => $node->nid, ':uid' => $user->uid))->fetch();
+        }
+        else {
+          $result = db_query("SELECT chid FROM {poll_vote} WHERE nid = :nid AND hostname = :hostname", array(':nid' => $node->nid, ':hostname' => ip_address()))->fetch();
+        }
+        if (isset($result->chid)) {
+          $node->vote = $result->chid;
+        }
+        else {
+          $node->vote = -1;
+          $node->allowvotes = TRUE;
+        }
+      }
+    }
+  }
+}
+
+/**
  * The node is about to be shown on the add/edit form.
  *
  * @param $node
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1027
diff -u -p -r1.1027 node.module
--- modules/node/node.module	26 Feb 2009 07:30:27 -0000	1.1027
+++ modules/node/node.module	3 Mar 2009 02:28:48 -0000
@@ -162,7 +162,7 @@ function node_fieldable_info() {
       'revision key' => 'vid',
       'bundle key' => 'type',
       // Node.module handles its own caching.
-      // 'cacheable' => FALSE,
+      'cacheable' => FALSE,
       // Bundles must provide human readable name so
       // we can create help and error messages about them.
       'bundles' => node_get_types('names'),
@@ -195,6 +195,28 @@ function node_field_build_modes($obj_typ
 }
 
 /**
+ * Implementation of hook_modules_enabled().
+ */
+function node_modules_enabled($modules) {
+  foreach ($modules as $module) {
+    if (drupal_function_exists($module . '_load') || drupal_function_exists($module . '_nodeapi_load')) {
+      cache_clear_all('*', 'cache_node', TRUE);
+    }
+  }
+}
+
+/**
+ * Implementation of hook_modules_disabled().
+ */
+function node_modules_disabled($modules) {
+  foreach ($modules as $module) {
+    if (drupal_function_exists($module . '_load') || drupal_function_exists($module . '_nodeapi_load')) {
+      cache_clear_all('*', 'cache_node', TRUE);
+    }
+  }
+}
+
+/**
  * Gather a listing of links to nodes.
  *
  * @param $result
@@ -622,6 +644,7 @@ function node_type_delete($type) {
  */
 function node_type_update_nodes($old_type, $type) {
   db_query("UPDATE {node} SET type = '%s' WHERE type = '%s'", $type, $old_type);
+  cache_clear_all('*', 'cache_node', TRUE);
   return db_affected_rows();
 }
 
@@ -832,20 +855,21 @@ function node_load_multiple($nids = arra
     }
   }
 
-  // Exclude any nodes loaded from cache if they don't match $conditions.
-  // This ensures the same behavior whether loading from memory or database.
-  if ($conditions) {
-    foreach ($nodes as $node) {
-      $node_values = (array) $node;
-      if (array_diff_assoc($conditions, $node_values)) {
-        unset($nodes[$node->nid]);
-      }
+  // Load any available nodes from the persistent cache.
+  $cached_nodes = array();
+  if ($nids && !$vid && !$conditions) {
+    $cache = cache_get_multiple($nids, 'cache_node');
+    foreach ($cache as $item) {
+      $cached_nodes[$item->cid] = $item->data;
     }
+    // Remove any nodes loaded from the $nids to load.
+    $nids = array_keys(array_diff_key(array_flip($nids), $cached_nodes));
   }
 
   // Load any remaining nodes from the database. This is the case if there are
   // any $nids left to load, if loading a revision, or if $conditions was
   // passed without $nids.
+  $queried_nodes = array();
   if ($nids || $vid || ($conditions && !$passed_nids)) {
     $query = db_select('node', 'n');
 
@@ -923,25 +947,57 @@ function node_load_multiple($nids = arra
       $function = $module . '_nodeapi_load';
       $function($queried_nodes, array_keys($typed_nodes));
     }
-    $nodes += $queried_nodes;
-    // Add nodes to the cache if we're not loading a revision.
+
+    // Revisions and unpublished nodes are not cached since they are not loaded
+    // sufficiently often to justify the additional space and complexity
+    // required to do so.
+    if (!$vid) {
+      foreach($queried_nodes as $node) {
+        if ($node->status) {
+          cache_set($node->nid, $node, 'cache_node');
+        }
+      }
+    }
+  }
+
+  // Pass all nodes loaded from the persistent cache or the database through
+  // hook_nodeapi_load_after_load() to allow modules to add information which
+  // cannot be persistently cached.
+  if (!empty($queried_nodes) || !empty($cached_nodes)) {
+    $nodes_to_process = $queried_nodes += $cached_nodes;
+    module_invoke_all('nodeapi_after_load', $nodes_to_process);
+    $nodes = $nodes_to_process + $nodes;
+    // Statically cache the node objects at this point to avoid calling the
+    // hook multiple times in one request.
     if (!$vid) {
-      $node_cache += $queried_nodes;
+      $node_cache = $nodes_to_process + $node_cache;
     }
   }
 
-  // Ensure that the returned array is ordered the same as the original $nids
-  // array if this was passed in and remove any invalid nids.
-  if ($passed_nids) {
-    // Remove any invalid nids from the array.
-    $passed_nids = array_intersect_key($passed_nids, $nodes);
-    foreach ($nodes as $node) {
-      $passed_nids[$node->nid] = $node;
+  // Ensure that the final array of node objects contains the same data in
+  // the same order whether loaded from cache or the node tables.
+  // $passed_nids is keyed in the same order as the original $nids array.
+  // Also remove any node IDs which weren't loaded.
+  $returned_nodes = $passed_nids ? array_intersect_key($passed_nids, $nodes) : array();
+
+  foreach ($nodes as $node) {
+    // Only return nodes which match $conditions.
+    if ($conditions) {
+      $node_values = (array) $node;
+      if (array_diff_assoc($conditions, $node_values)) {
+        unset($returned_nodes[$node->nid]);
+      }
+      else {
+        $returned_nodes[$node->nid] = $node;
+      }
+    }
+    // If no conditions, simply assign the node to the array.
+    else {
+      $returned_nodes[$node->nid] = $node;
     }
-    $nodes = $passed_nids;
   }
 
-  return $nodes;
+  return $returned_nodes;
 }
 
 /**
@@ -1140,6 +1196,7 @@ function node_save(&$node) {
 
   // Clear the page and block caches.
   cache_clear_all();
+  cache_clear_all($node->nid, 'cache_node');
 }
 
 /**
@@ -1188,6 +1245,7 @@ function node_delete($nid) {
     watchdog('content', '@type: deleted %title.', array('@type' => $node->type, '%title' => $node->title));
     drupal_set_message(t('@type %title has been deleted.', array('@type' => node_get_types('name', $node), '%title' => $node->title)));
   }
+  cache_clear_all($node->nid, 'cache_node');
 }
 
 /**
Index: modules/poll/poll.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/poll/poll.module,v
retrieving revision 1.289
diff -u -p -r1.289 poll.module
--- modules/poll/poll.module	18 Feb 2009 14:28:22 -0000	1.289
+++ modules/poll/poll.module	3 Mar 2009 02:28:50 -0000
@@ -403,30 +403,12 @@ function poll_validate($node, $form) {
  * Implementation of hook_load().
  */
 function poll_load($nodes) {
-  global $user;
   foreach ($nodes as $node) {
     $poll = db_query("SELECT runtime, active FROM {poll} WHERE nid = :nid", array(':nid' => $node->nid))->fetch();
 
     // Load the appropriate choices into the $poll object.
     $poll->choice = db_query("SELECT chid, chtext, chvotes, weight FROM {poll_choice} WHERE nid = :nid ORDER BY weight", array(':nid' => $node->nid))->fetchAllAssoc('chid', PDO::FETCH_ASSOC);
 
-    // Determine whether or not this user is allowed to vote.
-    $poll->allowvotes = FALSE;
-    if (user_access('vote on polls') && $poll->active) {
-      if ($user->uid) {
-        $result = db_query('SELECT chid FROM {poll_vote} WHERE nid = :nid AND uid = :uid', array(':nid' => $node->nid, ':uid' => $user->uid))->fetch();
-      }
-      else {
-        $result = db_query("SELECT chid FROM {poll_vote} WHERE nid = :nid AND hostname = :hostname", array(':nid' => $node->nid, ':hostname' => ip_address()))->fetch();
-      }
-      if ($result) {
-        $poll->vote = $result->chid;
-      }
-      else {
-        $poll->vote = -1;
-        $poll->allowvotes = TRUE;
-      }
-    }
     foreach ($poll as $key => $value) {
       $nodes[$node->nid]->$key = $value;
     }
@@ -434,6 +416,38 @@ function poll_load($nodes) {
 }
 
 /**
+ * Implementation of hook_nodeapi_after_load().
+ *
+ * Load user specific information into the node object for a single page
+ * request.
+ */
+function poll_nodeapi_after_load($nodes) {
+  global $user;
+
+  foreach ($nodes as $node) {
+    if ($node->type == 'poll') {
+      // Determine whether or not this user is allowed to vote.
+      $node->allowvotes = FALSE;
+      if (user_access('vote on polls') && $node->active) {
+        if ($user->uid) {
+          $result = db_query('SELECT chid FROM {poll_vote} WHERE nid = :nid AND uid = :uid', array(':nid' => $node->nid, ':uid' => $user->uid))->fetch();
+        }
+        else {
+          $result = db_query("SELECT chid FROM {poll_vote} WHERE nid = :nid AND hostname = :hostname", array(':nid' => $node->nid, ':hostname' => ip_address()))->fetch();
+        }
+        if (isset($result->chid)) {
+          $node->vote = $result->chid;
+        }
+        else {
+          $node->vote = -1;
+          $node->allowvotes = TRUE;
+        }
+      }
+    }
+  }
+}
+
+/**
  * Implementation of hook_insert().
  */
 function poll_insert($node) {
@@ -609,6 +623,7 @@ function poll_vote($form, &$form_state) 
   db_query("UPDATE {poll_choice} SET chvotes = chvotes + 1 WHERE chid = %d", $choice);
 
   cache_clear_all();
+  cache_clear_all($node->nid, 'cache_node');
   drupal_set_message(t('Your vote was recorded.'));
 
   // Return the user to whatever page they voted from.
@@ -782,6 +797,7 @@ function poll_cancel($form, &$form_state
 
   // Subtract from the votes.
   db_query("UPDATE {poll_choice} SET chvotes = chvotes - 1 WHERE chid = %d", $node->vote);
+  cache_clear_all($node->nid, 'cache_node');
 }
 
 /**
Index: modules/simpletest/tests/cache.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/cache.test,v
retrieving revision 1.3
diff -u -p -r1.3 cache.test
--- modules/simpletest/tests/cache.test	8 Dec 2008 21:41:54 -0000	1.3
+++ modules/simpletest/tests/cache.test	3 Mar 2009 02:28:51 -0000
@@ -162,6 +162,50 @@ class CacheSavingCase extends CacheTestC
   }
 }
 
+/**
+ * Test cache_get_multiple().
+ */
+class CacheGetMultipleUnitTest extends CacheTestCase {
+
+  function getInfo() {
+    return array(
+      'name' => t('Fetching multiple cache items'),
+      'description' => t('Confirm that multiple records are fetched correctly.'),
+      'group' => t('Cache'),
+    );
+  }
+
+  function setUp() {
+    $this->default_table = 'cache_page';
+    parent::setUp();
+  }
+  function testCacheMultiple() {
+    $item1 = $this->randomName(10);
+    $item2 = $this->randomName(10);
+    cache_set('item1', $item1, $this->default_table);
+    cache_set('item2', $item2, $this->default_table);
+    $this->assertTrue($this->checkCacheExists('item1', $item1), t('Item 1 is cached.'));
+    $this->assertTrue($this->checkCacheExists('item2', $item2), t('Item 2 is cached.'));
+
+    // Fetch both records from the database with cache_get_multiple().
+    $items = cache_get_multiple(array('item1', 'item2'), $this->default_table);
+    $this->assertEqual($items['item1']->data, $item1, t('Item was returned from cache successfully.'));
+    $this->assertEqual($items['item2']->data, $item2, t('Item was returned from cache successfully.'));
+
+    // Remove one item from the cache.
+    cache_clear_all('item2', $this->default_table);
+
+    // Confirm that only one item is returned by cache_get_multiple().
+    $items = cache_get_multiple(array('item1', 'item2'), $this->default_table);
+    $this->assertEqual($items['item1']->data, $item1, t('Item was returned from cache successfully.'));
+    $this->assertFalse(isset($items['item2']), $item2, t('Item was not returned from the cache.'));
+    $this->assertTrue(count($items) == 1, t('Only valid cache entries returned.'));
+  }
+}
+
+/**
+ * Test cache clearing methods.
+ */
 class CacheClearCase extends CacheTestCase {
   function getInfo() {
     return array(
@@ -224,4 +268,41 @@ class CacheClearCase extends CacheTestCa
                       || $this->checkCacheExists('test_cid_clear2', $this->default_value),
                       t('Two caches removed after clearing cid substring with wildcard true.'));
   }
-}
\ No newline at end of file
+
+  /**
+   * Test clearing using an array.
+   */
+  function testClearArray() {
+    // Create three cache entries.
+    cache_set('test_cid_clear1', $this->default_value, $this->default_table);
+    cache_set('test_cid_clear2', $this->default_value, $this->default_table);
+    cache_set('test_cid_clear3', $this->default_value, $this->default_table);
+    $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
+                      && $this->checkCacheExists('test_cid_clear2', $this->default_value)
+                      && $this->checkCacheExists('test_cid_clear3', $this->default_value),
+                      t('Three cache entries were created.'));
+
+    // Clear two entries using an array.
+    cache_clear_all(array('test_cid_clear1', 'test_cid_clear2'), $this->default_table);
+    $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value)
+                       || $this->checkCacheExists('test_cid_clear2', $this->default_value),
+                       t('Two cache entries removed after clearing with an array.'));
+
+    $this->assertTrue($this->checkCacheExists('test_cid_clear3', $this->default_value),
+                      t('Entry was not cleared from the cache'));
+
+    // Set the cache clear threshold to 2 to confirm that the full table is cleared
+    // when the threshold is exceeded.
+    variable_set('cache_clear_threshold', 2);
+    cache_set('test_cid_clear1', $this->default_value, $this->default_table);
+    cache_set('test_cid_clear2', $this->default_value, $this->default_table);
+    $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
+                      && $this->checkCacheExists('test_cid_clear2', $this->default_value),
+                      t('Two cache entries were created.'));
+    cache_clear_all(array('test_cid_clear1', 'test_cid_clear2', 'test_cid_clear3'), $this->default_table);
+    $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value)
+                       || $this->checkCacheExists('test_cid_clear2', $this->default_value)
+                       || $this->checkCacheExists('test_cid_clear3', $this->default_value),
+                       t('All cache entries removed when the array exceeded the cache clear threshold.'));
+  }
+}
Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.311
diff -u -p -r1.311 system.install
--- modules/system/system.install	1 Mar 2009 09:32:17 -0000	1.311
+++ modules/system/system.install	3 Mar 2009 02:29:02 -0000
@@ -595,6 +595,8 @@ function system_schema() {
   $schema['cache_page']['description'] = 'Cache table used to store compressed pages for anonymous users, if page caching is enabled.';
   $schema['cache_menu'] = $schema['cache'];
   $schema['cache_menu']['description'] = 'Cache table for the menu system to store router information as well as generated link trees for various menu/page/user combinations.';
+  $schema['cache_node'] = $schema['cache'];
+  $schema['cache_node']['description'] = 'Cache table for the node module to store fully loaded node objects.';
   $schema['cache_registry'] = $schema['cache'];
   $schema['cache_registry']['description'] = 'Cache table for the code registry system to remember what code files need to be loaded on any given page.';
 
@@ -3237,6 +3239,17 @@ function system_update_7020() {
 }
 
 /**
+ * Create the cache_node table.
+ */
+function system_update_7021() {
+  $ret = array();
+  $schema['cache_node'] = drupal_get_schema_unprocessed('system', 'cache');
+  $schema['cache_node']['description'] = t('Cache table used to store the output of node_load.');
+  db_create_table($ret, 'cache_node', $schema['cache_node']);
+  return $ret;
+}
+
+/**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
  */
Index: modules/taxonomy/taxonomy.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v
retrieving revision 1.461
diff -u -p -r1.461 taxonomy.module
--- modules/taxonomy/taxonomy.module	11 Feb 2009 03:53:36 -0000	1.461
+++ modules/taxonomy/taxonomy.module	3 Mar 2009 02:29:02 -0000
@@ -319,6 +319,8 @@ function taxonomy_term_save($term) {
   else {
     $status = drupal_write_record('taxonomy_term_data', $term);
     module_invoke_all('taxonomy_term_update', $term);
+    // This change can affect a large number of nodes, so clear the node cache.
+    taxonomy_clear_node_cache($term->tid);
   }
   
   $or = db_or()->condition('tid1', $term->tid)->condition('tid2', $term->tid);
@@ -392,6 +394,9 @@ function taxonomy_term_delete($tid) {
 
       $term = taxonomy_term_load($tid);
 
+      // Clear the node cache for all nodes associated with this term.
+      taxonomy_clear_node_cache($term->tid);
+
       db_query('DELETE FROM {taxonomy_term_data} WHERE tid = %d', $tid);
       db_query('DELETE FROM {taxonomy_term_hierarchy} WHERE tid = %d', $tid);
       db_query('DELETE FROM {taxonomy_term_relation} WHERE tid1 = %d OR tid2 = %d', $tid, $tid);
@@ -1480,3 +1485,14 @@ function taxonomy_hook_info() {
     ),
   );
 }
+
+/**
+ * Clear the node cache for all nodes associated with a term.
+ *
+ * @param $tid
+ *   The taxonomy term ID.
+ */
+function taxonomy_clear_node_cache($tid) {
+  $nids = db_query('SELECT nid FROM taxonomy_term_node WHERE tid = :tid', array(':tid' => $tid))->fetchCol();
+  cache_clear_all($nids, 'cache_node');
+}
Index: modules/translation/translation.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/translation/translation.module,v
retrieving revision 1.39
diff -u -p -r1.39 translation.module
--- modules/translation/translation.module	25 Feb 2009 13:39:20 -0000	1.39
+++ modules/translation/translation.module	3 Mar 2009 02:29:03 -0000
@@ -227,6 +227,7 @@ function translation_nodeapi_insert($nod
         db_query("UPDATE {node} SET tnid = %d, translate = %d WHERE nid = %d", $tnid, 0, $node->translation_source->nid);
       }
       db_query("UPDATE {node} SET tnid = %d, translate = %d WHERE nid = %d", $tnid, 0, $node->nid);
+      translation_clear_node_cache($tnid);
     }
   }
 }
@@ -244,6 +245,7 @@ function translation_nodeapi_update($nod
         // This is the source node, asking to mark all translations outdated.
         db_query("UPDATE {node} SET translate = 1 WHERE tnid = %d AND nid <> %d", $node->tnid, $node->nid);
       }
+      translation_clear_node_cache($node->tnid);
     }
   }
 }
@@ -278,10 +280,28 @@ function translation_remove_from_set($no
         db_query('UPDATE {node} SET tnid = %d WHERE tnid = %d', $new_tnid, $node->tnid);
       }
     }
+    translation_clear_node_cache($node->tnid);
   }
 }
 
 /**
+ * Clear the node_load() cache for all nodes in the translation set.
+ *
+ * This ensures we have the proper translation set information in every node.
+ *
+ * @param $tnid
+ *   The translation source nid of the translation set, the identifier
+ *   of the node used to derive all translations in the set.
+ */
+function translation_clear_node_cache($tnid) {
+  $nids = array();
+  foreach (translation_node_get_translations($tnid) as $node) {
+    $nids[] = $node->nid;
+  }
+  cache_clear_all($nids, 'cache_node');
+}
+
+/**
  * Get all nodes in a translation set, represented by $tnid.
  *
  * @param $tnid
