Index: includes/cache.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/cache.inc,v
retrieving revision 1.27
diff -u -p -r1.27 cache.inc
--- includes/cache.inc	12 Oct 2008 04:30:05 -0000	1.27
+++ includes/cache.inc	8 Dec 2008 13:03:47 -0000
@@ -15,9 +15,50 @@
  * @return The cache or FALSE on failure.
  */
 function cache_get($cid, $table = 'cache') {
-  global $user;
-
   // 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 and cache misses 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_filter', 'cache_menu', 'cache_page', or 'cache' for
+ *   the default cache.
+ *
+ * @return
+ *   An array in the form array('hits' => $cache, 'misses' =>$cids), cached
+ *   items are indexed by cid.
+ */
+function cache_get_multiple($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);
+    $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;
   $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 +69,19 @@ 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.
+ *
+ * @param $cache
+ *   An item loaded from cache_get or cache_get_multiple
+ *
+ * @return
+ *   The item with data unserialized as appropriate.
+ */
+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.
Index: modules/book/book.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.module,v
retrieving revision 1.476
diff -u -p -r1.476 book.module
--- modules/book/book.module	5 Dec 2008 22:18:44 -0000	1.476
+++ modules/book/book.module	8 Dec 2008 13:03:47 -0000
@@ -521,6 +521,8 @@ function _book_update_outline(&$node) {
         book_update_bid($node->book);
       }
     }
+    // Completely clear the node_load() cache since this change can affect many nodes.
+    cache_clear_all('*', 'cache_node', TRUE);
 
     return TRUE;
   }
Index: modules/book/book.pages.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.pages.inc,v
retrieving revision 1.12
diff -u -p -r1.12 book.pages.inc
--- modules/book/book.pages.inc	16 Nov 2008 15:02:45 -0000	1.12
+++ modules/book/book.pages.inc	8 Dec 2008 13:03:47 -0000
@@ -214,6 +214,8 @@ function book_remove_form_submit($form, 
       ->condition('nid', $node->nid)
       ->execute();
     drupal_set_message(t('The post has been removed from the book.'));
+    // Completely clear the node_load() cache since this change can affect many nodes.
+    cache_clear_all('*', 'cache_node', TRUE);
   }
   $form_state['redirect'] = 'node/' . $node->nid;
 }
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.669
diff -u -p -r1.669 comment.module
--- modules/comment/comment.module	5 Dec 2008 22:18:44 -0000	1.669
+++ modules/comment/comment.module	8 Dec 2008 13:03:49 -0000
@@ -837,6 +837,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.
@@ -1954,6 +1955,8 @@ function _comment_update_node_statistics
       ->condition('nid', $nid)
       ->execute();
   }
+  // Clear the node cache.
+  cache_clear_all($nid, 'cache_node');
 }
 
 /**
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1001
diff -u -p -r1.1001 node.module
--- modules/node/node.module	5 Dec 2008 22:18:45 -0000	1.1001
+++ modules/node/node.module	8 Dec 2008 13:03:50 -0000
@@ -569,6 +569,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();
 }
 
@@ -779,20 +780,20 @@ function node_load_multiple($nids = arra
     }
   }
 
-  // Exclude any nodes loaded from cache if they don't match $conditions.
-  // This ensures the same behaviour 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;
+      unset($nids[$item->cid]);
     }
   }
 
   // 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');
 
@@ -836,48 +837,80 @@ function node_load_multiple($nids = arra
       }
     }
     $queried_nodes = $query->execute()->fetchAllAssoc('nid');
-  }
+    // Pass all nodes loaded from the database through the node type specific
+    // callbacks and hook_nodeapi_load(), then add them to the internal cache.
+    if (!empty($queried_nodes)) {
+      // Create an array of nodes for each content type and pass this to the
+      // node type specific callback.
+      $typed_nodes = array();
+      foreach ($queried_nodes as $nid => $node) {
+        $typed_nodes[$node->type][$nid] = $node;
+      }
 
-  // Pass all nodes loaded from the database through the node type specific
-  // callbacks and hook_nodeapi_load(), then add them to the internal cache.
-  if (!empty($queried_nodes)) {
-    // Create an array of nodes for each content type and pass this to the
-    // node type specific callback.
-    $typed_nodes = array();
-    foreach ($queried_nodes as $nid => $node) {
-      $typed_nodes[$node->type][$nid] = $node;
-    }
-
-    // Call node type specific callbacks on each typed array of nodes.
-    foreach ($typed_nodes as $type => $nodes_of_type) {
-      if (node_hook($type, 'load')) {
-        $function = node_get_types('base', $type) . '_load';
-        $function($nodes_of_type);
+      // Call node type specific callbacks on each typed array of nodes.
+      foreach ($typed_nodes as $type => $nodes_of_type) {
+        if (node_hook($type, 'load')) {
+          $function = node_get_types('base', $type) . '_load';
+          $function($nodes_of_type);
+        }
       }
-    }
 
-    // Call hook_nodeapi_load(), pass the node types so modules can return early
-    // if not acting on types in the array.
-    foreach (module_implements('nodeapi_load') as $module) {
-      $function = $module . '_nodeapi_load';
-      $function($queried_nodes, array_keys($typed_nodes));
+      // Call hook_nodeapi_load(), pass the node types so modules can return early
+      // if not acting on types in the array.
+      foreach (module_implements('nodeapi_load') as $module) {
+        $function = $module . '_nodeapi_load';
+        $function($queried_nodes, array_keys($typed_nodes));
+      }
+      // Cache the nodes if they aren't revisions.
+      if (!$vid) {
+        foreach($queried_nodes as $node) {
+          cache_set($node->nid, $node, 'cache_node');
+        }
+      }
     }
-    $nodes += $queried_nodes;
-    // Add nodes to the cache if we're not loading a revision.
+  }
+
+  // Pass all nodes loaded from the persistent cache or the database through
+  // hook_nodeapi_post_load() to allow modules to add information which can
+  // not be persistently cached.
+  if (!empty($queried_nodes) || !empty($cached_nodes)) {
+    $nodes_to_process = $queried_nodes += $cached_nodes;
+    foreach (module_implements('nodeapi_post_load') as $module) {
+      $function = $module . '_nodeapi_post_load';
+      $function($nodes_to_process);
+    }
+    $nodes += $nodes_to_process;
+    // 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;
     }
   }
 
-  // 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);
+  // Ensure that the final array of node objects contains the same data in
+  // the same order whether loaded from cache or the node tables.
+  if (!$vid) {
+
+    // $passed_nids is keyed in the same order as the original $nids array.
+    // Remove any node IDs which weren't loaded.
+    $nids = $passed_nids ? array_intersect_key($passed_nids, $nodes) : array();
+
     foreach ($nodes as $node) {
-      $passed_nids[$node->nid] = $node;
+      // Only return nodes which match $conditions.
+      if ($conditions) {
+        $node_values = (array) $node;
+        if (array_diff_assoc($conditions, $node_values)) {
+          unset($nids[$node->nid]);
+        }
+        else {
+          $nids[$node->nid] = $node;
+        }
+      }
+      else {
+        $nids[$node->nid] = $node;
+      }
     }
-    $nodes = $passed_nids;
+    $nodes = $nids;
   }
 
   return $nodes;
@@ -1069,6 +1102,7 @@ function node_save(&$node) {
 
   // Clear the page and block caches.
   cache_clear_all();
+  cache_clear_all($node->nid, 'cache_node');
 }
 
 /**
@@ -1117,6 +1151,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.279
diff -u -p -r1.279 poll.module
--- modules/poll/poll.module	5 Dec 2008 22:18:45 -0000	1.279
+++ modules/poll/poll.module	8 Dec 2008 13:03:52 -0000
@@ -481,6 +481,38 @@ function poll_load($nodes) {
 }
 
 /**
+ * Implementation of hook_nodeapi_post_load().
+ *
+ * Load user specific information into the node object for a single page
+ * request.
+ */
+function poll_nodeapi_post_load($nodes) {
+  global $user;
+
+  foreach ($nodes as $node) {
+    if ($node->type == 'poll') {
+      // Determine whether or not this user is allowed to vote.
+      $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) {
@@ -656,6 +688,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.
@@ -829,6 +862,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/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.291
diff -u -p -r1.291 system.install
--- modules/system/system.install	3 Dec 2008 16:32:22 -0000	1.291
+++ modules/system/system.install	8 Dec 2008 13:03:53 -0000
@@ -607,6 +607,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.';
 
@@ -3143,6 +3145,17 @@ function system_update_7015() {
 }
 
 /**
+ * Create the cache_node table.
+ */
+function system_update_7016() {
+  $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.445
diff -u -p -r1.445 taxonomy.module
--- modules/taxonomy/taxonomy.module	5 Dec 2008 22:18:46 -0000	1.445
+++ modules/taxonomy/taxonomy.module	8 Dec 2008 13:03:54 -0000
@@ -333,8 +333,10 @@ function taxonomy_term_save($term) {
   else {
     $status = drupal_write_record('term_data', $term);
     module_invoke_all('taxonomy_term_update', $term);
+    // This change can affect a large number of nodes, so clear the node cache.
+    cache_clear_all('*', 'cache_node', TRUE);
   }
-  
+
   $or = db_or()->condition('tid1', $term->tid)->condition('tid2', $term->tid);
   db_delete('term_relation')->condition($or)->execute();
 
@@ -419,6 +421,8 @@ function taxonomy_term_delete($tid) {
   }
 
   cache_clear_all();
+  // This change can affect a lot of nodes, so clear the node cache.
+  cache_clear_all('*', 'cache_node', TRUE);
 
   return SAVED_DELETED;
 }
Index: modules/translation/translation.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/translation/translation.module,v
retrieving revision 1.34
diff -u -p -r1.34 translation.module
--- modules/translation/translation.module	11 Nov 2008 16:49:38 -0000	1.34
+++ modules/translation/translation.module	8 Dec 2008 13:03:54 -0000
@@ -221,6 +221,7 @@ function translation_nodeapi_insert(&$no
         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);
     }
   }
 }
@@ -238,6 +239,7 @@ function translation_nodeapi_update(&$no
         // 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);
     }
   }
 }
@@ -272,6 +274,21 @@ 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 node_load cache for all nodes in the translation set, so 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) {
+  foreach (translation_node_get_translations($tnid) as $node) {
+    cache_clear_all($node->nid, 'cache_node');
   }
 }
 
